Step 2: Risk and Stops - Generating reports with pdf. It is missing to reorder the PDF.

This commit is contained in:
DaM
2026-02-13 07:01:44 +01:00
parent 4d769af8bf
commit 44667df3dd
27 changed files with 4318 additions and 103 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,93 @@
%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

@@ -0,0 +1,93 @@
%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

@@ -0,0 +1,74 @@
%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

@@ -0,0 +1,74 @@
%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

@@ -0,0 +1,74 @@
%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

@@ -0,0 +1,74 @@
%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

@@ -49,6 +49,7 @@ python-dotenv==1.0.0
pytz==2025.2
PyYAML==6.0.1
redis==5.0.1
reportlab==4.4.9
requests==2.32.5
seaborn==0.13.2
setuptools==80.10.1

View File

View File

@@ -0,0 +1,444 @@
# src/calibration/reports/risk_report.py
import matplotlib.pyplot as plt
from reportlab.platypus import Image
import io
from datetime import datetime
from pathlib import Path
from typing import Dict, Any
from reportlab.platypus import (
SimpleDocTemplate,
Paragraph,
Spacer,
Table,
TableStyle,
)
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.platypus import PageBreak
# ============================================================
# HELPERS
# ============================================================
def _create_stop_histogram(stop_distances):
fig, ax = plt.subplots(figsize=(6, 4))
ax.hist(
[d * 100 for d in stop_distances],
bins=40,
alpha=0.7,
)
ax.set_title("Stop Distance Distribution")
ax.set_xlabel("Stop Distance (%)")
ax.set_ylabel("Frequency")
buf = io.BytesIO()
plt.tight_layout()
plt.savefig(buf, format="png")
plt.close(fig)
buf.seek(0)
return buf
def _create_position_size_plot(timestamps, position_sizes):
# Align and be robust
ts, ps = _align_xy(timestamps, position_sizes)
if not ps:
return None
x = list(range(len(ps))) # robust axis (avoid matplotlib categorical date issues)
y = [p * 100 for p in ps]
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, y, linewidth=0.8)
ax.set_title("Position Size Over Time")
ax.set_ylabel("Position Size (% of equity)")
ax.set_xlabel("Samples")
buf = io.BytesIO()
plt.tight_layout()
plt.savefig(buf, format="png", dpi=150)
plt.close(fig)
buf.seek(0)
return buf
def _create_effective_risk_plot(timestamps, effective_risks):
ts, er = _align_xy(timestamps, effective_risks)
if not er:
return None
x = list(range(len(er)))
y = [r * 100 for r in er]
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, y, linewidth=0.8)
ax.set_title("Effective Risk Over Time")
ax.set_ylabel("Effective Risk (%)")
ax.set_xlabel("Samples")
buf = io.BytesIO()
plt.tight_layout()
plt.savefig(buf, format="png", dpi=150)
plt.close(fig)
buf.seek(0)
return buf
def _align_xy(x, y):
"""
Ensures x and y have the same length.
Trims to the shortest length and drops None/NaN pairs.
Returns (x_aligned, y_aligned).
"""
if not x or not y:
return [], []
n = min(len(x), len(y))
x = x[:n]
y = y[:n]
x2, y2 = [], []
for xi, yi in zip(x, y):
if yi is None:
continue
try:
# Filter NaN
if isinstance(yi, float) and yi != yi:
continue
except Exception:
pass
x2.append(xi)
y2.append(yi)
return x2, y2
# ============================================================
# Footer (page number)
# ============================================================
def _add_footer(canvas, doc):
canvas.saveState()
footer_text = f"Trading Bot · Calibration Report · Page {doc.page}"
canvas.setFont("Helvetica", 8)
canvas.drawRightString(200 * mm, 10 * mm, footer_text)
canvas.restoreState()
# ============================================================
# Main PDF generator
# ============================================================
def generate_risk_report_pdf(
*,
output_path: Path,
context: Dict[str, Any],
config: Dict[str, Any],
results: Dict[str, Any],
):
styles = getSampleStyleSheet()
title_style = styles["Title"]
heading_style = styles["Heading2"]
normal_style = styles["Normal"]
story = []
# ============================================================
# TITLE
# ============================================================
story.append(
Paragraph(
"Calibration Report · Step 2 · Risk & Stops",
title_style,
)
)
story.append(Spacer(1, 12))
story.append(
Paragraph(
f"Generated at: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC",
normal_style,
)
)
story.append(Spacer(1, 24))
# ============================================================
# CONTEXT
# ============================================================
story.append(Paragraph("1. Context", heading_style))
story.append(Spacer(1, 8))
context_table = Table(
[[k, str(v)] for k, v in context.items()],
colWidths=[180, 300],
)
context_table.setStyle(
TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("BACKGROUND", (0, 0), (-1, 0), colors.whitesmoke),
])
)
story.append(context_table)
story.append(Spacer(1, 24))
# ============================================================
# CONFIGURATION
# ============================================================
story.append(Paragraph("2. Configuration", heading_style))
story.append(Spacer(1, 8))
config_table = Table(
[[k, str(v)] for k, v in config.items()],
colWidths=[180, 300],
)
config_table.setStyle(
TableStyle([
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
])
)
story.append(config_table)
story.append(Spacer(1, 24))
# ============================================================
# VALIDATION RESULT
# ============================================================
story.append(Paragraph("3. Risk Validation Result", heading_style))
story.append(Spacer(1, 8))
status = results.get("status", "unknown").upper()
status_color = {
"OK": colors.green,
"WARNING": colors.orange,
"FAIL": colors.red,
}.get(status, colors.black)
status_table = Table(
[["Status", status]],
colWidths=[180, 300],
)
status_table.setStyle(
TableStyle([
("TEXTCOLOR", (1, 0), (1, 0), status_color),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
])
)
story.append(status_table)
story.append(Spacer(1, 12))
story.append(
Paragraph(
results.get("message", ""),
normal_style,
)
)
story.append(Spacer(1, 24))
# ============================================================
# CHECKS
# ============================================================
checks = results.get("checks", {})
if checks:
story.append(Paragraph("4. Detailed Checks", heading_style))
story.append(Spacer(1, 8))
check_rows = [["Check", "Status", "Message"]]
for name, data in checks.items():
check_rows.append([
name,
data.get("status", "").upper(),
data.get("message", ""),
])
check_table = Table(
check_rows,
colWidths=[150, 80, 250],
)
check_table.setStyle(
TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
])
)
story.append(check_table)
story.append(Spacer(1, 24))
# ============================================================
# STOP DISTANCE STATISTICS
# ============================================================
stop_metrics = (
results.get("checks", {})
.get("stop_sanity", {})
.get("metrics", {})
)
if stop_metrics:
story.append(Paragraph("5. Stop Distance Statistics", heading_style))
story.append(Spacer(1, 8))
stats_table = Table(
[
["Metric", "Value (%)"],
["P50 (typical)", f"{stop_metrics.get('p50', 0) * 100:.2f}%"],
["P90 (wide)", f"{stop_metrics.get('p90', 0) * 100:.2f}%"],
["P99 (extreme)", f"{stop_metrics.get('p99', 0) * 100:.2f}%"],
],
colWidths=[180, 180],
)
stats_table.setStyle(
TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
])
)
story.append(stats_table)
story.append(Spacer(1, 24))
# ============================================================
# RISK SIZING BREAKDOWN (WITH FORMULAS)
# ============================================================
sizing_metrics = (
results.get("checks", {})
.get("sizer_feasibility", {})
.get("metrics", {})
)
stop_metrics = (
results.get("checks", {})
.get("stop_sanity", {})
.get("metrics", {})
)
if sizing_metrics and stop_metrics:
story.append(Paragraph("6. Risk Sizing Breakdown", heading_style))
story.append(Spacer(1, 8))
equity = context.get("Account equity", 0)
risk_pct = config.get("Risk per trade (%)", 0)
max_pos_pct = config.get("Max position fraction (%)", 0)
p50 = stop_metrics.get("p50", 0)
ideal_position = sizing_metrics.get("ideal_position_median", 0)
max_position_value = sizing_metrics.get("max_position_value", 0)
effective_position = sizing_metrics.get("effective_position_median", 0)
effective_risk = sizing_metrics.get("effective_risk_median", 0)
breakdown_lines = [
f"Position size (ideal) = {equity:,.0f} × {risk_pct:.2f}% ÷ {p50*100:.2f}% ≈ {ideal_position:,.0f}",
f"Max position size = {equity:,.0f} × {max_pos_pct:.2f}% = {max_position_value:,.0f}",
f"Effective position = min({ideal_position:,.0f}, {max_position_value:,.0f}) = {effective_position:,.0f}",
f"Effective risk = {effective_position:,.0f} × {p50*100:.2f}% ÷ {equity:,.0f}{effective_risk*100:.2f}%",
]
for line in breakdown_lines:
story.append(Paragraph(line, normal_style))
story.append(Spacer(1, 4))
story.append(Spacer(1, 24))
# ============================================================
# STOP CONFIGURATION DETAILS
# ============================================================
stop_snapshot = results.get("config_snapshot", {}).get("stop", {})
story.append(Paragraph("7. Stop Configuration Details", heading_style))
story.append(Spacer(1, 8))
for k, v in stop_snapshot.items():
story.append(Paragraph(f"{k}: {v}", normal_style))
story.append(Spacer(1, 4))
# ============================================================
# STOP DISTANCE HISTOGRAM
# ============================================================
series = results.get("series", {})
stop_distances = series.get("stop_distances")
if stop_distances:
story.append(Paragraph("6. Stop Distance Distribution", heading_style))
story.append(Spacer(1, 8))
img_buffer = _create_stop_histogram(stop_distances)
img = Image(img_buffer, width=400, height=250)
story.append(img)
story.append(Spacer(1, 24))
# ============================================================
# POSITION SIZE OVER TIME
# ============================================================
position_sizes = series.get("position_size_pct")
timestamps = series.get("timestamps")
if position_sizes and timestamps:
story.append(PageBreak())
story.append(Paragraph("7. Position Size Over Time", heading_style))
story.append(Spacer(1, 8))
img_buffer = _create_position_size_plot(timestamps, position_sizes)
if img_buffer:
img = Image(img_buffer, width=400, height=250)
story.append(img)
story.append(Spacer(1, 24))
# ============================================================
# EFFECTIVE RISK OVER TIME
# ============================================================
effective_risks = series.get("effective_risk_pct")
if effective_risks and timestamps:
story.append(Paragraph("8. Effective Risk Over Time", heading_style))
story.append(Spacer(1, 8))
img_buffer = _create_effective_risk_plot(timestamps, effective_risks)
if img_buffer:
img = Image(img_buffer, width=400, height=250)
story.append(img)
story.append(Spacer(1, 24))
# ============================================================
# BUILD
# ============================================================
doc = SimpleDocTemplate(
str(output_path),
pagesize=A4,
rightMargin=36,
leftMargin=36,
topMargin=36,
bottomMargin=36,
)
doc.build(story, onFirstPage=_add_footer, onLaterPages=_add_footer)

