feat(calibration): finalize Step 2 Risk & Stops with inline PDF reports and visual validation

This commit is contained in:
DaM
2026-02-13 20:56:34 +01:00
parent 44667df3dd
commit f4f4e8e5be
20 changed files with 184 additions and 1925 deletions

8
.gitignore vendored
View File

@@ -20,4 +20,10 @@ logs/
# Resultados # Resultados
backtest_results/ backtest_results/
output/ output/
# Ignorar SOLO la reports de la raíz
/reports/
# Pero permitir reports dentro de src
!src/calibration/reports

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,93 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260212210317+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260212210317+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1380
>>
stream
GatU3gJ[&k&:Ml+oF"lkWPV33AD+U)@:uT`QWc#5H(p0/M#n`]37uM1m+6/K<fCO9JO#)pBsh-bh5=I/$3WTCISZ:1-SZThV*Gp:F.h=K&H;i^`hUS:RgI>-j98;=;X-qp`]R!B"VuDJ%lgR?@2ropQE9N4,V@rC"VN.sOkaQK8CA5Xl?/OUq74)c5X.R4'B7?XoEB./@N2,-?5Q-T_CX*]$9C+-aiG-X%.Rsm"3(b`UTY*)"\e81]7Y+Tp/crS""VEY8cA^>TI=+/><ms*p+<p(5S6urRo?t[I1'Z]-(&ral[*)hDr4t#9s6CJeMC%@0egIr6c'=QP*'86+&`L'j2m>1RThS_$WRc6K;a$O$N8$tWmIK7*_b!^Tmml,>6hIi+[UL+O.HcIW9s%B!KeO\jqA?/E1,Y"6NW%!+O`%E]S!]X[U!>H^o?GTear>i?.:"KP\=0uS]$_emoQE,]4(ca0:A@^ofQ/Gh/1MQo(t%<_TmY!:=IIf!<'E!MW\kuV)BXpkE24%EU@-.s)[6-6DK&to6L-@Vr2:TaeeYV%Mj"pA]0GHNhiOM:;0op#>U#+XUHFRb&C2"W*;r&(14GqL!VHL$;'u&.^Mg>-[2%8kPP$(K^@eN%n-_\+o[=sj<#-O.%]$RN,b;daLWe[88;qi>%^Z>J^n^&5.LSKaDBjm!E,PhhlolI`7@FhpXBZWXLkm-cD7)r`qh5b:UB\-ctKtAI>$jJ3@<`nM!:0sSa!P)D-?IZp,dmYm,pY^qmjHpqgGY1bC8BC2qspLq.A#Y[dadp9`mV;p.YXU"fg592$B]^?Ltj:Gjt1ZHaDXQ7(QMtWRP(t)G);mM526%qp.sXXpQr6#M:`1eI6Is6`0O>#"ai,jViX(/i5[:`B=q0)DL*L0@%;qA=V!>AK:I&<#7#e@)k%5f@,Mf(BWj!)Pcu>aU=Nt!e]9Q\dfl-+6X93Zo#+N4)0qWZsGJsVT/O+m7c\'2RT)%Yq5Pp9K1`$-&_0q5ZCC"P+f.i;`R:In@2?8?5l0]NV"m3fJJZ.[_p*uhl'm!<]($-XltC>Wq78tG.V"]H8flUI2b0]HgK%@]MPZ.L..OpjN%mtV^mK#kWc?Cj?K3kbY`^H35`"H3P!P?\J:B;9(P,@hH/^2D5iE6@#:p`l6WUAY3jU6^u6YGaV;-WQsU3Fdg97D.#g9+/i;5Tjrup7Xb5(S*DA*G319IU.a$ML16f@B<F/F3n>8g3T(R_a/!Wcs1L4P5FKgnGPCM'9*EF&)NY/iD@s=A8e`%%OV!lFh@%`uOO,V>]A>\824pE&=3t2O001(#1mEG4.f4:k@0p;halQ/cj)Vq/#F`ri-jK!D]JW-[u5Xs<u+<u5GpF,qUqH]%foKT.N!(6~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 522
>>
stream
GatUpa\K`-&;KY%MRaiEKT,<:9.KVOFooUFA#7>T:1ZuVi.UdfEC.U@3^l>sEgX,os(6_,#[m[pp:j]3*2[[o8q9`/1E\E0+[(IAFS%9P19sIsLj*h\-!c@p;A9d1J4^Np4SD:r0M\0BRUQGU[Y>[]!fg5tDOmee/`9g!eogLU[F.<a+h`MMY:4n]7DESiO9A.C8<<!K\4sK`G5r%k,C+6#4aG4()p0m*E,DV%\1qnRM::a=BqL;*>//Utpf.-]@VdP#r;cA?`%;735q["cg:f*/VI;A@9<Id</kgi_7n!(A0G<H'P(>aYl35<P9N.V]%-(ZWL]p@`1m$cDbOhMe:\p"401-mS&-[uAUU]I79E?AmGBtu_'Z/cP8BBiY[V`b)!l7TJ9/s4GAlauK><:?C13qCTb/;g#_:I/?V4N4;?*A;$.6cC`aq%pP3ONPsT>6.4F>$#6-C/K*/XmW*ht'`-A![jI;P-(8_2Z=Md#HJpY93AiPe#1Z!YO^G]1dV0+u"<JK'?CC~>endstream
endobj
xref
0 11
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000524 00000 n
0000000728 00000 n
0000000796 00000 n
0000001076 00000 n
0000001141 00000 n
0000002612 00000 n
trailer
<<
/ID
[<b164637c0884769535c99f7ca3ce39d9><b164637c0884769535c99f7ca3ce39d9>]
% ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R
/Root 6 0 R
/Size 11
>>
startxref
3225
%%EOF

File diff suppressed because one or more lines are too long

View File

@@ -1,93 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260212210640+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260212210640+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1380
>>
stream
GatU3gJ[&k&:Ml+oF"lkWPV33j9olnfU8bqV3[$PSuAB:UO\+F%^n/2p?T16.un+eJO#)pBsh-bh5=I/$3WTCI)cVFZNs=?;P4Gm),Ta?M#MriUXA\Sc3/G;F?D'D.CCK^@h,D#"Vs'MpGf0(?lW`uROfQ=/1oeGGs4p_4LrdKOebX[<i\'`nk8eOJ:<+6-UaT7kptP2B,c6UGCgqG&8$cR<m\D6aU]C(q>pSSVSJS=GOh""21UPZA.LB^gA[sG.q%K63m"#P%htk@Hjf._cb=t^"=V+o?uq4WZ#@JF^BE"=@=1k0n')f1^T"%rZt]rR3!Dd=G>-XD:,6i^]MhS`HSjDs>'"KAG+nInG7YKB;5#)B5nt59e:C3&/Ca-_Dem[;GDP`/cUCWU9[rc[6BCmBLZT6^/TI3:^]OYYL`fVMqn.]nl*c"LHGHN=im#[+5+U:Jf9BrFWLZJAPOCHn;7iP-bUo6R/CZ57$e9+@C@]Ct;o6b.M;r]k/3lA%PN&Fn>cCF(r]-dOVHs'8rr3ruK$_l>d!T/L_^X5S4h38\`5*F2N/UjNfosobb/m`N>gLQ3Ya&XM8f8+2)7>CeDXg^7>mTh,Wpo8(9P&sXZfGP#k'S7XL%'Hbaog%B@YfLX/GY&)"cAXH%qD]</@$nN=H]*k[8IkV=C;m!NfS'B%5OD4@XurEni*Yu4>HL@fc?QuJ9AI>e!]hA?!Eq5E\gEKK1=$!lm#3uR:RQPL6u;He<sWIf9S;_B>6=V_LCoqZCeaaZJTfIZg7o<HIM8LQ`oScAK/tbhDB2hnBa"r=WiWF?:-uXi&0D-a+JmA5*uMW,LXhg8u9gbejrtQ9-i>dh1'ann#@KtI=MX!>YZHo2'f>k1e;X$/6;Q+I]d(B'TZ'Q._pSX^,Wa!PAr3:P]>K0F&g:lNZH=(;8l=V0*K^!2+Hn[P7oXB$Oh(9Bfh!0I%WjrI7#dm\YCL.9'[4ECMM(qe1O^l@oM,b'$F1V_fWAI?S_(f!(=5*0,Y[T>Es9:$I<LemBmPEcq++uo6'Y,CccI?p7;,8@?j1km>=)`V+T&WA@B\CZ?IOogUE;F0ur]=aU>Y<(!P/!.7]-64C(SO8,nF2-i^14DMHl__&89Xmc6jLqGpRI.F%<IH!cC9\"tQ"K-dZuWL(-K]!aut",EMQ,u`;O@;[OD:Fg7\UGV>M\PVbARu;70[1:KkFO7RWj!=%E<0Xk!ALOsOWk=kuhO>u0HIha!X].3p03"&[nR*Su-i^ce40142&gMK[`pP[?jnoS$V!Ig7d6KX9)/>Y1A?+M55'd\L3XlF301:/3mEF(Sf4:k@1%UGieii:X27c:0lKfS8a]Ie%!bkdnJ[DMH5[*m,mSUJUqH](goKSS3!(-~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 522
>>
stream
GatUpa\K`-&;KY%MRaiEKT,<:9.KVOFooUFA#7>T:1ZuVi.UdfEC.U@3^l>sEgX,os(6_,#[m[pp:j]3*2[[o8q9`/1E\E0+[(IAFS%9P19sIsLj*h\-!c@p;A9d1J4^Np4SD:r0M\0BRUQGU[Y>[]!fg5tDOmee/`9g!eogLU[F.<a+h`MMY:4n]7DESiO9A.C8<<!K\4sK`G5r%k,C+6#4aG4()p0m*E,DV%\1qnRM::a=BqL;*>//Utpf.-]@VdP#r;cA?`%;735q["cg:f*/VI;A@9<Id</kgi_7n!(A0G<H'P(>aYl35<P9N.V]%-(ZWL]p@`1m$cDbOhMe:\p"401-mS&-[uAUU]I79E?AmGBtu_'Z/cP8BBiY[V`b)!l7TJ9/s4GAlauK><:?C13qCTb/;g#_:I/?V4N4;?*A;$.6cC`aq%pP3ONPsT>6.4F>$#6-C/K*/XmW*ht'`-A![jI;P-(8_2Z=Md#HJpY93AiPe#1Z!YO^G]1dV0+u"<JK'?CC~>endstream
endobj
xref
0 11
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000524 00000 n
0000000728 00000 n
0000000796 00000 n
0000001076 00000 n
0000001141 00000 n
0000002612 00000 n
trailer
<<
/ID
[<fc42dcfa3952c93b73ce887dcde189a4><fc42dcfa3952c93b73ce887dcde189a4>]
% ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R
/Root 6 0 R
/Size 11
>>
startxref
3225
%%EOF

File diff suppressed because one or more lines are too long

View File

@@ -1,74 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260211220412+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260211220412+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 854
>>
stream
GatUr9lo#B&;KZOMS6Aa14pgnP&0=4cmCRIQqi7^;_AOqM,T?R>7BKJG:aa`Ld.aN%p5oVf<9?>#q-@tW;2s]!*:=K>Z7:(_?Ed(E!X4rQZ2RXU5^O5i-CpSqgje?3.W5pT*>0ln,c)_Sr[6r(`9;O>[0(XI[-.bX*B6d1g25e7AQ,`"a;HB$4RM>H8*et#?HoSP4CPZ6rTD<B+VdBGRfpir$^tKY=!k-lieG+ES&XQ'6CYh<\;1QJXA3['o[(\8tfE'6_lAd&LJlY9dgGg]d3N=1<5YBE%0Rte"+uhEAZkl-iIFL*3[=i3Ooab)fa>CV:D('H`m"pMrHH]\l_\KW:b*WOMp`N*8aW=$GuuO/dP/?nV)S6q172Yd\.Rp#nf5"ehE\4e&Q9=8[b-+ft1ODdu4+VkHNc>!cs?*R&"Io7&fHHfQcuMY#N9^J""]ioh'j_>pjsQSZ_[`"CDYDgoAeO`V7Qs&VL@O_X[Tm#,bk8X(5s[M1[+uN/]Y@EaZ.WlH+6iY&%o6f\*Jr[1*AKNKH93\i7I7mr?/aeqL)bhW^C[P,EH9l2+j'_X3&Bqm8`-&:>\#iPQ<4oJZ0SV=BLddD\aGW&^Yo<Ic]r=M_oM_o(.Mf#/.&7/r)"+OA3K*0?!Mg^FCm2mH$r+PB),Ps$ao9q6&)PL^?4Ik^V!K-:_Z0]0LpQs4QI>NXhjD\BLOcUB5XS=PPOJ`IXHjMe`<]Q1^P4g6s6hZIa=:>H#s(5VUoZkSUMkDgAO(P8:::.F/i41Z$V]@[Cr[]qQq[*eR$lW6f+?,t"4^Aua&BGEm;*?]G#4G0Ft6^m&%a/+A^U7X_9/&6m0I=6:LN>miU!!FD`U&~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000524 00000 n
0000000592 00000 n
0000000872 00000 n
0000000931 00000 n
trailer
<<
/ID
[<2ae2cf0ad41711ce62e7e9efe6438c88><2ae2cf0ad41711ce62e7e9efe6438c88>]
% ReportLab generated PDF document -- digest (opensource)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1875
%%EOF

View File

@@ -1,74 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260212201938+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260212201938+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 929
>>
stream
GatUr9lldX&;KZQ'mjZ8ac)'%"G5DIVo_We9UhFAPRB4O6Y_P^ni6I>?%hPEiFD-kgPFBpos/nN5_7iZp&@,T\-V$ZM?7?#/:_c!6gt2l#,@NIl1'W@hf=\sXji$^N*2YX$I#hmmW*D@?lW]c'X*jBOXbT`Y(15rMBB0$`T\</j<i-1rE$Ln5YiMIL>]EEI>a<'`s*]?ja"Rq1.sLs+Is<)$/]cNiSe>Z!FphtZRZ5Z$RY_kQ8%`qI\u01=P:EHo0;^^:$F"^$_#T)CO&X*NdhVmm,D%4HSP2_@)Yk(g_66*I0\eJ\X'0p7M)S@@"c'(_C!o,rH%nj*o%"L>1/j<1V6;Fle@&lGc;&5j'%_G<Da.$kuu21eMjldg(WHObI''g/orN6>J<a`a.[cJ7;En*E8Vdg#qF"A/5Bu,Rq\pUklRcO`S"JdR'%p%.o+SU\6T6dZhrEed-g*!ZV.Ua6nYc!Okg%t-\(Cgjll[so6IoQ/!)F==M8j4/%mCJO$tO97;BTrms/G(K_d<Tk5"E`]3r+oA>4.%N>9[EN>N>.=Xl%8kD+&cCM_YUgMhj=)`C;-21t/ji7f#P#jihd&bJUlR3m%g0=U&7n)Mr<6nbb!F#m8/n`TZnIooN4]kr(O0qP)]\r\Y.l5I'm)(7&?&g#&b*_lp]-C-FfohG2gRl$1KI71[0\hOcPN5b0XqEZd/7p3?[dbS<XEWl7]dKMEP@Uk5dK1hL%b.+lcoC)=@J1^D7\!)8Y+%s@cQ-Zbnps=rmcb+Y;l7Lcc%.E!V[dP<M%EpF1eMaTZ+G[pOhUago5JJ:7_8d;TVVpB1V4_rWNNm%@D6H*P7@35<+@GSuH5XVC"F3j3k1,V!EK5caK,9S!$+j;&JU3K0$Qf#DZgDiIB<u*)':+)aWY\sNPU_VnH*8,abo2Q?!Wm[P3W~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000524 00000 n
0000000592 00000 n
0000000872 00000 n
0000000931 00000 n
trailer
<<
/ID
[<fbf718fd6966c08977bc531039e9de4e><fbf718fd6966c08977bc531039e9de4e>]
% ReportLab generated PDF document -- digest (opensource)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1950
%%EOF

View File

@@ -1,74 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260212202036+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260212202036+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 930
>>
stream
GatUr9lldX&;KZQ'mjZ8ac)'%"G5DIVo_We9UhFAPRB4O6Y\ddni6I>>m,>$n1f(1fZ:,Dl=KX%"U0Joe\B95i51,Q.ghMl[fX\]%c.`kl:kS3<KC(#\;";Bd7UGNS2#Xp6%M%Na_d%j&PTm^&&NQ#(Q!_KAdb"'S+&0(<LC^D-ds?Am(`J+"F:.+1O,fdkZaSL4Z3Qh%**?.PZb#[#O/mg8[sN:$4$TW!AIR<41ere[:#`4C^nk-kncsh^rl@VR/_``BM-^#05/pmYN<.q>4+[a]8P:Kn&7+*Je\fiF&-oGob1$&ALp]\%)>s-^^.8ZK[$`b]'#$BRl9&Ko1%0^;_taUQ0k$8Wd(KMkMoa2UK(.nk"2Z8Wu@G\(>T;%KOL@;8Xikjq$kq&_VrkUUS8T\=WFq=@"o"D$*2O0hKIVA@+rDqAo[>V4XQYejI-a)%W3*>r9SlPl#f)A^s%u.0TrK\O(<aUb*,'_KsGh#QfU!U`X:eM2H4VniC\#\n7.QK>ERWXpjJ_N(87m*Tma\f`\,p21E#F^@UfKJD'P=&1b:MNZLbsnjKGq!@rK7".W*fdD6t3ThZr%^i'M*!WKs2IP0,V\X)Pa(p\YT+Q6c`\jc7ld@Z3Op/c!;9!pV?X+K"`%<Ruq!"f\Y%WQ[V9JX,^YrUb%a8iAE*&ZBXG=%JN;eFaF<?L#O$oP,ppB/0mA&O"55?'+DBK9db7s,\j873XY083aW68["->g,8n*Ihf2?IW"o.ccota2i[AZqi-A;YWqaMm:NS=iu(%j%8>A:FrODm^fT:'VLTX%YPh_p*;o_`1^+t&Un<6k9:^[[Q)oob.qq'#q('$E?Nh<o&(&M``rY6i%9_7T)MC*hU(mViDC"R8o`XPO!\UuI[*GQ!9:X+a,u,<R[Ad;NiDM-iguO*&83L`Bq$X1U>;-~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000524 00000 n
0000000592 00000 n
0000000872 00000 n
0000000931 00000 n
trailer
<<
/ID
[<7157e2a041ebda766e80d1441656b00d><7157e2a041ebda766e80d1441656b00d>]
% ReportLab generated PDF document -- digest (opensource)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1951
%%EOF

View File

@@ -1,74 +0,0 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260211215935+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260211215935+01'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 640
>>
stream
Gat=h9lldX&;KZQ'mjZ8l&;km&-?a/g[ro8)CmaRA<::6o+;OVr9q8=FqSErb^a:B5[rV@YBH66_*RHOc2_`.bjQrV#''gqF$,Q!iMW3Y9^$2ea$9ph"@3m_+#nhdd'kF54p)BX4;7h1aoiPs6d5!9d%Ch?m&1t6\kp"\>q(sQ3S`r+W!obp048L9$1.G7T@V?X'gEFEI09$Hj>WH3`(.>P3%b4lIfoU2jgcJccS1]T@-o;B%E-t.G;/ulF,dOf!G*H_K_[F@As>nW+s'3+G<AW*N;Oe7B@Nu$0("<:F:c_Z_P*NlKaTHeU<&=C21<&b+M)ZC_+0s"4Cmbe(Lc/Ojq#WWZ5::uCV<Ct]r=u.,Oi.T&q&?IlOr+8\7fmbqP?d.+i6g`*7?A"G%R.<L(3N4aA)N`*6Y'PSVNbM`3DQs*[fMVQpZ^".oZ,`qdQ@U/a6/7?n.FT=A?Q3DB^25n!2pC1\?`SKsIXUgL`-r@f(+BA\A&!q<2_DasND--cQ'jD'5N3l`<o9*4G`j6<Ur#@B3+_S[QshZg2HB?Jp!9#hmi)XUXpPm*B:V2uBFKVPtfE_A:QPQkn#]."#hXgMNuRJ\"S?pg[;i>2tC>`0^-l_`fq^0O#!"OST(^PQ)F7SGi~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000061 00000 n
0000000102 00000 n
0000000209 00000 n
0000000321 00000 n
0000000524 00000 n
0000000592 00000 n
0000000872 00000 n
0000000931 00000 n
trailer
<<
/ID
[<dd9e03bef959be273127c681a80b79d4><dd9e03bef959be273127c681a80b79d4>]
% ReportLab generated PDF document -- digest (opensource)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1661
%%EOF

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -24,7 +24,12 @@ from reportlab.platypus import PageBreak
# HELPERS # HELPERS
# ============================================================ # ============================================================
def _create_stop_histogram(stop_distances): def _create_stop_histogram(
stop_distances,
p50=None,
p90=None,
p99=None,
):
fig, ax = plt.subplots(figsize=(6, 4)) fig, ax = plt.subplots(figsize=(6, 4))
@@ -34,6 +39,16 @@ def _create_stop_histogram(stop_distances):
alpha=0.7, alpha=0.7,
) )
# Percentile lines
if p50 is not None:
ax.axvline(p50 * 100, linestyle=":", linewidth=1)
if p90 is not None:
ax.axvline(p90 * 100, linestyle=":", linewidth=1)
if p99 is not None:
ax.axvline(p99 * 100, linestyle=":", linewidth=1)
ax.set_title("Stop Distance Distribution") ax.set_title("Stop Distance Distribution")
ax.set_xlabel("Stop Distance (%)") ax.set_xlabel("Stop Distance (%)")
ax.set_ylabel("Frequency") ax.set_ylabel("Frequency")
@@ -46,18 +61,30 @@ def _create_stop_histogram(stop_distances):
return buf return buf
def _create_position_size_plot(timestamps, position_sizes): def _create_position_size_plot(
# Align and be robust timestamps,
position_sizes,
*,
max_position_fraction=None,
):
ts, ps = _align_xy(timestamps, position_sizes) ts, ps = _align_xy(timestamps, position_sizes)
if not ps: if not ps:
return None return None
x = list(range(len(ps))) # robust axis (avoid matplotlib categorical date issues) x = list(range(len(ps)))
y = [p * 100 for p in ps] y = [p * 100 for p in ps]
fig, ax = plt.subplots(figsize=(6, 4)) fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, y, linewidth=0.8) ax.plot(x, y, linewidth=0.8)
if max_position_fraction is not None:
ax.axhline(
max_position_fraction * 100,
linestyle="--",
linewidth=1,
)
ax.set_title("Position Size Over Time") ax.set_title("Position Size Over Time")
ax.set_ylabel("Position Size (% of equity)") ax.set_ylabel("Position Size (% of equity)")
ax.set_xlabel("Samples") ax.set_xlabel("Samples")
@@ -67,9 +94,19 @@ def _create_position_size_plot(timestamps, position_sizes):
plt.savefig(buf, format="png", dpi=150) plt.savefig(buf, format="png", dpi=150)
plt.close(fig) plt.close(fig)
buf.seek(0) buf.seek(0)
return buf return buf
def _create_effective_risk_plot(timestamps, effective_risks):
def _create_effective_risk_plot(
timestamps,
effective_risks,
*,
risk_target=None,
p50=None,
p90=None,
p99=None,
):
ts, er = _align_xy(timestamps, effective_risks) ts, er = _align_xy(timestamps, effective_risks)
if not er: if not er:
return None return None
@@ -78,8 +115,18 @@ def _create_effective_risk_plot(timestamps, effective_risks):
y = [r * 100 for r in er] y = [r * 100 for r in er]
fig, ax = plt.subplots(figsize=(6, 4)) fig, ax = plt.subplots(figsize=(6, 4))
# Main curve
ax.plot(x, y, linewidth=0.8) ax.plot(x, y, linewidth=0.8)
# Risk target line
if risk_target is not None:
ax.axhline(
risk_target * 100,
linestyle="--",
linewidth=1,
)
ax.set_title("Effective Risk Over Time") ax.set_title("Effective Risk Over Time")
ax.set_ylabel("Effective Risk (%)") ax.set_ylabel("Effective Risk (%)")
ax.set_xlabel("Samples") ax.set_xlabel("Samples")
@@ -89,6 +136,7 @@ def _create_effective_risk_plot(timestamps, effective_risks):
plt.savefig(buf, format="png", dpi=150) plt.savefig(buf, format="png", dpi=150)
plt.close(fig) plt.close(fig)
buf.seek(0) buf.seek(0)
return buf return buf
def _align_xy(x, y): def _align_xy(x, y):
@@ -386,7 +434,18 @@ def generate_risk_report_pdf(
story.append(Paragraph("6. Stop Distance Distribution", heading_style)) story.append(Paragraph("6. Stop Distance Distribution", heading_style))
story.append(Spacer(1, 8)) story.append(Spacer(1, 8))
img_buffer = _create_stop_histogram(stop_distances) stop_metrics = (
results.get("checks", {})
.get("stop_sanity", {})
.get("metrics", {})
)
img_buffer = _create_stop_histogram(
stop_distances,
p50=stop_metrics.get("p50"),
p90=stop_metrics.get("p90"),
p99=stop_metrics.get("p99"),
)
img = Image(img_buffer, width=400, height=250) img = Image(img_buffer, width=400, height=250)
story.append(img) story.append(img)
@@ -405,7 +464,12 @@ def generate_risk_report_pdf(
story.append(Paragraph("7. Position Size Over Time", heading_style)) story.append(Paragraph("7. Position Size Over Time", heading_style))
story.append(Spacer(1, 8)) story.append(Spacer(1, 8))
img_buffer = _create_position_size_plot(timestamps, position_sizes) img_buffer = _create_position_size_plot(
timestamps,
position_sizes,
max_position_fraction=config.get("Max position fraction (%)", 0) / 100,
)
if img_buffer: if img_buffer:
img = Image(img_buffer, width=400, height=250) img = Image(img_buffer, width=400, height=250)
story.append(img) story.append(img)
@@ -422,7 +486,14 @@ def generate_risk_report_pdf(
story.append(Paragraph("8. Effective Risk Over Time", heading_style)) story.append(Paragraph("8. Effective Risk Over Time", heading_style))
story.append(Spacer(1, 8)) story.append(Spacer(1, 8))
img_buffer = _create_effective_risk_plot(timestamps, effective_risks) risk_target = config.get("Risk per trade (%)", 0) / 100
img_buffer = _create_effective_risk_plot(
timestamps,
effective_risks,
risk_target=risk_target,
)
if img_buffer: if img_buffer:
img = Image(img_buffer, width=400, height=250) img = Image(img_buffer, width=400, height=250)
story.append(img) story.append(img)

View File

@@ -68,6 +68,18 @@ def create_app() -> FastAPI:
name="static", name="static",
) )
# -------------------------
# Reports folder (public access)
# -------------------------
reports_root = PROJECT_ROOT / "reports"
reports_root.mkdir(exist_ok=True)
app.mount(
"/reports",
StaticFiles(directory=str(reports_root)),
name="reports",
)
# ================================================== # ==================================================
# ROUTES — UI ONLY (TEMPORAL) # ROUTES — UI ONLY (TEMPORAL)
# ================================================== # ==================================================

View File

@@ -3,9 +3,10 @@
import logging import logging
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, JSONResponse
from pathlib import Path from pathlib import Path
import uuid import uuid
import re
from src.data.storage import StorageManager from src.data.storage import StorageManager
from src.calibration.risk_inspector import ( from src.calibration.risk_inspector import (
@@ -261,15 +262,29 @@ def generate_risk_report(
results = result results = result
# --------------------------------------------- # ============================================================
# Generate file # BUILD SAFE OUTPUT PATH (OUTSIDE src/)
# --------------------------------------------- # ============================================================
reports_dir = Path("reports") # Project root (3 niveles arriba desde router)
reports_dir.mkdir(exist_ok=True) project_root = Path(__file__).resolve().parents[5]
filename = f"risk_report_{uuid.uuid4().hex}.pdf" # Reports folder outside src
output_path = reports_dir / filename reports_dir = project_root / "reports" / "risk"
reports_dir.mkdir(parents=True, exist_ok=True)
# Sanitize symbol for filesystem
safe_symbol = re.sub(r"[^a-zA-Z0-9_-]", "_", payload.symbol)
# Unique filename
file_id = uuid.uuid4().hex
filename = f"risk_report_{safe_symbol}_{payload.timeframe}_{file_id}.pdf"
symbol_dir = reports_dir / safe_symbol
symbol_dir.mkdir(exist_ok=True)
output_path = symbol_dir / filename
generate_risk_report_pdf( generate_risk_report_pdf(
output_path=output_path, output_path=output_path,
@@ -278,8 +293,24 @@ def generate_risk_report(
results=results, results=results,
) )
return FileResponse( # ---------------------------------------------
path=str(output_path), # Create public URL
media_type="application/pdf", # ---------------------------------------------
filename=filename,
public_url = f"/reports/risk/{safe_symbol}/{filename}"
return JSONResponse(
content={
"status": "ok",
"url": public_url,
}
) )
# return FileResponse(
# path=str(output_path),
# media_type="application/pdf",
# filename=output_path.name,
# headers={
# "Content-Disposition": f'inline; filename="{output_path.name}"'
# },
# )

View File

@@ -212,7 +212,7 @@ async function validateCalibrationRisk() {
async function generateRiskReport() { async function generateRiskReport() {
console.log("[calibration_risk] generateRiskReport() START"); console.log("[calibration_risk] generateRiskReport() START");
const payload = buildRiskPayload(); // reutiliza la misma función que validate const payload = buildRiskPayload();
const res = await fetch("/api/v2/calibration/risk/report", { const res = await fetch("/api/v2/calibration/risk/report", {
method: "POST", method: "POST",
@@ -220,25 +220,23 @@ async function generateRiskReport() {
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
if (!res.ok) { const data = await res.json();
console.error("Failed to generate report");
return; if (data.url) {
// Mostrar visor
const viewer = document.getElementById("pdf_viewer_section");
const frame = document.getElementById("pdf_frame");
frame.src = data.url;
viewer.classList.remove("d-none");
// Scroll suave hacia el visor
viewer.scrollIntoView({ behavior: "smooth" });
} else {
alert("Failed to generate report");
} }
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "risk_report.pdf";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} }
// ================================================= // =================================================
// WIZARD NAVIGATION // WIZARD NAVIGATION
// ================================================= // =================================================
@@ -804,6 +802,13 @@ document.addEventListener("DOMContentLoaded", () => {
document document
.getElementById("generate_report_btn") .getElementById("generate_report_btn")
?.addEventListener("click", generateRiskReport); ?.addEventListener("click", generateRiskReport);
document
.getElementById("close_pdf_btn")
?.addEventListener("click", () => {
document.getElementById("pdf_viewer_section").classList.add("d-none");
document.getElementById("pdf_frame").src = "";
});
}); });

View File

@@ -238,6 +238,27 @@
<div id="stop_distance_plot" style="height: 360px;"></div> <div id="stop_distance_plot" style="height: 360px;"></div>
</div> </div>
<!-- ========================= -->
<!-- PDF REPORT VIEWER -->
<!-- ========================= -->
<div id="pdf_viewer_section" class="mt-5 d-none">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="card-title mb-0">Risk Report Preview</h4>
<button id="close_pdf_btn" class="btn btn-sm btn-outline-secondary">
Close
</button>
</div>
<iframe
id="pdf_frame"
style="width: 100%; height: 800px; border: none;"
></iframe>
</div>
</div>
</div>
</div> </div>
<!-- Plotly --> <!-- Plotly -->
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>