Step 2: Risk and Stops - Generating reports with pdf. It is missing to reorder the PDF.
This commit is contained in:
188
reports/risk_report_008bec591119454f9c18fb13725f0b76.pdf
Normal file
188
reports/risk_report_008bec591119454f9c18fb13725f0b76.pdf
Normal file
File diff suppressed because one or more lines are too long
119
reports/risk_report_35d067a3179043438cf1f924c072966e.pdf
Normal file
119
reports/risk_report_35d067a3179043438cf1f924c072966e.pdf
Normal file
File diff suppressed because one or more lines are too long
93
reports/risk_report_47be73ff7ac84f2f9bf59dba82638cc0.pdf
Normal file
93
reports/risk_report_47be73ff7ac84f2f9bf59dba82638cc0.pdf
Normal 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
|
||||||
188
reports/risk_report_4d4066816bf4421a9805a3bff768b08b.pdf
Normal file
188
reports/risk_report_4d4066816bf4421a9805a3bff768b08b.pdf
Normal file
File diff suppressed because one or more lines are too long
93
reports/risk_report_807b2811f8d44b2e90248217e8632301.pdf
Normal file
93
reports/risk_report_807b2811f8d44b2e90248217e8632301.pdf
Normal 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
|
||||||
188
reports/risk_report_861f294f412e4bacb26b30ac7c33c6d6.pdf
Normal file
188
reports/risk_report_861f294f412e4bacb26b30ac7c33c6d6.pdf
Normal file
File diff suppressed because one or more lines are too long
74
reports/risk_report_861f4e713b52441f8718aa6e508326a3.pdf
Normal file
74
reports/risk_report_861f4e713b52441f8718aa6e508326a3.pdf
Normal 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
|
||||||
74
reports/risk_report_8774df860dc745f1856455be173c2047.pdf
Normal file
74
reports/risk_report_8774df860dc745f1856455be173c2047.pdf
Normal 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
|
||||||
74
reports/risk_report_8dec26ec4eec430494e3d31c828904ab.pdf
Normal file
74
reports/risk_report_8dec26ec4eec430494e3d31c828904ab.pdf
Normal 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
|
||||||
74
reports/risk_report_a29912469dbb4e41b4552f9f0541f4ad.pdf
Normal file
74
reports/risk_report_a29912469dbb4e41b4552f9f0541f4ad.pdf
Normal 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
|
||||||
188
reports/risk_report_a83677983e4d47529ec9171e17b0bdce.pdf
Normal file
188
reports/risk_report_a83677983e4d47529ec9171e17b0bdce.pdf
Normal file
File diff suppressed because one or more lines are too long
179
reports/risk_report_af5958b64b2b42ad91536049370e9681.pdf
Normal file
179
reports/risk_report_af5958b64b2b42ad91536049370e9681.pdf
Normal file
File diff suppressed because one or more lines are too long
167
reports/risk_report_d2476cfc5ce14cf7a7b03274e6e15e60.pdf
Normal file
167
reports/risk_report_d2476cfc5ce14cf7a7b03274e6e15e60.pdf
Normal file
File diff suppressed because one or more lines are too long
188
reports/risk_report_f893e90c579148efb4b628031579743d.pdf
Normal file
188
reports/risk_report_f893e90c579148efb4b628031579743d.pdf
Normal file
File diff suppressed because one or more lines are too long
@@ -49,6 +49,7 @@ python-dotenv==1.0.0
|
|||||||
pytz==2025.2
|
pytz==2025.2
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
redis==5.0.1
|
redis==5.0.1
|
||||||
|
reportlab==4.4.9
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
seaborn==0.13.2
|
seaborn==0.13.2
|
||||||
setuptools==80.10.1
|
setuptools==80.10.1
|
||||||
|
|||||||
0
src/calibration/reports/__init__.py
Normal file
0
src/calibration/reports/__init__.py
Normal file
444
src/calibration/reports/risk_report.py
Normal file
444
src/calibration/reports/risk_report.py
Normal 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)
|
||||||
449
src/calibration/risk_inspector.py
Normal file
449
src/calibration/risk_inspector.py
Normal 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,
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import time
|
|||||||
|
|
||||||
from .settings import settings
|
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_data import router as calibration_data_router
|
||||||
|
from src.web.api.v2.routers.calibration_risk import router as calibration_risk_router
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -92,11 +93,24 @@ def create_app() -> FastAPI:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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 routers (versionados)
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
api_prefix = settings.api_prefix
|
api_prefix = settings.api_prefix
|
||||||
app.include_router(calibration_data_router, prefix=api_prefix)
|
app.include_router(calibration_data_router, prefix=api_prefix)
|
||||||
|
app.include_router(calibration_risk_router, prefix=api_prefix)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
285
src/web/api/v2/routers/calibration_risk.py
Normal file
285
src/web/api/v2/routers/calibration_risk.py
Normal 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,
|
||||||
|
)
|
||||||
84
src/web/api/v2/schemas/calibration_risk.py
Normal file
84
src/web/api/v2/schemas/calibration_risk.py
Normal 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]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* =================================================
|
||||||
|
Wizard navigation (Calibration steps)
|
||||||
|
================================================= */
|
||||||
|
|
||||||
|
a[aria-disabled="true"] {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ async function inspectCalibrationData() {
|
|||||||
resultEl.textContent = "⏳ Inspeccionando datos en DB...";
|
resultEl.textContent = "⏳ Inspeccionando datos en DB...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// por si vienes de un estado previo OK y ahora inspeccionas otra cosa
|
||||||
|
disableNextStep();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/v2/calibration/data/inspect", {
|
const res = await fetch("/api/v2/calibration/data/inspect", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -48,11 +51,22 @@ async function inspectCalibrationData() {
|
|||||||
|
|
||||||
renderDataSummary(data);
|
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) {
|
} catch (err) {
|
||||||
console.error("[calibration_data] inspect FAILED", err);
|
console.error("[calibration_data] inspect FAILED", err);
|
||||||
if (resultEl) {
|
if (resultEl) {
|
||||||
resultEl.textContent = "❌ Error inspeccionando datos";
|
resultEl.textContent = "❌ Error inspeccionando datos";
|
||||||
}
|
}
|
||||||
|
disableNextStep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +188,28 @@ function renderDownloadProgress(job) {
|
|||||||
text.textContent = job.message || job.status;
|
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) {
|
function renderDataSummary(data) {
|
||||||
@@ -209,6 +243,9 @@ function renderDataSummary(data) {
|
|||||||
logEl.textContent =
|
logEl.textContent =
|
||||||
`❌ No hay datos para ${data.symbol} @ ${data.timeframe}`;
|
`❌ 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,8 +278,9 @@ function renderDataSummary(data) {
|
|||||||
`${(dq.checks.coverage.ratio * 100).toFixed(2)}%`;
|
`${(dq.checks.coverage.ratio * 100).toFixed(2)}%`;
|
||||||
document.getElementById("dq-volume").textContent =
|
document.getElementById("dq-volume").textContent =
|
||||||
dq.checks.volume;
|
dq.checks.volume;
|
||||||
|
} else {
|
||||||
|
dqCard?.classList.add("d-none");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|||||||
809
src/web/ui/v2/static/js/pages/calibration_risk.js
Normal file
809
src/web/ui/v2/static/js/pages/calibration_risk.js
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -3,7 +3,55 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-xl">
|
<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 -->
|
<!-- FORMULARIO -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
246
src/web/ui/v2/templates/pages/calibration/calibration_risk.html
Normal file
246
src/web/ui/v2/templates/pages/calibration/calibration_risk.html
Normal 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 %}
|
||||||
Reference in New Issue
Block a user