View File

@@ -0,0 +1,449 @@
# src/calibration/risk_inspector.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal, Dict, Any
import numpy as np
import pandas as pd
from src.data.storage import StorageManager
from src.utils.logger import log
# ============================================================
# Domain models (pueden moverse luego a models.py)
# ============================================================
StopType = Literal["fixed", "trailing", "atr"]
@dataclass
class StopConfig:
type: StopType
# fixed / trailing
stop_fraction: float | None = None
# atr
atr_period: int | None = None
atr_multiplier: float | None = None
@dataclass
class RiskConfig:
risk_fraction: float
max_position_fraction: float
@dataclass
class GlobalRiskRules:
max_drawdown_pct: float
# reservados v1+
daily_loss_limit_pct: float | None = None
max_consecutive_losses: int | None = None
cooldown_bars: int | None = None
# ============================================================
# Public entry point
# ============================================================
def inspect_risk_config(
*,
storage: StorageManager,
symbol: str,
timeframe: str,
stop: StopConfig,
risk: RiskConfig,
rules: GlobalRiskRules,
account_equity: float,
data_quality: Dict[str, Any],
) -> Dict[str, Any]:
"""
Inspecta si una configuración de riesgo + stop es viable
con los datos disponibles para un símbolo y timeframe.
No ejecuta estrategias.
No persiste estado.
"""
log.info("🔍 Inspecting risk & stop configuration")
# --------------------------------------------------
# Load market data (read-only)
# --------------------------------------------------
df = storage.load_ohlcv(symbol=symbol, timeframe=timeframe)
if df is None or df.empty:
return _fail_result("No OHLCV data available for risk inspection")
# --------------------------------------------------
# Run checks
# --------------------------------------------------
checks: Dict[str, Any] = {}
checks["data_compatibility"] = _check_data_compatibility(data_quality)
checks["stop_availability"] = _check_stop_availability(df, stop)
checks["stop_sanity"] = _check_stop_sanity(df, stop)
checks["sizer_feasibility"] = _check_sizer_feasibility(
df=df,
stop=stop,
risk=risk,
account_equity=account_equity,
)
# --------------------------------------------------
# Aggregate quality
# --------------------------------------------------
status = _aggregate_status(checks)
message = _build_human_message(status, checks)
series = _compute_position_series(
df=df,
stop=stop,
risk=risk,
account_equity=account_equity,
)
return {
"valid": status != "fail",
"status": status,
"checks": checks,
"message": message,
"series": series,
"config_snapshot": {
"symbol": symbol,
"timeframe": timeframe,
"account_equity": account_equity,
"stop": stop.__dict__,
"risk": risk.__dict__,
"rules": rules.__dict__,
},
}
# ============================================================
# Checks
# ============================================================
def _check_data_compatibility(data_quality: Dict[str, Any]) -> Dict[str, Any]:
"""
Evalúa si la calidad de datos permite usar stops con fiabilidad.
"""
status = data_quality.get("status")
if status == "fail":
return {
"status": "fail",
"message": "Data quality is insufficient for risk calibration",
}
gaps = data_quality.get("gaps", {}).get("count", 0)
if gaps > 0:
return {
"status": "warning",
"message": (
"Data contains gaps. Stops are evaluated on close price "
"and may trigger late."
),
}
return {
"status": "ok",
"message": "Data quality is compatible with stop evaluation",
}
def _check_stop_availability(df: pd.DataFrame, stop: StopConfig) -> Dict[str, Any]:
"""
Comprueba que el stop puede calcularse con los datos disponibles.
"""
if stop.type in ("fixed", "trailing"):
return {"status": "ok"}
if stop.type == "atr":
if stop.atr_period is None or stop.atr_multiplier is None:
return {
"status": "fail",
"reason": "atr_parameters_missing",
}
if len(df) < stop.atr_period + 5:
return {
"status": "fail",
"reason": "not_enough_candles_for_atr",
}
required_cols = {"high", "low", "close"}
if not required_cols.issubset(df.columns):
return {
"status": "fail",
"reason": "missing_ohlc_columns",
}
return {"status": "ok"}
return {"status": "fail", "reason": "unknown_stop_type"}
def _check_stop_sanity(df: pd.DataFrame, stop: StopConfig) -> Dict[str, Any]:
"""
Evalúa si el stop tiene una magnitud razonable.
"""
stop_distances = _compute_stop_distances(df, stop)
if stop_distances is None or len(stop_distances) == 0:
return {
"status": "fail",
"message": "Unable to compute stop distances",
}
quantiles = np.quantile(stop_distances, [0.5, 0.9, 0.99])
p50, p90, p99 = quantiles
status = "ok"
message = "Stop distance looks reasonable"
if p50 < 0.002:
status = "warning"
message = "Stop distance is very tight and may cause overtrading"
if p90 > 0.2:
status = "warning"
message = "Stop distance is very wide and may reduce trade frequency"
recommendation = None
if status == "warning":
# Recomendación basada en estructura del mercado
recommended_stop = float(np.clip(p75 := np.quantile(stop_distances, 0.75), 0.002, 0.2))
recommendation = {
"suggested_stop_fraction": recommended_stop,
"explanation": (
f"Based on market volatility, a stop around "
f"{recommended_stop:.2%} would better balance trade frequency and risk."
),
}
return {
"status": status,
"metrics": {
"p50": float(p50),
"p90": float(p90),
"p99": float(p99),
},
"message": message,
"recommendation": recommendation,
}
def _check_sizer_feasibility(
*,
df: pd.DataFrame,
stop: StopConfig,
risk: RiskConfig,
account_equity: float,
) -> Dict[str, Any]:
stop_distances = _compute_stop_distances(df, stop)
prices = df["close"].values
risk_amount = account_equity * risk.risk_fraction
max_position_value = account_equity * risk.max_position_fraction
effective_risks = []
effective_positions = []
ideal_positions = []
for price, stop_fraction in zip(prices, stop_distances):
if stop_fraction <= 0 or price <= 0:
continue
ideal_position_value = risk_amount / stop_fraction
ideal_positions.append(ideal_position_value)
position_value = min(ideal_position_value, max_position_value)
effective_positions.append(position_value)
effective_risk = position_value * stop_fraction / account_equity
effective_risks.append(effective_risk)
if not effective_risks:
return {
"status": "fail",
"message": "Unable to compute sizing feasibility",
}
eff_risk_median = float(np.median(effective_risks))
ideal_position_median = float(np.median(ideal_positions))
effective_position_median = float(np.median(effective_positions))
status = "ok"
message = "Risk sizing looks feasible"
if eff_risk_median < risk.risk_fraction * 0.1:
status = "warning"
message = (
"Effective risk is much lower than intended. "
"Position sizing is heavily capped by limits."
)
if eff_risk_median > risk.risk_fraction * 1.5:
status = "fail"
message = (
"Effective risk exceeds intended risk. "
"Risk configuration is unsafe."
)
return {
"status": status,
"metrics": {
"ideal_position_median": ideal_position_median,
"max_position_value": max_position_value,
"effective_position_median": effective_position_median,
"effective_risk_median": eff_risk_median,
},
"message": message,
}
def _compute_position_series(
*,
df: pd.DataFrame,
stop: StopConfig,
risk: RiskConfig,
account_equity: float,
) -> Dict[str, Any]:
"""
Calcula series temporales de sizing, riesgo efectivo
y distancias de stop (en fracción).
"""
stop_distances = _compute_stop_distances(df, stop)
prices = df["close"].values
timestamps = df.index.astype(str).tolist()
risk_amount = account_equity * risk.risk_fraction
max_position_value = account_equity * risk.max_position_fraction
position_size_pct = []
effective_risk_pct = []
stop_distances_out = []
for price, stop_fraction in zip(prices, stop_distances):
if stop_fraction is None or stop_fraction <= 0 or price <= 0:
position_size_pct.append(None)
effective_risk_pct.append(None)
stop_distances_out.append(None)
continue
position_value = risk_amount / stop_fraction
if position_value > max_position_value:
position_value = max_position_value
pos_pct = position_value / account_equity
eff_risk = position_value * stop_fraction / account_equity
position_size_pct.append(float(pos_pct))
effective_risk_pct.append(float(eff_risk))
stop_distances_out.append(float(stop_fraction))
return {
"timestamps": timestamps,
"position_size_pct": position_size_pct,
"effective_risk_pct": effective_risk_pct,
"stop_distances": stop_distances_out,
}
# ============================================================
# Utilities
# ============================================================
def _compute_stop_distances(df: pd.DataFrame, stop: StopConfig) -> np.ndarray:
"""
Devuelve un array con la distancia RELATIVA del stop respecto al precio.
Es decir: |stop_price - price| / price
"""
close = df["close"].values.astype(float)
if stop.type in ("fixed", "trailing"):
if stop.stop_fraction is None:
return None
# stop_fraction ya es relativa (ej. 0.01 = 1%)
return np.full_like(close, stop.stop_fraction, dtype=float)
if stop.type == "atr":
high = df["high"].astype(float)
low = df["low"].astype(float)
close_series = df["close"].astype(float)
tr = pd.concat(
[
high - low,
(high - close_series.shift()).abs(),
(low - close_series.shift()).abs(),
],
axis=1,
).max(axis=1)
atr = tr.rolling(stop.atr_period).mean()
# ATR stop distance RELATIVE to price
stop_distance = (atr * stop.atr_multiplier) / close_series
return stop_distance.dropna().values.astype(float)
return None
def _aggregate_status(checks: Dict[str, Any]) -> Literal["ok", "warning", "fail"]:
"""
Agrega los estados de los checks.
"""
statuses = [c["status"] for c in checks.values()]
if "fail" in statuses:
return "fail"
if "warning" in statuses:
return "warning"
return "ok"
def _build_human_message(status: str, checks: Dict[str, Any]) -> str:
"""
Construye un mensaje humano de alto nivel.
"""
if status == "ok":
return "Risk and stop configuration is compatible with the data"
if status == "warning":
return "Risk configuration has warnings. Review details before continuing"
return "Risk configuration is not usable with the current data"
def _fail_result(message: str) -> Dict[str, Any]:
"""
Construye una respuesta de fallo inmediato y consistente.
"""
return {
"valid": False,
"status": "fail",
"checks": {},
"message": message,
}

View File

@@ -10,6 +10,7 @@ import time
from .settings import settings
from src.web.api.v2.routers.calibration_data import router as calibration_data_router
from src.web.api.v2.routers.calibration_risk import router as calibration_risk_router
# --------------------------------------------------
# Logging
@@ -91,12 +92,25 @@ def create_app() -> FastAPI:
"step": 1,
},
)
@app.get("/calibration/risk", response_class=HTMLResponse)
def calibration_risk_page(request: Request):
return templates.TemplateResponse(
"pages/calibration/calibration_risk.html",
{
"request": request,
"page": "calibration",
"step": 2,
},
)
# --------------------------------------------------
# API routers (versionados)
# --------------------------------------------------
api_prefix = settings.api_prefix
app.include_router(calibration_data_router, prefix=api_prefix)
app.include_router(calibration_risk_router, prefix=api_prefix)
return app

View File

@@ -0,0 +1,285 @@
# src/web/api/v2/routers/calibration_risk.py
import logging
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from pathlib import Path
import uuid
from src.data.storage import StorageManager
from src.calibration.risk_inspector import (
inspect_risk_config,
StopConfig,
RiskConfig,
GlobalRiskRules,
)
from ..schemas.calibration_risk import (
CalibrationRiskInspectRequest,
CalibrationRiskInspectResponse,
CalibrationRiskValidateResponse,
)
from src.calibration.reports.risk_report import generate_risk_report_pdf
logger = logging.getLogger("tradingbot.api.v2")
router = APIRouter(
prefix="/calibration/risk",
tags=["calibration"],
)
# =================================================
# Dependencies
# =================================================
def get_storage() -> StorageManager:
return StorageManager.from_env()
# =================================================
# INSPECT (RISK & STOPS)
# =================================================
@router.post(
"/inspect",
response_model=CalibrationRiskInspectResponse,
)
def inspect_calibration_risk(
payload: CalibrationRiskInspectRequest,
storage: StorageManager = Depends(get_storage),
):
logger.info(
f"🛡️ Inspecting risk | {payload.symbol} {payload.timeframe}"
)
# --------------------------------------------------
# Step 1 dependency check
# --------------------------------------------------
# Por ahora: recalculamos data_quality igual que Step 1
# (más adelante se puede cachear si quieres)
df = storage.load_ohlcv(
symbol=payload.symbol,
timeframe=payload.timeframe,
)
if df is None or df.empty:
raise HTTPException(
status_code=400,
detail="No OHLCV data found. Run Step 1 first.",
)
# reutilizamos la misma función que en calibration_data
from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe)
# --------------------------------------------------
# Map schemas -> domain models
# --------------------------------------------------
stop = StopConfig(
type=payload.stop.type,
stop_fraction=payload.stop.stop_fraction,
atr_period=payload.stop.atr_period,
atr_multiplier=payload.stop.atr_multiplier,
)
risk = RiskConfig(
risk_fraction=payload.risk.risk_fraction,
max_position_fraction=payload.risk.max_position_fraction,
)
rules = GlobalRiskRules(
max_drawdown_pct=payload.global_rules.max_drawdown_pct,
daily_loss_limit_pct=payload.global_rules.daily_loss_limit_pct,
max_consecutive_losses=payload.global_rules.max_consecutive_losses,
cooldown_bars=payload.global_rules.cooldown_bars,
)
# --------------------------------------------------
# Domain inspection
# --------------------------------------------------
result = inspect_risk_config(
storage=storage,
symbol=payload.symbol,
timeframe=payload.timeframe,
stop=stop,
risk=risk,
rules=rules,
account_equity=payload.account_equity,
data_quality=data_quality,
)
logger.info(
f"🧪 Risk inspect result | status={result['status']}"
)
return CalibrationRiskInspectResponse(**result)
# =================================================
# VALIDATE (RISK & STOPS — with series for plots)
# =================================================
@router.post(
"/validate",
response_model=CalibrationRiskValidateResponse,
)
def validate_calibration_risk(
payload: CalibrationRiskInspectRequest,
storage: StorageManager = Depends(get_storage),
):
logger.info(
f"📊 Validating risk (with series) | {payload.symbol} {payload.timeframe}"
)
df = storage.load_ohlcv(
symbol=payload.symbol,
timeframe=payload.timeframe,
)
if df is None or df.empty:
raise HTTPException(
status_code=400,
detail="No OHLCV data found. Run Step 1 first.",
)
from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe)
stop = StopConfig(
type=payload.stop.type,
stop_fraction=payload.stop.stop_fraction,
atr_period=payload.stop.atr_period,
atr_multiplier=payload.stop.atr_multiplier,
)
risk = RiskConfig(
risk_fraction=payload.risk.risk_fraction,
max_position_fraction=payload.risk.max_position_fraction,
)
rules = GlobalRiskRules(
max_drawdown_pct=payload.global_rules.max_drawdown_pct,
daily_loss_limit_pct=payload.global_rules.daily_loss_limit_pct,
max_consecutive_losses=payload.global_rules.max_consecutive_losses,
cooldown_bars=payload.global_rules.cooldown_bars,
)
result = inspect_risk_config(
storage=storage,
symbol=payload.symbol,
timeframe=payload.timeframe,
stop=stop,
risk=risk,
rules=rules,
account_equity=payload.account_equity,
data_quality=data_quality,
)
logger.info(
f"📈 Risk validate result | status={result['status']}"
)
return CalibrationRiskValidateResponse(**result)
# =================================================
# REPORT (PDF)
# =================================================
@router.post("/report")
def generate_risk_report(
payload: CalibrationRiskInspectRequest,
storage: StorageManager = Depends(get_storage),
):
logger.info(
f"🧾 Generating risk report | {payload.symbol} {payload.timeframe}"
)
df = storage.load_ohlcv(
symbol=payload.symbol,
timeframe=payload.timeframe,
)
if df is None or df.empty:
raise HTTPException(
status_code=400,
detail="No OHLCV data found. Run Step 1 first.",
)
from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe)
stop = StopConfig(
type=payload.stop.type,
stop_fraction=payload.stop.stop_fraction,
atr_period=payload.stop.atr_period,
atr_multiplier=payload.stop.atr_multiplier,
)
risk = RiskConfig(
risk_fraction=payload.risk.risk_fraction,
max_position_fraction=payload.risk.max_position_fraction,
)
rules = GlobalRiskRules(
max_drawdown_pct=payload.global_rules.max_drawdown_pct,
daily_loss_limit_pct=payload.global_rules.daily_loss_limit_pct,
max_consecutive_losses=payload.global_rules.max_consecutive_losses,
cooldown_bars=payload.global_rules.cooldown_bars,
)
result = inspect_risk_config(
storage=storage,
symbol=payload.symbol,
timeframe=payload.timeframe,
stop=stop,
risk=risk,
rules=rules,
account_equity=payload.account_equity,
data_quality=data_quality,
)
# ---------------------------------------------
# Prepare PDF data
# ---------------------------------------------
context = {
"Symbol": payload.symbol,
"Timeframe": payload.timeframe,
"Account equity": payload.account_equity,
}
config = {
"Stop type": payload.stop.type,
"Risk per trade (%)": payload.risk.risk_fraction * 100,
"Max position fraction (%)": payload.risk.max_position_fraction * 100,
"Max drawdown (%)": payload.global_rules.max_drawdown_pct * 100,
}
results = result
# ---------------------------------------------
# Generate file
# ---------------------------------------------
reports_dir = Path("reports")
reports_dir.mkdir(exist_ok=True)
filename = f"risk_report_{uuid.uuid4().hex}.pdf"
output_path = reports_dir / filename
generate_risk_report_pdf(
output_path=output_path,
context=context,
config=config,
results=results,
)
return FileResponse(
path=str(output_path),
media_type="application/pdf",
filename=filename,
)

View File

@@ -0,0 +1,84 @@
# src/web/api/v2/schemas/calibration_risk.py
from typing import Literal, Optional, Dict, Any
from pydantic import BaseModel, Field, model_validator
class StopConfigSchema(BaseModel):
type: Literal["fixed", "trailing", "atr"]
stop_fraction: Optional[float] = Field(None, gt=0)
atr_period: Optional[int] = Field(None, gt=1)
atr_multiplier: Optional[float] = Field(None, gt=0)
@model_validator(mode="after")
def validate_by_type(self):
if self.type in ("fixed", "trailing"):
if self.stop_fraction is None:
raise ValueError(
"stop_fraction required for fixed/trailing stop"
)
if self.type == "atr":
if self.atr_period is None:
raise ValueError(
"atr_period required for ATR stop"
)
if self.atr_multiplier is None:
raise ValueError(
"atr_multiplier required for ATR stop"
)
return self
# class StopConfigSchema(BaseModel):
# type: Literal["fixed", "trailing", "atr"]
# stop_fraction: Optional[float] = Field(None, gt=0)
# atr_period: Optional[int] = Field(None, gt=1)
# atr_multiplier: Optional[float] = Field(None, gt=0)
class RiskConfigSchema(BaseModel):
risk_fraction: float = Field(..., gt=0, lt=1)
max_position_fraction: float = Field(..., gt=0, lt=1)
class GlobalRiskRulesSchema(BaseModel):
max_drawdown_pct: float = Field(..., gt=0, lt=1)
daily_loss_limit_pct: Optional[float] = Field(None, gt=0, lt=1)
max_consecutive_losses: Optional[int] = Field(None, gt=0)
cooldown_bars: Optional[int] = Field(None, ge=0)
class CalibrationRiskInspectRequest(BaseModel):
symbol: str
timeframe: str
stop: StopConfigSchema
risk: RiskConfigSchema
global_rules: GlobalRiskRulesSchema
account_equity: float = Field(..., gt=0)
class CalibrationRiskInspectResponse(BaseModel):
valid: bool
status: Literal["ok", "warning", "fail"]
checks: Dict[str, Any]
message: str
class CalibrationRiskValidateResponse(BaseModel):
valid: bool
status: Literal["ok", "warning", "fail"]
checks: Dict[str, Any]
message: str
# ⬇️ NUEVO
series: Dict[str, Any]

View File

@@ -0,0 +1,9 @@
/* =================================================
Wizard navigation (Calibration steps)
================================================= */
a[aria-disabled="true"] {
pointer-events: none;
opacity: 0.4;
cursor: not-allowed;
}

View File

@@ -32,6 +32,9 @@ async function inspectCalibrationData() {
resultEl.textContent = "⏳ Inspeccionando datos en DB...";
}
// por si vienes de un estado previo OK y ahora inspeccionas otra cosa
disableNextStep();
try {
const res = await fetch("/api/v2/calibration/data/inspect", {
method: "POST",
@@ -48,11 +51,22 @@ async function inspectCalibrationData() {
renderDataSummary(data);
// ✅ habilita el paso 2 si:
// - valid = true
// - data_quality no es FAIL (warnings dejan pasar)
const dqStatus = data.data_quality?.status ?? "ok";
if (data.valid && dqStatus !== "fail") {
enableNextStep();
localStorage.setItem("calibration.symbol", data.symbol);
localStorage.setItem("calibration.timeframe", data.timeframe);
}
} catch (err) {
console.error("[calibration_data] inspect FAILED", err);
if (resultEl) {
resultEl.textContent = "❌ Error inspeccionando datos";
}
disableNextStep();
}
}
@@ -174,8 +188,28 @@ function renderDownloadProgress(job) {
text.textContent = job.message || job.status;
}
function enableNextStep() {
const btn = document.getElementById("next-step-btn");
if (!btn) return;
btn.style.display = "inline-flex";
btn.classList.remove("btn-outline-secondary");
btn.classList.add("btn-outline-primary");
btn.setAttribute("aria-disabled", "false");
}
function disableNextStep() {
const btn = document.getElementById("next-step-btn");
if (!btn) return;
btn.style.display = "inline-flex";
btn.classList.remove("btn-outline-primary");
btn.classList.add("btn-outline-secondary");
btn.setAttribute("aria-disabled", "true");
}
// =================================================
// DATA SUMMARY (YA EXISTENTE)
// DATA SUMMARY
// =================================================
function renderDataSummary(data) {
@@ -183,7 +217,7 @@ function renderDataSummary(data) {
if (!card) return;
card.classList.remove("d-none");
document.getElementById("first-ts").textContent =
data.first_available ?? "";
document.getElementById("last-ts").textContent =
@@ -209,6 +243,9 @@ function renderDataSummary(data) {
logEl.textContent =
`❌ No hay datos para ${data.symbol} @ ${data.timeframe}`;
}
// si no es válido, ocultamos data quality
document.getElementById("data-quality-card")?.classList.add("d-none");
return;
}
@@ -241,8 +278,9 @@ function renderDataSummary(data) {
`${(dq.checks.coverage.ratio * 100).toFixed(2)}%`;
document.getElementById("dq-volume").textContent =
dq.checks.volume;
} else {
dqCard?.classList.add("d-none");
}
}
// =================================================

View File

@@ -0,0 +1,809 @@
// src/web/ui/v2/static/js/pages/calibration_risk.js
console.log(
"[calibration_risk] script loaded ✅",
new Date().toISOString()
);
// =================================================
// INSPECT RISK & STOPS
// =================================================
async function inspectCalibrationRisk() {
console.log("[calibration_risk] inspectCalibrationRisk() START ✅");
// --------------------------------------------------
// Load calibration context from Step 1
// --------------------------------------------------
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (!symbol || !timeframe) {
alert("Calibration context not found. Please complete Step 1 (Data) first.");
return;
}
// --------------------------------------------------
// Read form inputs
// --------------------------------------------------
const accountEquityEl = document.getElementById("account_equity");
const riskFractionEl = document.getElementById("risk_fraction");
const maxPositionEl = document.getElementById("max_position_fraction");
const stopTypeEl = document.getElementById("stop_type");
const stopFractionEl = document.getElementById("stop_fraction");
const atrPeriodEl = document.getElementById("atr_period");
const atrMultiplierEl = document.getElementById("atr_multiplier");
const maxDrawdownEl = document.getElementById("max_drawdown_pct");
if (!accountEquityEl || !riskFractionEl || !maxPositionEl || !stopTypeEl || !maxDrawdownEl) {
console.error("[calibration_risk] Missing required form elements");
alert("Internal error: risk form is incomplete.");
return;
}
// --------------------------------------------------
// Build stop config
// --------------------------------------------------
const stopType = stopTypeEl.value;
let stopConfig = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
if (!stopFractionEl) {
alert("Stop fraction input missing");
return;
}
stopConfig.stop_fraction = parseFloat(stopFractionEl.value) / 100;
}
if (stopType === "atr") {
if (!atrPeriodEl || !atrMultiplierEl) {
alert("ATR parameters missing");
return;
}
stopConfig.atr_period = parseInt(atrPeriodEl.value);
stopConfig.atr_multiplier = parseFloat(atrMultiplierEl.value);
}
// --------------------------------------------------
// Build payload
// --------------------------------------------------
const payload = buildRiskPayload();
console.log("[calibration_risk] inspect payload:", payload);
// --------------------------------------------------
// Disable next step while inspecting
// --------------------------------------------------
disableNextStep();
// --------------------------------------------------
// Call API
// --------------------------------------------------
try {
const res = await fetch("/api/v2/calibration/risk/inspect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
console.log("[calibration_risk] inspect response:", data);
renderRiskResult(payload, data);
// --------------------------------------------------
// Enable next step if OK or WARNING
// --------------------------------------------------
if (data.valid && data.status !== "fail") {
enableNextStep();
}
} catch (err) {
console.error("[calibration_risk] inspect FAILED", err);
alert("Error inspecting risk configuration");
disableNextStep();
}
}
// =================================================
// VALIDATE RISK & STOPS
// =================================================
async function validateCalibrationRisk() {
console.log("[calibration_risk] inspectCalibrationRisk() START ✅");
// --------------------------------------------------
// Load calibration context from Step 1
// --------------------------------------------------
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (!symbol || !timeframe) {
alert("Calibration context not found. Please complete Step 1 (Data) first.");
return;
}
// --------------------------------------------------
// Read form inputs
// --------------------------------------------------
const accountEquityEl = document.getElementById("account_equity");
const riskFractionEl = document.getElementById("risk_fraction");
const maxPositionEl = document.getElementById("max_position_fraction");
const stopTypeEl = document.getElementById("stop_type");
const stopFractionEl = document.getElementById("stop_fraction");
const atrPeriodEl = document.getElementById("atr_period");
const atrMultiplierEl = document.getElementById("atr_multiplier");
const maxDrawdownEl = document.getElementById("max_drawdown_pct");
if (!accountEquityEl || !riskFractionEl || !maxPositionEl || !stopTypeEl || !maxDrawdownEl) {
console.error("[calibration_risk] Missing required form elements");
alert("Internal error: risk form is incomplete.");
return;
}
// --------------------------------------------------
// Build stop config
// --------------------------------------------------
const stopType = stopTypeEl.value;
let stopConfig = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
if (!stopFractionEl) {
alert("Stop fraction input missing");
return;
}
stopConfig.stop_fraction = parseFloat(stopFractionEl.value) / 100;
}
if (stopType === "atr") {
if (!atrPeriodEl || !atrMultiplierEl) {
alert("ATR parameters missing");
return;
}
stopConfig.atr_period = parseInt(atrPeriodEl.value);
stopConfig.atr_multiplier = parseFloat(atrMultiplierEl.value);
}
// --------------------------------------------------
// Build payload
// --------------------------------------------------
const payload = buildRiskPayload();
console.log("[calibration_risk] inspect payload:", payload);
// --------------------------------------------------
// Disable next step while inspecting
// --------------------------------------------------
disableNextStep();
// --------------------------------------------------
// Call API
// --------------------------------------------------
try {
const res = await fetch("/api/v2/calibration/risk/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
console.log("[calibration_risk] inspect response:", data);
renderRiskResult(payload, data);
// --------------------------------------------------
// Enable next step if OK or WARNING
// --------------------------------------------------
if (data.valid && data.status !== "fail") {
enableNextStep();
}
} catch (err) {
console.error("[calibration_risk] inspect FAILED", err);
alert("Error inspecting risk configuration");
disableNextStep();
}
}
// =================================================
// PDF REPORT RISK & STOPS
// =================================================
async function generateRiskReport() {
console.log("[calibration_risk] generateRiskReport() START");
const payload = buildRiskPayload(); // reutiliza la misma función que validate
const res = await fetch("/api/v2/calibration/risk/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
console.error("Failed to generate report");
return;
}
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
// =================================================
function enableNextStep() {
const btn = document.getElementById("next-step-btn");
if (!btn) return;
btn.classList.remove("btn-outline-secondary");
btn.classList.add("btn-outline-primary");
btn.setAttribute("aria-disabled", "false");
}
function disableNextStep() {
const btn = document.getElementById("next-step-btn");
if (!btn) return;
btn.classList.remove("btn-outline-primary");
btn.classList.add("btn-outline-secondary");
btn.setAttribute("aria-disabled", "true");
}
// =================================================
// RENDER RESULT
// =================================================
function renderRiskResult(payload, data) {
const container = document.getElementById("risk_result");
const badge = document.getElementById("risk_status_badge");
const message = document.getElementById("risk_message");
const debug = document.getElementById("risk_debug");
if (!container || !badge || !message || !debug) {
console.warn("[calibration_risk] Result elements missing");
return;
}
container.classList.remove("d-none");
badge.className = "badge";
if (data.status === "ok") {
badge.classList.add("bg-success");
} else if (data.status === "warning") {
badge.classList.add("bg-warning");
} else {
badge.classList.add("bg-danger");
}
badge.textContent = data.status.toUpperCase();
message.textContent = data.message;
debug.textContent = JSON.stringify(data.checks, null, 2);
renderRiskChecks(data.checks);
if (data.checks?.stop_sanity?.metrics) {
renderStopQuantiles(data.checks.stop_sanity.metrics);
}
renderRiskFormulas(payload, data)
if (!data.series) {
console.warn("[calibration_risk] No series returned, skipping plots");
return;
}
if (data.series?.timestamps && data.series?.position_size_pct) {
renderPositionSizePlot({
timestamps: data.series.timestamps,
positionSizePct: data.series.position_size_pct,
maxPositionFraction: payload.risk.max_position_fraction
});
renderEffectiveRiskPlot({
timestamps: data.series.timestamps,
effectiveRiskPct: data.series.effective_risk_pct,
targetRiskFraction: payload.risk.risk_fraction
});
renderStopDistanceDistribution({
stopDistances: data.series.stop_distances,
quantiles: data.checks?.stop_sanity?.metrics
});
}
}
// =================================================
// RENDER RISK CHECKS
// =================================================
function renderRiskChecks(checks) {
const list = document.getElementById("risk_checks_list");
if (!list || !checks) return;
list.innerHTML = "";
Object.entries(checks).forEach(([key, check]) => {
const li = document.createElement("li");
li.classList.add("mb-1");
let icon = "❌";
let color = "text-danger";
if (check.status === "ok") {
icon = "✅";
color = "text-success";
} else if (check.status === "warning") {
icon = "⚠️";
color = "text-warning";
}
const title = key.replace(/_/g, " ");
li.innerHTML = `
<span class="${color}">
${icon} <strong>${title}</strong>
</span>
${check.message ? `<span class="text-muted"> — ${check.message}</span>` : ""}
`;
list.appendChild(li);
});
}
function renderStopQuantiles(metrics) {
const el = document.getElementById("stop_quantiles");
if (!el || !metrics) return;
el.innerHTML = `
<li>P50 (typical): ${(metrics.p50 * 100).toFixed(2)}%</li>
<li>P90 (wide): ${(metrics.p90 * 100).toFixed(2)}%</li>
<li>P99 (extreme): ${(metrics.p99 * 100).toFixed(2)}%</li>
`;
}
// =================================================
// RENDER RISK FORMULAS
// =================================================
function renderRiskFormulas(payload, result) {
const el = document.getElementById("risk_formulas");
if (!el) return;
const equity = payload.account_equity;
const riskFraction = payload.risk.risk_fraction;
const maxPositionFraction = payload.risk.max_position_fraction;
const stopMetrics = result.checks?.stop_sanity?.metrics;
if (!stopMetrics || stopMetrics.p50 == null) {
el.textContent = "Unable to compute risk formulas (missing stop metrics)";
return;
}
const stopP50 = stopMetrics.p50;
const idealPosition = (equity * riskFraction) / stopP50;
const maxPosition = equity * maxPositionFraction;
const effectivePosition = Math.min(idealPosition, maxPosition);
const effectiveRisk = (effectivePosition * stopP50) / equity;
el.innerHTML = `
<li>Position size (ideal)
= ${equity.toLocaleString()} × ${(riskFraction * 100).toFixed(2)}% ÷ ${(stopP50 * 100).toFixed(2)}%
${idealPosition.toFixed(0)}</li>
<li>Max position size
= ${equity.toLocaleString()} × ${(maxPositionFraction * 100).toFixed(2)}%
= ${maxPosition.toFixed(0)}</li>
<li>Effective position
= min(${idealPosition.toFixed(0)}, ${maxPosition.toFixed(0)})
= ${effectivePosition.toFixed(0)}</li>
<li>Effective risk
= ${effectivePosition.toFixed(0)} × ${(stopP50 * 100).toFixed(2)}% ÷ ${equity.toLocaleString()}
${(effectiveRisk * 100).toFixed(2)}%</li>
`;
}
// =================================================
// STOP TYPE UI LOGIC
// =================================================
function updateStopParamsUI() {
const stopType = document.getElementById("stop_type")?.value;
if (!stopType) return;
// Hide all stop param blocks
document.querySelectorAll(".stop-param").forEach(el => {
el.classList.add("d-none");
});
// Show relevant blocks
if (stopType === "fixed" || stopType === "trailing") {
document.querySelectorAll(".stop-fixed, .stop-trailing").forEach(el => {
el.classList.remove("d-none");
});
}
if (stopType === "atr") {
document.querySelectorAll(".stop-atr").forEach(el => {
el.classList.remove("d-none");
});
}
}
// =================================================
// RENDER PLOTS
// =================================================
function renderPositionSizePlot({
timestamps,
positionSizePct,
maxPositionFraction
}) {
const el = document.getElementById("position_size_plot");
if (!el) {
console.warn("[calibration_risk] position_size_plot not found");
return;
}
if (!timestamps || !positionSizePct || timestamps.length === 0) {
el.innerHTML = "<em>No position size data available</em>";
return;
}
const trace = {
x: timestamps,
y: positionSizePct.map(v => v * 100),
type: "scatter",
mode: "lines",
name: "Position size",
line: {
color: "#59a14f",
width: 2
}
};
const layout = {
yaxis: {
title: "Position size (% equity)",
rangemode: "tozero"
},
xaxis: {
title: "Time"
},
shapes: [
{
type: "line",
xref: "paper",
x0: 0,
x1: 1,
y0: maxPositionFraction * 100,
y1: maxPositionFraction * 100,
line: {
dash: "dot",
color: "#e15759",
width: 2
}
}
],
annotations: [
{
xref: "paper",
x: 1,
y: maxPositionFraction * 100,
xanchor: "left",
text: "Max position cap",
showarrow: false,
font: {
size: 11,
color: "#e15759"
}
}
],
margin: { t: 20 }
};
Plotly.newPlot(el, [trace], layout, {
responsive: true,
displayModeBar: true
});
}
function renderEffectiveRiskPlot({
timestamps,
effectiveRiskPct,
targetRiskFraction
}) {
const el = document.getElementById("effective_risk_plot");
if (!el) {
console.warn("[calibration_risk] effective_risk_plot not found");
return;
}
if (!timestamps || !effectiveRiskPct || timestamps.length === 0) {
el.innerHTML = "<em>No effective risk data available</em>";
return;
}
const trace = {
x: timestamps,
y: effectiveRiskPct.map(v => v * 100),
type: "scatter",
mode: "lines",
name: "Effective risk",
line: {
color: "#f28e2b",
width: 2
}
};
const layout = {
yaxis: {
title: "Effective risk (% equity)",
rangemode: "tozero"
},
xaxis: {
title: "Time"
},
shapes: [
{
type: "line",
xref: "paper",
x0: 0,
x1: 1,
y0: targetRiskFraction * 100,
y1: targetRiskFraction * 100,
line: {
dash: "dot",
color: "#4c78a8",
width: 2
}
}
],
annotations: [
{
xref: "paper",
x: 1,
y: targetRiskFraction * 100,
xanchor: "left",
text: "Target risk",
showarrow: false,
font: {
size: 11,
color: "#4c78a8"
}
}
],
margin: { t: 20 }
};
Plotly.newPlot(el, [trace], layout, {
responsive: true,
displayModeBar: true
});
}
function renderStopDistanceDistribution({
stopDistances,
quantiles
}) {
const el = document.getElementById("stop_distance_plot");
if (!el) {
console.warn("[calibration_risk] stop_distance_plot not found");
return;
}
if (!stopDistances || stopDistances.length === 0) {
el.innerHTML = "<em>No stop distance data available</em>";
return;
}
const valuesPct = stopDistances
.filter(v => v != null && v > 0)
.map(v => v * 100);
const trace = {
x: valuesPct,
type: "histogram",
nbinsx: 40,
name: "Stop distance",
marker: {
color: "#bab0ac"
}
};
const shapes = [];
const annotations = [];
if (quantiles) {
const qMap = [
{ key: "p50", label: "P50 (typical)", color: "#4c78a8" },
{ key: "p90", label: "P90 (wide)", color: "#f28e2b" },
{ key: "p99", label: "P99 (extreme)", color: "#e15759" }
];
qMap.forEach(q => {
const val = quantiles[q.key];
if (val != null) {
shapes.push({
type: "line",
xref: "x",
yref: "paper",
x0: val * 100,
x1: val * 100,
y0: 0,
y1: 1,
line: {
dash: "dot",
width: 2,
color: q.color
}
});
annotations.push({
x: val * 100,
y: 1,
yref: "paper",
xanchor: "left",
text: q.label,
showarrow: false,
font: {
size: 11,
color: q.color
}
});
}
});
}
const layout = {
xaxis: {
title: "Stop distance (% price)"
},
yaxis: {
title: "Frequency"
},
shapes: shapes,
annotations: annotations,
margin: { t: 20 }
};
Plotly.newPlot(el, [trace], layout, {
responsive: true,
displayModeBar: true
});
}
// =================================================
// UTILS
// =================================================
function num(id) {
const el = document.getElementById(id);
if (!el) return null;
const val = el.value;
if (val === "" || val === null || val === undefined) return null;
const n = Number(val);
return Number.isFinite(n) ? n : null;
}
function buildRiskPayload() {
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
const stopType = document.getElementById("stop_type")?.value;
const stop = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
stop.stop_fraction = num("stop_fraction");
}
if (stopType === "atr") {
stop.atr_period = num("atr_period");
stop.atr_multiplier = num("atr_multiplier");
}
const payload = {
symbol,
timeframe,
account_equity: num("account_equity"),
risk: {
risk_fraction: num("risk_fraction") / 100,
max_position_fraction: num("max_position_fraction") / 100,
},
stop,
global_rules: {
max_drawdown_pct: num("max_drawdown_pct") / 100,
daily_loss_limit_pct: num("daily_loss_limit_pct")
? num("daily_loss_limit_pct") / 100
: null,
max_consecutive_losses: num("max_consecutive_losses"),
cooldown_bars: num("cooldown_bars"),
},
};
console.log("[calibration_risk] FINAL PAYLOAD:", payload);
return payload;
}
// =================================================
// INIT
// =================================================
document.addEventListener("DOMContentLoaded", () => {
console.log("[calibration_risk] DOMContentLoaded ✅");
document
.getElementById("inspect_risk_btn")
?.addEventListener("click", inspectCalibrationRisk);
const stopTypeSelect = document.getElementById("stop_type");
stopTypeSelect?.addEventListener("change", updateStopParamsUI);
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (symbol && timeframe) {
const symbolEl = document.getElementById("ctx_symbol");
const timeframeEl = document.getElementById("ctx_timeframe");
if (symbolEl) symbolEl.textContent = symbol;
if (timeframeEl) timeframeEl.textContent = timeframe;
}
// Initial state
updateStopParamsUI();
});
document.addEventListener("DOMContentLoaded", () => {
console.log("[calibration_risk] DOMContentLoaded ✅");
document
.getElementById("validate_risk_btn")
?.addEventListener("click", validateCalibrationRisk);
const stopTypeSelect = document.getElementById("stop_type");
stopTypeSelect?.addEventListener("change", updateStopParamsUI);
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (symbol && timeframe) {
const symbolEl = document.getElementById("ctx_symbol");
const timeframeEl = document.getElementById("ctx_timeframe");
if (symbolEl) symbolEl.textContent = symbol;
if (timeframeEl) timeframeEl.textContent = timeframe;
}
// Initial state
updateStopParamsUI();
});
document.addEventListener("DOMContentLoaded", () => {
console.log("[calibration_risk] DOMContentLoaded ✅");
document
.getElementById("generate_report_btn")
?.addEventListener("click", generateRiskReport);
});

View File

@@ -3,7 +3,55 @@
{% block content %}
<div class="container-xl">
<h2 class="mb-4">Calibración · Paso 1 · Datos</h2>
<!-- ========================= -->
<!-- Wizard header -->
<!-- ========================= -->
<div class="d-flex align-items-center mb-4">
<!-- Back arrow (disabled on step 1) -->
<div class="me-3">
<button class="btn btn-outline-secondary btn-icon" disabled>
<svg xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-left"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="19" y1="12" x2="5" y2="12" />
<line x1="11" y1="18" x2="5" y2="12" />
<line x1="11" y1="6" x2="5" y2="12" />
</svg>
</button>
</div>
<!-- Center title -->
<div class="flex-grow-1 text-center">
<h2 class="mb-0">Calibración · Paso 1 · Datos</h2>
</div>
<!-- Forward arrow (JS will enable when Step1 is OK) -->
<div class="ms-3">
<a
id="next-step-btn"
href="/calibration/risk"
class="btn btn-outline-secondary btn-icon"
aria-disabled="true"
>
<svg xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-right"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="13" y1="6" x2="19" y2="12" />
<line x1="13" y1="18" x2="19" y2="12" />
</svg>
</a>
</div>
</div>
<!-- FORMULARIO -->
<div class="card mb-4">

View File

@@ -1,99 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<!-- Page header -->
<div class="page-header d-print-none mb-4">
<div class="row align-items-center">
<div class="col">
<h2 class="page-title">
Calibration · Step 1 — Data
</h2>
<div class="text-muted mt-1">
Select market data used for calibration
</div>
</div>
</div>
</div>
<!-- Data selection card -->
<div class="row">
<div class="col-12 col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="card-title">Market data</h3>
</div>
<div class="card-body">
<div class="row g-3">
<!-- Symbol -->
<div class="col-md-6">
<label class="form-label">Symbol</label>
<select class="form-select">
<option selected>BTC/USDT</option>
<option>ETH/USDT</option>
</select>
</div>
<!-- Timeframe -->
<div class="col-md-6">
<label class="form-label">Timeframe</label>
<select class="form-select">
<option>1m</option>
<option>5m</option>
<option>15m</option>
<option selected>1h</option>
<option>4h</option>
<option>1d</option>
</select>
</div>
<!-- Start date -->
<div class="col-md-6">
<label class="form-label">Start date</label>
<input type="date" class="form-control">
</div>
<!-- End date -->
<div class="col-md-6">
<label class="form-label">End date</label>
<input type="date" class="form-control">
</div>
</div>
</div>
<div class="card-footer text-end">
<a href="/calibration/step2" class="btn btn-primary">
Continue
</a>
</div>
</div>
</div>
<!-- Help / info -->
<div class="col-12 col-lg-4">
<div class="card">
<div class="card-body">
<h4>What happens in this step?</h4>
<p class="text-muted">
You define the historical market data that will be used to:
</p>
<ul class="text-muted">
<li>download candles</li>
<li>compute indicators</li>
<li>calibrate strategies</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,246 @@
{% extends "layout.html" %}
{% block content %}
<div class="container-xl">
<!-- ========================= -->
<!-- Wizard header -->
<!-- ========================= -->
<div class="d-flex align-items-center mb-4">
<!-- Back arrow -->
<div class="me-3">
<a href="/calibration/data" class="btn btn-outline-secondary btn-icon">
<svg xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-left"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="19" y1="12" x2="5" y2="12" />
<line x1="11" y1="18" x2="5" y2="12" />
<line x1="11" y1="6" x2="5" y2="12" />
</svg>
</a>
</div>
<!-- Center title -->
<div class="flex-grow-1 text-center">
<h2 class="mb-0">Calibración · Paso 2 · Risk & Stops</h2>
</div>
<!-- Forward arrow (disabled until Step2 OK) -->
<div class="ms-3">
<a
id="next-step-btn"
href="/calibration/strategies"
class="btn btn-outline-secondary btn-icon"
aria-disabled="true"
>
<svg xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-right"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="5" y1="12" x2="19" y2="12" />
<line x1="13" y1="6" x2="19" y2="12" />
<line x1="13" y1="18" x2="19" y2="12" />
</svg>
</a>
</div>
</div>
<!-- ========================= -->
<!-- Risk configuration form -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-body">
<h3 class="card-title">Data Context</h3>
<div class="row g-3">
<!-- Symbol -->
<div class="col-md-4">
<label class="form-label">Símbolo</label>
<div
class="form-control bg-light text-muted"
style="cursor: not-allowed;"
>
<span id="ctx_symbol">-</span>
</div>
</div>
<!-- Timeframe -->
<div class="col-md-4">
<label class="form-label">Timeframe</label>
<div
class="form-control bg-light text-muted"
style="cursor: not-allowed;"
>
<span id="ctx_timeframe">-</span>
</div>
</div>
</div>
<hr class="my-4">
<h3 class="card-title">Risk configuration</h3>
<div class="row g-3">
<!-- Account equity -->
<div class="col-md-4">
<label class="form-label">Account equity</label>
<input id="account_equity" class="form-control" type="number" value="10000" min="0">
</div>
<!-- Risk per trade -->
<div class="col-md-4">
<label class="form-label">Risk per trade (%)</label>
<input id="risk_fraction" class="form-control" type="number" step="0.01" value="0.5">
</div>
<!-- Max position -->
<div class="col-md-4">
<label class="form-label">Max position size (%)</label>
<input id="max_position_fraction" class="form-control" type="number" step="1" value="25">
</div>
</div>
<hr class="my-4">
<!-- Stop configuration -->
<h3 class="card-title">Stop configuration</h3>
<div class="row g-3">
<!-- Stop type -->
<div class="col-md-4">
<label class="form-label">Stop type</label>
<select id="stop_type" class="form-select">
<option value="fixed">Fixed stop</option>
<option value="trailing">Trailing stop</option>
<option value="atr">ATR stop</option>
</select>
</div>
<!-- Fixed / trailing -->
<div class="col-md-4 stop-param stop-fixed stop-trailing">
<label class="form-label">Stop distance (%)</label>
<input id="stop_fraction" class="form-control" type="number" step="0.1" value="2">
</div>
<!-- ATR params -->
<div class="col-md-4 stop-param stop-atr d-none">
<label class="form-label">ATR period</label>
<input id="atr_period" class="form-control" type="number" value="14">
</div>
<div class="col-md-4 stop-param stop-atr d-none">
<label class="form-label">ATR multiplier</label>
<input id="atr_multiplier" class="form-control" type="number" step="0.1" value="3">
</div>
</div>
<hr class="my-4">
<!-- Global rules -->
<h3 class="card-title">Global rules</h3>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Max drawdown (%)</label>
<input id="max_drawdown_pct" class="form-control" type="number" step="1" value="20">
</div>
</div>
<div class="mt-4">
<button id="inspect_risk_btn" class="btn btn-primary">
Inspect Risk & Stops
</button>
<button id="validate_risk_btn" class="btn btn-primary">
Validate Risk & Stops
</button>
<button id="generate_report_btn" class="btn btn-outline-dark">
Generate PDF Report
</button>
</div>
</div>
</div>
<!-- ========================= -->
<!-- Risk quality result -->
<!-- ========================= -->
<div id="risk_result" class="d-none">
<div class="card">
<div class="card-body">
<h3 class="card-title">Risk quality</h3>
<div id="risk_status_badge" class="mb-3"></div>
<p id="risk_message" class="mb-3"></p>
<pre id="risk_debug" class="bg-light p-3 rounded small d-none"></pre>
</div>
</div>
<!-- RISK CHECKS -->
<div class="mt-3">
<ul id="risk_checks_list" class="list-unstyled mb-0"></ul>
</div>
<!-- STOP QUANTILES -->
<div class="mt-3">
<h5 class="mb-2">Stop distance statistics</h5>
<ul class="list-unstyled text-muted" id="stop_quantiles">
<li>P50: </li>
<li>P90: </li>
<li>P99: </li>
</ul>
</div>
<!-- RISK FORMULAS -->
<div class="mt-3">
<h5 class="mb-2">Risk sizing breakdown</h5>
<ul id="risk_formulas" class="list-unstyled text-muted">
Calculating…
</ul>
</div>
</div>
<!-- ========================= -->
<!-- Gráficas -->
<!-- ========================= -->
<!-- POSITION SIZE OVER TIME -->
<div class="mt-4">
<h5 class="mb-2">Position size over time</h5>
<div id="position_size_plot" style="height: 360px;"></div>
</div>
<!-- EFFECTIVE RISK OVER TIME -->
<div class="mt-4">
<h5 class="mb-2">Effective risk over time</h5>
<div id="effective_risk_plot" style="height: 360px;"></div>
</div>
<!-- STOP DISTANCE DISTRIBUTION -->
<div class="mt-4">
<h5 class="mb-2">Stop distance distribution</h5>
<div id="stop_distance_plot" style="height: 360px;"></div>
</div>
</div>
<!-- Plotly -->
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<!-- Page logic -->
<script src="/static/js/pages/calibration_risk.js"></script>
{% endblock %}