From 50960bab216e1d442675a0275ace57797603d9fd Mon Sep 17 00:00:00 2001 From: Hermes - BrainSteel VPS Date: Sun, 17 May 2026 16:25:55 +0000 Subject: [PATCH] =?UTF-8?q?BrainSteel=20Fin=20v1.0=20=E2=80=94=203-agent?= =?UTF-8?q?=20BTC=20trading=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Brief: Market Intelligence (Binance data + LLM analysis) - Decio: Strategy decision (BUY/HOLD/SELL) - PaperT: Order executor (Binance API) - Anime-style Flask dashboard - Traefik-ready Docker deployment --- .env | 1 + .gitignore | 16 + Dockerfile | 7 + agents/__init__.py | 1 + agents/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 140 bytes agents/__pycache__/audit.cpython-313.pyc | Bin 0 -> 9550 bytes agents/__pycache__/brief.cpython-313.pyc | Bin 0 -> 23660 bytes agents/__pycache__/decio.cpython-313.pyc | Bin 0 -> 9584 bytes agents/__pycache__/papert.cpython-313.pyc | Bin 0 -> 18197 bytes agents/audit.py | 173 +++++ agents/brief.py | 497 ++++++++++++++ agents/decio.py | 182 +++++ agents/papert.py | 385 +++++++++++ agents/token_tracker.py | 78 +++ app.py | 702 ++++++++++++++++++++ data/.env | 3 + data/agents.db | Bin 0 -> 28672 bytes data/executions.db | Bin 0 -> 20480 bytes data/portfolio.db | Bin 0 -> 20480 bytes logs/watchdog.log | 10 + requirements.txt | 2 + static/script.js | 608 +++++++++++++++++ static/style.css | 407 ++++++++++++ templates/index.html | 213 ++++++ 24 files changed, 3285 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 agents/__init__.py create mode 100644 agents/__pycache__/__init__.cpython-313.pyc create mode 100644 agents/__pycache__/audit.cpython-313.pyc create mode 100644 agents/__pycache__/brief.cpython-313.pyc create mode 100644 agents/__pycache__/decio.cpython-313.pyc create mode 100644 agents/__pycache__/papert.cpython-313.pyc create mode 100644 agents/audit.py create mode 100644 agents/brief.py create mode 100644 agents/decio.py create mode 100644 agents/papert.py create mode 100644 agents/token_tracker.py create mode 100644 app.py create mode 100644 data/.env create mode 100644 data/agents.db create mode 100644 data/executions.db create mode 100644 data/portfolio.db create mode 100644 logs/watchdog.log create mode 100644 requirements.txt create mode 100644 static/script.js create mode 100644 static/style.css create mode 100644 templates/index.html diff --git a/.env b/.env new file mode 100644 index 0000000..798f7fd --- /dev/null +++ b/.env @@ -0,0 +1 @@ +OPENROUTER_API_KEY=sk-or-v1-26c1cbfbe091551a814874f8141a2739b70deef89f464be668fdcac49cfabecd7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eeb2ec7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# BrainSteel Fin - .gitignore +__pycache__/ +*.pyc +*.pyo +venv/ +.venv/ +*.db +*.log +.env +data/*.db +data/.env +logs/*.log +*.egg-info/ +dist/ +build/ +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7d23cc4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir flask requests +COPY . . +EXPOSE 3100 +CMD ["python", "app.py"] diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000..a79f4dc --- /dev/null +++ b/agents/__init__.py @@ -0,0 +1 @@ +# BrainSteel Fin Agents \ No newline at end of file diff --git a/agents/__pycache__/__init__.cpython-313.pyc b/agents/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5b729e2a2ad20c1223a28797bbabf2b1a382fff GIT binary patch literal 140 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 literal 0 HcmV?d00001 diff --git a/agents/__pycache__/brief.cpython-313.pyc b/agents/__pycache__/brief.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65faad205943fc07d39efeecef932678edd5eb18 GIT binary patch literal 23660 zcmd6P3vgRkdfvtRL4pMM1o#9=i6Zy_B|h|^CDD9IvK}PGD_E2%84H3S2?+$K3y_ky z@~-2$8G78#LfN>YY-dI7?$qG!Hm2KYmub>QesrqMZcn52bY-SqT+D_Kq zblUH~7Z(62P*ReeOs~Wr|2^kFk9+Pp|M_3%J~SBg46cJ(-9@E?Vg8hEq)VAZ9{vH4 zHyD=T7*@v0Psli#@GIx!!mol;2){~BiC@JD)o~T4IYutS}Sv2Hc}u} zo&K$5SQV@8l#3~xep@O{235|sRN4%xTr-2(b!7~zPt|XjlG}3TehaPXdpVca&xSl6 zpY@2>Z~c>(f79B_c|DWXo+*z%WNnylv*}N{xJ#ap^*MjY>Ty)K_vRNrYG_WFD-F4$!q^0)L&qaEvYYrlu{&bvb1c~1~`EZXqSP(#

O^5&r*$Q#Ug z2Ctc+SB%#yFJtg>6@V&M38-dOfOsW7ch@C0OqrK zfJQbS(8L-63s@6ZI8|UP<{-O3IsJY??t{CG?&*^o~3?a6}qi zzF=s0U~pu}ab{pdO5^BJ7Fwo5q1j+pYinTE>Isi$Ht6wOYL$?d`Hq%JpDQ@sHOYBAHZ`yGyD&%OfgrD*@`OD8c|I>) zxAT(c+NS$C&c~}}0+>yhOUj_fHyNbiw(IEnZD2D6*-%c__$A``BW z)GUxvgm({q#VQWS{c?vq+w5SKU84-+s%MxndNINUO09&ae@AlCxP)~kkg*=d+rOK53q^PugUkR1Vc+`ZR<- zsUF)WwL_h?Pr5W8#z^H*uePDg-9%}43}xOX%6x%Rwu7l*tV}gC_@53bs+mby^6r() zI|r!5dz*3LAlNik9oRHYee1l}W$pC^+?RwjJC>q#c5d9~bz8w-txeV$&&+Hf;Ijv3 zU2czkp;=nZF?kv{ydI_ffthQR znnT3Ndz*&Wa-Pd`s4U1Uz=`;rV8|7k3p(B4HoR)m7jT7mWyo_i#H)fK&O6I1F9rjC zn}#c;hSi)Wh;5CRb9?aD#w+Q40Bh%!XeAibfXFIdoS6!koB_Y^mYn0>(4_C0hvO?U z`O(%~3kn4-Sg$WLpPC96)f;lP&h%#{%gu`4sEAgbj6Q!hW*S}WP2`)t@wL~!wpxeqP5;M6rmrayfZeF=@<(t>S z%EYdwcdp*Kx|*9=fi9zhQ_-`dG1Iy2E7)m!XY|f!#JAQH-+35)Scn;G6UoGxwbSvP z-BMyh^Sjy=?P|r^<#q7e(|E= z%OIE`^J-5{zma+0*rV$&Wj-kD>FBRwJ~GPtixeM~%KOU|A63cw>lB;zXf=un{P{0| zyzw+VP~y!phwL%@rWag7HbO6c%a!7R@-dy11>T*F8)itDtfmE=oYkiG`RpZhX*rVB z+vPj(Ji8f*uMn2x9XWH)pzXdnjw4Ynx!kNY=RJaeD6HS072EuT*FV*4#j=?qrV#L3 zeV%!bFN1qzU8~8&mf+kB*7r5860LDn1grpjPACv^`JD4X?}E5=r;y6L{HncYo%SSN z8@h^>H|GzLC@D}mXU2IwQjcrHtd+ks73-#o`lJ zUP)@@CHv?Uo|>=8w){4cbWvR|0lds?D6s+_7|l0xez}>b9;-icPZqZxPt?}G({QID z(zSXyUVGp(mBL)G!6-}x8!86t)W{eM7InYaP>J&#i+fS`WiS^#c%`SlrCf} z^K7R1Cd_6f#_{QAGppPU8Q_4zzulZx3-ZRXC(r3^X0oQ0k!CKdWpzrXka=Dal(#ZL zc_DMGh+zWlSyzCb&5_Ct%TJXsOb%mZ{7S!q&2^y9()=Nk9;g53hMKZMP%-she1h7NTKOM+MTxZR%F(3eIRHd&}r z)P+{sSytp+HTunKrai6+4;K{IjYiyOf`jWMR`M#LazQrd^Tp0MNk0pDJ;4=awiSYP zLmL*?O@I~$FZ1zQsZ5Y+#Y&OZiouzzl!nQ}n=_Y4W)6*T@NEFB40TRwWh9I*B+QnC zr6yX}88z?yOrtdAE%t2a8KWuNGD++|`1{)5(cZP(>y7UpiVlxNPmjd*v+?pXKi$u+ z?zug1YhZa-r0tI@>_`o3qvdB3W?N)`w6r5??AXxIipdpL%om^QmvC~>7If(DQf`!Iootz8JAyr~V;6n!` zJ~Sza+ajI7eJFv{@mbC@~qbsAU zm+tO~w+zI09ZBj04;oq`6Yoy1Os`(Qdpd4E8gDq3l)n?ryWc&waxB_;jfz#BvD*?^X!eY-#izVKd@9Rjog0z*7M7Qt3C0`y>ZLF zusUJyh?X|Sj7>m(xbWV>-R9`<>G-~pn0bEi*kdijS=F{+-)!^ePPVuUt5SP3K7h|8~OB?v!wpmCI<;!jm6{mj*mAU9MW< zwW&Me{0de_TlkO+W+o-8-^R$2!{$nym_(?o3W8zbU|+!NKMKwe1pCS4RIaHh&ND@t z0@-|#?YRo+hV+9B{&Z2|PqY+Uh)20m-4sa=L+-(-ow6KVNrS{BU z!xVmkSd=m0FA5W0MX$rJ%n)|6TNc%@Ekcl^OA=W8swb9$li-n3>a-{^>TsxAP$R1& zC}`+<5(Ls(3q&1JC{nS8uHVF>MIYF_A)Q0T=6AubB-|T^WUL94f~Y|jz8Z$Km<(HA z;lj*7(k5s1c`wg-p^lI;z7X(Z*^%Lo)>&G5fWGdARFAn0D!ZPA@yqS$O34#a6`l#} zY#<1I0p@ziR!EADePLW$zmLinqAVg>+oybiahLBTtOM+HfD1vJA+3Fcn$1|lBy-fm zePhuV-LXD*taBndburd?G3uL%xBA0JqUBAoqUJNNi(HwXmiuLiqu1gDkVL@K$lTNb`2>@zgUA^xdZG^{LLy!Aov6A3%ew?LlT2)Q?b@b|5R(YXgz7$qQM9| zy|{@BEI~Ez^38eH1zEjFz@2UqJ-m}i8w;-)OfmtrD7)B{yl!&J>lBDZR44%_15!mA zK96wdRr4ZQcCNuId~l9l!0Sf-7GsOb8}iHqIe~8|siGiR86ZmFk{kn*7#$}hEYAcq z7>vzk9Jzad~$b))Yr2gH;mB!UkSnwNr;`P0s96R}gx};`t<*qz#?~6C~f2LDalx#4n z;u6UAaEi+^60+Mou~9Ow^qKm~m=B8-1k2=o)!WD=lVBV#s$bMRy@3Bj=JEAk1@mAb zfhZZjq7_#B)3O;@L|Gkv)fqB~rUT}eGUlSzp(tb|KY_GZH_13^P$EoW8%mW!>BwX= zDV8)kz~+*qA}}T;BoPB^qy;>BW=ndWn9k;lzuR~bOmllsSs!<8HWV0kArwUL9r!(0 zg4Wd5YL(Q50GC?jBho5&3q@zCsGt>K)eEMJ0L1LpZr5CB+8GL5^7s$cy9HN;8-`6X zVl)q&Io8Y$ovgovC31)w#D{ahqIMw55=W^YB%G!?G9Ubr2ihU>q>6AM1cg-86!w zu=QfG;-uYWMn0CZggNAxQizwis6CTq@=5!|1jN@5be2Qw;TGMK%a` zwaOQ`;^9^lyxhK1rOU( zrPA1s(2s}Yj{3zq^D+}1y7JT6`RFCz{o%8*;j{7KbI}Vg-oG#ryD$;IF!{&BlgRzz zAq~nuY1u6kCQ`6f>gq4eOBx}_bz31{C)Z@ z^wy5zUkJUmT>Z9VS@uK6dychzcYEWVMGm?#>1JPU zR+9ff75)Xl7bG_orR1iBaRN4&Cs|r0*~t>=jhkCn6{`hNb4OV9xtuBU$-`>;9l-B{ z63fSyt9Kl6%hhWMqxWCS(~2`Q!t$X0_>1)XA&8g}qJ82j(1tI}`+)F7Zb_K=6KE{iz5xmIU-&>l}rVXWsWSLu%mE!6Y+bD3E@mtwlQwaQ& zw#03mSDBGr+3PT*>tORprrJhk%3r7x467dZrGPaU5W;UM0s+QT(t)h--E#2n*d-B( z6d}QaNCnbDQad6oA%?WV3`z(>l&~>*5TeA*d29*_({f*ey@St5hF1vzNqH1IHHYxW zYficsWTq84HWwG?f?7fjXUwaTr`LrKq<+#@B0D$gknvU5J*n?tOrWz%*kjDIrgkI`U|I1RA`V+Koj zHc{8Owm0e+i|RZL*U3bq{oRI@hG_fId!~5fiG;n4d@<2|XAsyGw~s{$N>+uX4fO>}`>_G3N9C@+!s33S2^Ya1 z%2e=&X{-5`)siS7sXYk$C`^WZs1P;^+4Qkx1^-8uvg)TOtKX(w%~O=k*`}-(YFoFKU5)F(McUD#D?JfwBvUvUwf%Vx!#320EY$gq{1i2$w9&s#)>Gg&j^UZbi!H>zT!)bqS<0s| zNqe;Xri>n`*diL6R7=G~?=RRyQL>4mRG=ty6gUd0UVo9Jh&7W*-(T#|j$kHky~c{A z6o*!7NBZ@bqh%(HCjTgocvFU_nsV@|21mc#cr@ddp!$CV%smEA`Z-jFBixTX8c|i?gN{ zIf{i@oEjZRsl)8BILaL58FN+3S~K5Way~U?j{hd=noR0VIh*RC8J~PRDZen@P9a-s z&ynVnRd7Zn!|ark2)46Qz}@39U5YLz6RTmy%A^6*FfLk=71eNx`73%5wFPUkfKAFK zrC`;%g=hL){t2I_W!x2n$(RDp;buWBc#zZ=I^5J`9bqZN{Zvn1zZJ1`T;M7Nu)`o@ zU)Y%`N+rWFG6ktiJ};bTye1hO!^`(f3sMloD76?j!)t|D64-`eP@wQV(c1K1@D%H$ zOsj(Q!vz4=-Gq%$_Ro>MPFuG3&k3UunR@dUT|esf z{t=P{f7tpjDU+t0_b&(|9nyOin0J)GUjo>2c$FA}qu_#GUOD4(PmtK>o^S@eQwV6} z^Mo704Nf8%uSpId8JnaLR=Otd~U2R(dk4bow!Zk;}?&*Wapt zt0AoUSt~`}eD6x6=Ix!!vgMjP%5OETTtTpnW#7;2opJNt?@dKaZx=6JUN+qdeXD$B zDy%~6=FeI>Aj*F4Y()09YRR=MyEXBxoRza-Wwf;OF!(?TeVAj-m1OhqtXXIvA4h7zBKn%S4941_gZmS z`$=mj66W7}CQ|Uz@}{*;q%?PgM}Jz}uvUwBq>}QbzUB5vO=Ntf{?6f8MH|ABO7?~o z=rux>jx4t?cPybNwn%TxY!9m*n9CncOjOh?58OHx)+H>p%j~V{uo_O!%9`89Zyo<` zFw*gzYw?ObVcmnW>NU&V)4yB!UBz;Lq%2~K?&$c~+WC{d=i=2zR5Z$%^lyd2h}?}M__vgqlvKOT#ozYsrl;q3uLh}7QcT2-%d@9AT8U2CUfb%*cP z#cBucUjDe^xu|;r9UK~ovgdyM)#!^~i4T3{ZO8JFNZXy`t0ilS_sU~C4z68}?RfTX zU#t%5$;SwHpPUNo(?wkIAy-B#q4#oQbyTE|iquk(iWAYP>9FooW64d!4MU=`Zn^i? z3yJco2N*qDR9vPyD~VC64mvI+U7(}qX-|o=!%Zx z$uC8xe9`fl_>2DNWiI|A_XE`r+^aiQgR7y{#_tC{QrzvoS9GuV?uq!b!!i4Clzl#C ze?B^T?&F3RqF2BAS!r%{!OxjoQ{hIH;GzEF^D!Ayw*L#>y!Q*fv{U%|T+Nh?$dJ_a z1#da{i=Q8nF-FTn2BByFHP@J+BPYKU0`tENUd8Hswfkf@^S;7-vO@WOxtj1A^T`_J z`v(NP+kCQK`9Ue+A5@x8HYq>ov;hAwujJ%z<%c_TgtzEUb}B#YQUiX(P|8O--N`QH zM@^>=bJkkC20^~W_2ze^ETqC+I z3ApV{Ci&%L3_78rh_w;g=eArp;Hq)RCEbS(1g0eaTC}GWwLm`(^|3;T4}Jx0h}Dza zk2^u*+G++8-B|EO+;ov~m6)S?dgi~%}0L>8`eD+Rw;E={iX9S^TMzf`k z%1w$yc5gtkytH^ilQ`2FxY7cjQcKFLlHrOX;b3rKu;Ki~S36#60c=Y`+t`acE;M#G zv~-`}(LHv4qWMCT?R>DYVFstLu@hb&7k_Qtl!!BPXyiH^OZ00KqysVjMP}vx1WgEb zZaOc64v~4s!{-u4)Yz`CAcVz6R1g=FCuV3Ix$gnspqhu*;I!-n9$}NyS|BOLY~nSL zrrgsW(Rclb*V>3+7Q_NNr3PUD$~3cV$!tc2L8zvJp#@>2nFndk#*Cv`ebzUx-?;wG zuZ5L=YP5V>R&l%W&Bo}?`RLW_@v@hMaJNL|j^&YCV+eI?uw!G<-Q9by=KTZrg70@n zpFa=I8hhS(|9MZ;?TPhG#>%InMN<@>vhDer!@UaT1BJX-`;W84&IwD5Y_D6c^m5w9 zx~WRrM1`eB3}DN}s?#)C157pPb7iy;x3PiZsJygXpK!K}BCsG$Tl+G z@D2+nZh}_Cc97i?eqt9+yIALgbTBk|wk6LET9=iCc_!&)YB zzKf`j{`=-*G4ru|#T0XG9to>Hwb>K(d)^7$2}Jh};q*OqvtTm=$jSEI3kLKOrLqSFSKUrepx3HB?epP zf-Mtt_>q@;Cl<8Qofu_AYMxD==AOV)VLd@&?vkmwF!_QJRKR(}aDX6R^(Z6E%pa!L zOCg5zdi5z@m@xLh$+UD4ar+eHm$W$NF$U8%TuX~8UNKiPza_8T%&`o}5JvxRK_VJ{ z9SmupTQ8*2!)&>bs@#%$4 z2+S$apQ;eer4XCQsj}r_6={C#*Elp;X)yOCZtK@Nv{FkGXo*bm5U#X##SRk8vbCVg zXhF9PjXr}$zYR@J22IX3G`Sfxx!cefGH49j(Bx&%{PNjm{#8wKmKs`v;;wWINvQihY)mf@BwOO4e@H8{+Y;d7gM3fV@# zg{{t7qDgAmU$)KLZq9hy<=fD-WYAPND%d7rjNrz1SlC9vkB*Vt{M?Q*)-F78YsM2- zqU>&A9I3*J0>xqA z)k11FFspz)3oKv2dVpo;J-z8?5-9sXX%w)2U?z4zsQn1=0wMJ%utEVl2CPKDoY zV8?-31dLiN6R;D*GHKrVYj!iN(xG+A*pm(m;3-EL;2^tCd}?-xO0&aMl08jN$&Szy zwCh2|I%)vVjMYmcHX;qNlz`v#PBzk%k(@-O{SWmx95O&~BS5pG7;ulH60prt1=!-K z0Bm-Y13v%ckptCPM?T=F!vuKFQQ*WVItm?F*I1h`*i{2EQu^r(%qBMh+)RFaeQ1n~ ztit6;IlFetMulUya3X-FM}1Ur>H+4NfM>W7#s+P5SL)K`s=ItMR!f(+6&XypE#Rh0t|g$4ci%cr`0T67agzH_>?QDN>1HY~Xb%JqR{t zj)QdA2C0wxpLh&GA(?9hHh_#StN+9A1gw(*FCEJXe)zBOr44If9Dx$}z|TY=XoYzK z2WZ{EdDXzk$j}IddPv{c$VH{F-3R=WFk?{27M&*Lb`v0to3j(J5$GWB3N??&W`_HK zz<(sb5|}0M5&=3VDVWGPvOuqcWs1Iy^&`NhzFPF#NB_sdtKAav``l$B2m;vT3%LVV zLpXsfM*H(h8a1REq*QSPRl?8uJ``Ce&L#LzcxBJYGd+Ury$;h8v;KeJ<-n)PdY&8Y z;njl!jx!@eqMs#gf_D?0gKD6luC$e&m+e?kHrsbk^0IEhK(e4BJ#)u`2H&jfnC`MJ zD4XniCKq%&aSwk75(E_9B%pcpVLS3Dk?%yLZJS)sB9)z+#XVso9(*oOz>d@(1tzyP zaKDB=Ea+{>9Ynkw5_5qjk3i!NO?FH!7q))R<+Bkao+x4Fcz6T?=Lunf zso*=_ut}@s^OGUjNgq`Z3F7mm5)fC2m?FUeV=LkI(rbN|z#xII5V%O-8i7Ru|Czwg z2;|c^m=(kVYaHIAi&*I#sYnPo3|!%L-e5rdJe93hQ1sw3 z!cdT9kJlszgv0&`v)QCyJRP2gR_N z;afKxuYuQ3KoF`fPJv)89>@B`OLSi;xH&(cm!Z|-ejI%WzJbjh-_0v5Uew}Tho$nq z*$&4^qO|;eXxzF*!EEAL2v#goZixSnt(ev(TFcd4Ci{{Qj`OYu>hVM1s(lZfxJg;d{f#bTjD{fURUs`R8 zTlOyICFsQdjohUR2n{v1E~*lFB?A3noYKv+gTA2bX7i2ar3Ks<*cUYt{aBt1?`~7EBMCrYt!MmrSW)P4fdg3NKjuxt! z@`}ZLcnTNuf#>Fj3trzJc1C2eV%rU8!e9)yy*?WLdZZ>++7$jeQ51&HyncA8JK~C! zx8CYb7>dGkuRE6ltEO00+fsliN|xlWcP|Y^F2^c%-x^97N|$Q4P!vT=TjM#ctJQaV zVtWSOt4pWhF z#s1oHdeYw4&n1d0lKJ1QTXuh^A#(aobF9+7Dvw**UpxM3apmHXgr)p#_j27^(-G4x zU(C|DcpU8%TW%h^acpTUQWGy~#+$Iy_>AS7$F2LK)%%m_vIE5;re{j3!zVU!sGMGAHH%m-Sj4h2{Iv}OrRU1( z%S0@XDc`wNv5`-yd9r%5h#5g0zP4eaRHLk>RKyAx^R8&gu8l%UE?^q=NLX8sEV{cV zc_-_Ym568&Q(74w-6$sVB6@-(R@1a}UBa8&e=ejd7@cXO!cw45V0k$<6u>^sEecO= zsDNRt43?$F4GrO1#!#|Uyfn9b29v8UG8q}gsk@77=kJ-LBaZ0l=cCVGh`u;U=Wb_-9%(4gbdc#0jM#fOSbZo;!coAbL2)Az( z6JEj?DwifVN(nck@$i)m3*lvAr^*Sh5b;XFtI))fWuuyKE2Y=n-gRr&a#Q4Tyll@# z4JFqyhU#VQMjhchsM(_%I|;8BtJy_(15@1q-%g_1PXEg)Ki`8?u&WO7Z1K+nVCa?b z%Np7fwKlAj%irG{t!WEuZ{~&bez~DQ`jpWdsA?NtHC z;R-Sh3OhPSrb7<)1boA|h&~ldU#G>VV)afZzB)VSBRtpXygcXfC3Etf&Pgv93{kj< zKj3t7WJ>%82`lxqr+aBnFLnx_rt`X;t`I&sIgZcm2A$3ohJz{u@rD3mr$mqsBzPL& z*O|}dO4Yv43#wHmpZCggRRy0{F(g-&h^NJ w-DfKQJJayrm^!Q%Q{heBZ|I_Bv@aUlUN$6*1y4x$8|Ao0R`NMRP-y!90Y|dAGynhq literal 0 HcmV?d00001 diff --git a/agents/__pycache__/decio.cpython-313.pyc b/agents/__pycache__/decio.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d86b1fdf8f26c15696f80cbdb95f6aad79f1153 GIT binary patch literal 9584 zcmb6e0 zJC_fUiqs?*(z)mHec$=cch31;zs#VmMWh=50&o|B*77+ zjFexOaWd&G=j75`!71RaxURgWXOsX}t6I`QYw(vNd=m?h&3 z6G2zPEc)F$-$Rp3ARI;B5W~h%-^P$rKd}~Of{2WBbesu=`8Ya-d}00$S_lTi0fy6$ z4lITv5t>7*fXRoWEMnMDm}NX@BFYBCG`sr&jV_{dqdn+%-+m8WU6`G8p>h99ZgOgN zR_Y>W(NGU^qoqYO+T+qc!+g#=WukT!ri_QznLv^AVks*%K7S4>-zRPpAOHd{9u_Wl z5S)@!K&iru=G3GLN)4%oQcG%})R9_FAJRGXqF!35w9scdAQdRdMMD2QJH?Md@eWaF zYE3D>qboSNzhnHQ13eD^Ae=G&gZXDIf8QcZyePbMD|h~OuH)sD@>kl{LdRV3ZT&_{ z`Ca|zK!6C~Gf{ppo6;HShRXK|KioqP^o_~-N)W5CYl!g6N%;jhFWrPwk+=jyle-_{ zD;DMvjn?V-8UsZ*02jrB*eD9%OS8(b0S5i<{$IhZIfHvaig5p6OI**c0|_iI@9dIS zRE>LQmc1VWY0d)uqW%C~RP*5wOGkLjBSMI4!T|LY2bY%$h9j`^qODq7RRd-KnUlX5Q#cpArVW*)Ff3^>TQuMi}^jO2YUaR6rPTA3e(iL3a?ut;%Hq5y=Mt zl2nurdd--!c5Fo6jp?Y4q@%8dt)Zi-9m8}&TBmMaRDjwE-G29|YKArRP5`e+YUN;G_ank_xU2yIPjED#n2d6xDP1%u_mM$XXw<$y*@ai1#khj~uGzqC*P2TBC?iSR+k;iqzd0W*YQum}!4 zzKTjZ7{F@sE5phUPC@xJuwMsXIY?CSZ66{)V^DZU1thSXN@lwSi+N9rqhD!=?l zxz$yzMSToReHD!67#KqpjP@9qhAJ4{F)+p|82vFYrYe{^ve7SVAV{-cedPKqWK&rN z0A)P}s<{Trb_|rg2CBucIdVov4OHtfP;E6(?SAc%v^r{_P8iQClGdpjs9t~Fkx+d#P|mM$a!((eT-de#uYq}{26JGzovrU6NEc8zP!^S1 z12HTo-Q*y7mK<^^Spz^-9GnUeK@R)t$q|>_Eh9(CZ@3g}!!2!j)F-*kBzJ4eAS({_$`%rS_uO7PfvQU*IzTRP2kitfonT{^iKILqu9d018{_|y;!d< z{pKTQ*BqQ0f5k(n@Br=$IpH^xlf6KSoPydFsiue8bftC`*Zk&g;5S5c0tY9E`H3;b z2_h&fwn9X)<(!!p&63j^Wvu!ZESU(%Sn+Ss|i=ApwcZH$3 zY4Dy`MI8yAEJQeLT+~fQ*TXCgj$h&jK=4L1u>03&_d^s#MThYWV)#lBMk7AD0xluA zhO0EH4dYyTFSvcXe+y1!^b87eVJN}HWY%}z$6YS8$T2*_ZP2?P?EV<=8598bKTb!` zB@{^nxG3Uw-;ae?qb|K3jr(S%uFT9&qsfJ0-J?(7MZ?hI%&gZpMWV?m4KaiKYyy8|R(H}6oA zr81v05HZ6hPO&7W$^9eO?klr0DZP z>^Se8Aw59<$iN+64;~*Fa%yhTHzft~sEaSs?gvq{yNL!}=buA;14BJdSojishk;Wb z4Tj@2KxbWpqo}Wcv2^F3cn@zDwc82+er=H zc!ons6CI01N`&06@liI}Tn_&aB5D^z)X9cg72;Hi25%z17UjZ8iH&4E>?{QI@qwi` zVob9B>eABUfR70>Fi%pmJg^jwM;K9=0KI}^Q!WZE4Xg~Kgdf0RgOJG#YD3hnGdvIR zJCE<>Jt`i(!?64_tXQ22QHgmJHERsL%5c1>!I}|L6wmfBrY5i!g=h=9OC-3f(4ADg ziuBwp)Sf9Zlq!L#L!_pUL_;O=oEDELvA`jxRLW8%uwkh>uB8msJKw{HN>Yz3Y+*?= ziiVm^0aZ{wGh$tp_?<=$+h8e+Au1pPQd%>|+)aS;=0#;J3OXEn6=EG9r{f8p3PAot z;$Kw3P&g*)VStPCuZQDnqAC)lS9!6inDB5((xSi!1nw{#w}hv{@to6I4F0ucb0%u4 zEtX`dBqJp%<6xge70ejk5EYT|T~Prl3+GHV zOiJ)bf)|xIGsBUX3CszPJr;D-Q%pS6N%nP>p(2rWv3U}wZoD{+hGxh*kX2UQF!5|aQl_q7?rcUn$i}` z&71Cr?!37tYwk(2+bcQq`IM&6Xx%*V@I=0`JKNZuUe4UhH9nV87pyIt)I%z7?aNyG zGHu&j&gx0&3#O(`<3nTKbTVr?nV!zPkTbbcs(m$Kvc3D}{WrHRWaK$xe@a;}**5DQ z)@{vYhH@rXN>ymEzI*5Xoh?_UE7vfPQk2{MX)f1r8rsI@TKp1xd3;}~w8{3hgk1lQ zexFd(nPEy;cHhIktwlCx3eNqpQN$++H9z;uD5RfqX@v0(mNwr9#45 zTu9u@QEv#ZB=-r~n=*UB-u_tkNGEiSY?C?rdBNt{>3cHz&*%T~{N8Qh=F7Q>SA=m2 zKwp)$71~Zbo_;hfbYsxA^FpgnBQHyzb4rF0J32~Fu0)KZ7!It zo4pTv1;;=pkTVYnjlF9Q+kb6u``>+&OxIYb zH^II9AHE0f>E5}C6LaT?k8O^*LE_`H7oq-(LHXQ>;uk|ITt6qDyPybw=C3>&s7a}( zB&3s$OFj8Yp+f}=D)_rbIej3dh^hxm^&|1BGV3f(5q(v>!zrS#iU%JWDtzJ;(O1RO zR;>?GM5Im%L5&c|s7SpBGO9Fupdjm5HCdkk*~RXW23EK59zZr!K^i?^VZQ)r0?4<4 z7aW%!dX~*)Zm})I4-NVEnt4iTonZw~xmn@tBtThMHBiNSdg$>hNK2V+iQ6VAIb`*# zAdS%MSCTd(F{}oPXDchf6zzY6qWuuXmSZStYq)o?I=|X4^XvQ?KcvIRR+k#G@NM%& zi#Q03OCcTS#15G;5*v6(S9cfK{W9UQ%RhGqUhxQF=Rd(Sw($AqXW(ajzz0{l>|1F zJ>`#TN4qRs7r-PCL7IL~w7ViX+?Q&39aU>t4!k9HM~bPBL_i{6js8#=QMo#`{%vRuOuHvHASJsESZ;Y>;a*1X>Q z;6|>lHLXam{8;ui42pojz|-BxH3sF5WP=M zKCwOt?vcU^Knu@GsusXFL5BPY zMM$a-)cP?HnlFXWlDe|$JBEgytSdv11X-^HE|25}=T*Pz>J4N=nP&OzSA1E%wJc{J z-p4~vd5ko=bgU9gfCnOQZs;chU}+Ex6`af+29sA1HlEfjQ-p)|AvC#{*cE z9>eKAPBIyig2Nh_Tfl{+xv&Ls>NspiMLoR^9|_|)U2_;;LOoD*Ivn6axVIjYFq1mS zU0^j285>zoQVT6|)`Pl}$}_IPpwwPk#BBwPJX^$*OOdTg3J;wcE{eNe#|7SkqPoQw zLR7HP*F{~Jk3wn!Qh}haL9W0l#9|bLb%{t^R2Sbx9ju0^JC=xx_2p=pg4mSDS^1*& zEoxo`mte()Ds2nx9{~*SFKgJmwI^%s$ys|-`rn>#K6GT_dG|!tJ&|)yrPNQ4)T&RL z+qQ0IoBImrR63bOL)+0DntIf=cec=S0()VaFmf=Ec=VQ+!4}?T72c$NPWWxpI=HVwd zD%H8qOmfw@>@&Se)%2NZQ3hpD?o{c2zbvz>nu`Q5u2U^)C`fk( zfHeLvBV|RnTeyY~agl!QoTBz(afO$-?*a%m7knoa`wE#%_NiPcQ~p6u$Q(JM<{9 literal 0 HcmV?d00001 diff --git a/agents/__pycache__/papert.cpython-313.pyc b/agents/__pycache__/papert.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..301a0e4bf2bb0516313a9de9ca090cf99e08466b GIT binary patch literal 18197 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 literal 0 HcmV?d00001 diff --git a/agents/audit.py b/agents/audit.py new file mode 100644 index 0000000..c2acf6f --- /dev/null +++ b/agents/audit.py @@ -0,0 +1,173 @@ +""" +BrainSteel Fin — Audit Agent (v2) +5-Pillar Compliance Auditor +Reviews pipeline against all 5 analysis pillars. +""" +import os, json, requests +from datetime import datetime + +class AuditAgent: + def __init__(self): + self.name = "Audit" + + def audit_pipeline(self, brief_data, decio_data, papert_data): + checks = [] + + # Check 1: Brief delivered all 3 required points + briefing = brief_data.get("briefing_for_decio", "") + has_price = "BTC $" in briefing or "$" in briefing + has_change = any(x in briefing for x in ["4h:", "24h:", "+", "-"]) + has_sr = "Sup:" in briefing and "Res:" in briefing + checks.append({ + "check": "Brief 3 pontos", + "passed": has_price and has_change and has_sr, + "detail": f"price={has_price} change={has_change} sr={has_sr}" + }) + + # Check 2: Brief signal was computed correctly + brief_conf = brief_data.get("confidence", 0) + brief_signal = brief_data.get("signal", "NEUTRO") + has_rsi = brief_data.get("rsi") is not None + has_fg = brief_data.get("fear_greed") is not None + checks.append({ + "check": "Brief pillars completos", + "passed": has_rsi and has_fg, + "detail": f"RSI={brief_data.get('rsi')} F&G={brief_data.get('fear_greed')} news={brief_data.get('news_sentiment','?')}" + }) + + # Check 3: Decio respected 75% rule + decio_action = decio_data.get("action", "HOLD") + decio_conf = decio_data.get("confidence", 0) + sl = decio_data.get("stop_loss", 0) + tp = decio_data.get("take_profit", 0) + + respected_75 = True + if decio_conf < 75 and decio_action != "HOLD": + respected_75 = False + checks.append({ + "check": "Decio regra 75%", + "passed": respected_75, + "detail": f"conf={decio_conf}% action={decio_action}" + }) + + # Check 4: Decio SL/TP correct if BUY/SELL + price = brief_data.get("price", 0) + if decio_action in ("BUY", "SELL"): + sl_correct = (decio_action == "BUY" and abs(sl - price * 0.98) < price * 0.01) or \ + (decio_action == "SELL" and abs(sl - price * 1.02) < price * 0.01) + tp_correct = (decio_action == "BUY" and abs(tp - price * 1.05) < price * 0.01) or \ + (decio_action == "SELL" and abs(tp - price * 0.95) < price * 0.01) + else: + sl_correct = (sl == 0 and tp == 0) + checks.append({ + "check": "Decio SL/TP 2%/5%", + "passed": sl_correct, + "detail": f"SL=${sl} TP=${tp} price=${price}" + }) + + # Check 5: PaperT logged correctly + papert_logged = papert_data.get("log_appended", False) + papert_status = papert_data.get("status", "") + checks.append({ + "check": "PaperT executou log", + "passed": papert_logged and papert_status == "validated", + "detail": f"logged={papert_logged} status={papert_status}" + }) + + # Check 6: Portfolio updated if BUY/SELL + portfolio = papert_data.get("portfolio", {}) + if decio_action in ("BUY", "SELL"): + pf_updated = portfolio.get("total_trades", 0) > 0 + else: + pf_updated = True # No trade = no portfolio change needed + checks.append({ + "check": "Portfolio atualizado", + "passed": pf_updated, + "detail": f"trades={portfolio.get('total_trades',0)} balance=${portfolio.get('current_balance','?')}" + }) + + # Score + passed_count = sum(1 for c in checks if c["passed"]) + total = len(checks) + score = round(passed_count / total * 100, 1) + + # Signal quality assessment + net = brief_data.get("net_signal", 0) + bullish = brief_data.get("bullish_signals", 0) + bearish = brief_data.get("bearish_signals", 0) + rsi = brief_data.get("rsi", 50) + + quality_notes = [] + if rsi > 70: quality_notes.append("RSI overbought — cautela") + if rsi < 30: quality_notes.append("RSI oversold — oportunidade") + if net > 2: quality_notes.append(f"Sinal bullish forte (net={net})") + if net < -2: quality_notes.append(f"Sinal bearish forte (net={net})") + if decio_action == "HOLD" and brief_data.get("confidence", 0) < 75: + quality_notes.append("HOLD correto — confiança baixa preservou capital") + + return { + "audit_id": f"AUD-{datetime.now().strftime('%Y%m%d%H%M%S')}", + "timestamp": datetime.now().isoformat(), + "checks": checks, + "compliance_score": score, + "status": "approved" if score >= 83 else "flagged" if score >= 66 else "rejected", + "passed_count": passed_count, + "total_checks": total, + "quality_notes": quality_notes, + "brief_summary": brief_data.get("summary", "")[:100], + "decio_decision": decio_action, + "decio_confidence": decio_conf, + "portfolio_balance": portfolio.get("current_balance", 0), + "portfolio_pnl": portfolio.get("total_pnl", 0), + "portfolio_pnl_pct": portfolio.get("total_pnl_pct", 0), + } + + def generate_llm_report(self, brief_data, decio_data): + if not os.getenv("PAPERT_API_KEY") and not os.getenv("OPENROUTER_API_KEY", ""): + return {"report": "OpenRouter key not available", "status": "local_only"} + try: + prompt = f"""Você é o Audit, auditor de compliance da BrainSteel Fin. +Gere um relatório de auditoria em português,分析昨日操作质量: + +Brief: {brief_data.get('summary','N/A')} +Signal: {brief_data.get('signal','?')} ({brief_data.get('confidence','?')}% conf) +RSI: {brief_data.get('rsi','?')} | F&G: {brief_data.get('fear_greed_class','?')} +Bullish signals: {brief_data.get('bullish_signals',0)} | Bearish: {brief_data.get('bearish_signals',0)} + +Decio: {decio_data.get('action','?')} conf {decio_data.get('confidence','?')}% +Justification: {decio_data.get('justification','?')[:100]} + +Responda em JSON: +{{"verdict": "APROVADO|REPROVADO|FLAG", + "risks": ["risk1","risk2"], + "recommendations": ["rec1","rec2"], + "score": 0-100, + "summary_pt": "resumo em português"}} +""" + headers = { + "Authorization": "Bearer " + (os.getenv("PAPERT_API_KEY") or os.getenv("OPENROUTER_API_KEY", "")), + "Content-Type": "application/json", + "HTTP-Referer": "https://brainsteel.fin", + "X-Title": "BrainSteel Fin" + } + r = requests.post( + "https://openrouter.ai/api/v1/chat/completions", + json={"model": "deepseek/deepseek-v4-flash:free", "messages": [{"role": "user", "content": prompt}], "max_tokens": 400}, + headers=headers, timeout=30 + ) + if r.status_code == 200: + resp_data = r.json() + content = resp_data["choices"][0]["message"]["content"] + usage = resp_data.get("usage", {}) + if usage: + try: + from agents.token_tracker import log_tokens + log_tokens("Audit", "deepseek/deepseek-v4-flash:free", usage) + except: + pass + content = content.strip("` \n") + if content.startswith("json"): content = content[4:] + return {"report": content, "status": "generated"} + except: + pass + return {"report": "Audit local", "status": "error"} \ No newline at end of file diff --git a/agents/brief.py b/agents/brief.py new file mode 100644 index 0000000..b459bd0 --- /dev/null +++ b/agents/brief.py @@ -0,0 +1,497 @@ +""" +BrainSteel Fin — Brief Agent (v2) +Market Intelligence Analyst — Super Financial Agent +5 Pillars: On-Chain | Derivatives | Sentiment | Technical | Macro +""" +import os, json, requests, re +from datetime import datetime, timedelta +from collections import defaultdict + +# ── Data Sources ───────────────────────────────────────────────────────────── +GLASSNODE_API = os.getenv("GLASSNODE_API_KEY", "") +COINGLASS_API = os.getenv("COINGLASS_API_KEY", "") +SANTIMENT_API = os.getenv("SANTIMENT_API_KEY", "") + +class BriefAgent: + def __init__(self): + self.name = "Brief" + self.openrouter_key = os.getenv("BRIEF_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) + self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" + self.model = "deepseek/deepseek-v4-flash:free" + + # ═══════════════════════════════════════════════════════ + # PILLAR 1 — ON-CHAIN ANALYSIS + # ═══════════════════════════════════════════════════════ + def _onchain_bitflyer(self): + """On-chain data via Blockchain.com public API + mempool.space.""" + result = {} + try: + # BTC UTXO set — circulating supply estimate + r1 = requests.get("https://blockchain.info/q/estimatedbtcsupply", timeout=8) + if r1.status_code == 200: + result["btc_supply"] = float(r1.text.strip()) + except: + pass + + try: + # Market cap + r2 = requests.get("https://api.blockchain.com/v3/index/c BTC/information", timeout=8) + if r2.status_code == 200: + data = r2.json() + result["market_cap"] = data.get("market_cap") + except: + pass + + try: + # Mempool congestion — fee estimation + r3 = requests.get("https://mempool.space/api/v1/fees/recommended", timeout=8) + if r3.status_code == 200: + fees = r3.json() + result["fee_fast"] = fees.get("fastestFee", 0) + result["fee_hour"] = fees.get("halfHourFee", 0) + result["fee_economy"] = fees.get("economyFee", 0) + except: + pass + + return result + + def _onchain_mempool_state(self): + """Current mempool state — blocks pending, congestion level.""" + try: + r = requests.get("https://mempool.space/api/v1/mempool-summary", timeout=8) + if r.status_code == 200: + data = r.json() + return { + "vsize_mb": round(data.get("total_vsize", 0) / 1e6, 1), + "tx_pending": data.get("tx_count", 0), + } + except: + pass + return {} + + def _arkham_trace(self): + """Arkham Intelligence — track institutional large wallets (public data).""" + # Arkham has a free public API for entity tags + try: + r = requests.get( + "https://api.arkhamintelligence.com/intelligence/labels?chain=BTC&limit=5", + timeout=8, + headers={"Accept": "application/json"} + ) + if r.status_code == 200: + data = r.json() + entities = [l.get("label", "") for l in data.get("results", [])[:5]] + return {"arkham_entities": entities} + except: + pass + return {} + + # ═══════════════════════════════════════════════════════ + # PILLAR 2 — DERIVATIVES & LIQUIDITY + # ═══════════════════════════════════════════════════════ + def _derivatives_binance(self): + """Funding rates + open interest from Binance futures (public).""" + result = {} + try: + # Funding rate for BTC perpetual + r = requests.get( + "https://fapi.binance.com/fapi/v1/premiumIndex?symbol=BTCUSDT", + timeout=8 + ) + if r.status_code == 200: + data = r.json() + result["funding_rate_pct"] = float(data.get("lastFundingRate", 0)) * 100 + result["mark_price"] = float(data.get("markPrice", 0)) + result["index_price"] = float(data.get("indexPrice", 0)) + result["next_funding_ts"] = data.get("nextFundingTime", "") + except: + pass + + try: + # Open interest + r2 = requests.get( + "https://fapi.binance.com/fapi/v1/openInterest?symbol=BTCUSDT", + timeout=8 + ) + if r2.status_code == 200: + oi = r2.json() + btc_oi = int(oi.get("openInterest", 0)) + result["open_interest_btc"] = btc_oi + result["open_interest_usd"] = btc_oi * result.get("mark_price", 0) + except: + pass + + return result + + def _coinglass_funding(self): + """CoinGlass funding rates aggregate across exchanges (public).""" + try: + r = requests.get( + "https://open-api.coinglass.com/public/v2/funding_rate_history?symbol=BTC&exchange=Binance,OKX,Bybit&type=1", + timeout=10, + headers={"Accept": "application/json"} + ) + if r.status_code == 200: + data = r.json() + records = data.get("data", {}).get("history", []) + if records: + # Latest avg funding + latest = records[-1] if records else {} + return {"avg_funding_pct": latest.get("rate", 0)} + except: + pass + return {} + + def _liquidations_map(self): + """Estimate liquidation zones via open interest concentration. + Source: Binance liquidated positions public data.""" + result = {} + try: + r = requests.get( + "https://fapi.binance.com/futures/data/globalLongShortAccountRatio?symbol=BTCUSDT&period=1h&limit=5", + timeout=8 + ) + if r.status_code == 200: + data = r.json() + if data: + latest = data[-1] + long_ratio = float(latest.get("longAccount", 0)) + short_ratio = float(latest.get("shortAccount", 0)) + result["long_ratio_pct"] = round(long_ratio / (long_ratio + short_ratio + 0.001) * 100, 1) + result["sentiment"] = "overbought" if long_ratio > 60 else "oversold" if long_ratio < 40 else "neutral" + except: + pass + return result + + # ═══════════════════════════════════════════════════════ + # PILLAR 3 — SENTIMENT & BEHAVIOR + # ═══════════════════════════════════════════════════════ + def _fear_greed_index(self): + """Alternative.me Fear & Greed Index (free).""" + try: + r = requests.get("https://api.alternative.me/fng/?limit=2", timeout=8) + if r.status_code == 200: + data = r.json() + items = data.get("data", []) + if items: + latest = items[0] + prev = items[1] if len(items) > 1 else {} + return { + "fgi_value": int(latest.get("value", 50)), + "fgi_class": latest.get("value_classification", "Neutral"), + "fgi_change": int(latest.get("change", 0)), + "fgi_prev_value": int(prev.get("value", 50)) if prev else 50, + } + except: + pass + return {} + + def _sentiment_cryptopanic(self): + """CryptoPanic — news sentiment aggregator (free).""" + try: + r = requests.get( + "https://cryptopanic.com/api/v1/posts/?auth_token=¤cies=BTC,ETH,SOL&kind=news", + timeout=10 + ) + if r.status_code == 200: + data = r.json() + posts = data.get("results", [])[:20] + pos_kw = ["bullish", "surge", "rally", "record", "growth", "high", "adoption", "buy", "soar", "peak", " ETF ", "institutional", "accumulation", "breakout"] + neg_kw = ["bearish", "drop", "crash", "sell", "risk", "warn", "hack", "ban", "regulation", "reject", "loss", "fear"] + pos = sum(1 for p in posts for t in [p.get("title", "")] if any(k.lower() in t.lower() for k in pos_kw)) + neg = sum(1 for p in posts for t in [p.get("title", "")] if any(k.lower() in t.lower() for k in neg_kw)) + return { + "news_pos": pos, + "news_neg": neg, + "news_total": len(posts), + "sentiment_label": "Predominantemente Otimista" if pos > neg + 3 else + "Predominantemente Pessimista" if neg > pos + 3 else "Incerto" + } + except: + pass + return {} + + def _social_volume(self): + """CoinGecko social stats (free tier).""" + try: + r = requests.get( + "https://api.coingecko.com/api/v3/coins/bitcoin", + params={"tickers": "false", "market_data": "true", "community_data": "true"}, + timeout=10 + ) + if r.status_code == 200: + data = r.json() + comm = data.get("community_data", {}) or {} + return { + "twitter_followers": data.get("followers", 0), + "forum_posts_24h": comm.get("forum_posts_active_24h", 0), + "reddit_subscribers": comm.get("reddit_subscribers", 0), + "telegram_channel_users": comm.get("telegram_channel_user_count", 0), + } + except: + pass + return {} + + # ═══════════════════════════════════════════════════════ + # PILLAR 4 — TECHNICAL ANALYSIS + # ═══════════════════════════════════════════════════════ + def _technical_binance(self): + """Candle-based support/resistance + RSI + MACD approximation.""" + result = {} + try: + # 1h candles for 200 periods + r = requests.get( + "https://api.binance.com/api/v3/klines", + params={"symbol": "BTCUSDT", "interval": "1h", "limit": 200}, + timeout=10 + ) + if r.status_code == 200: + candles = r.json() + closes = [float(c[4]) for c in candles] + highs = [float(c[2]) for c in candles] + lows = [float(c[3]) for c in candles] + vols = [float(c[5]) for c in candles] + + # RSI(14) approximation + deltas = [closes[i] - closes[i-1] for i in range(1, len(closes))] + gains = [d for d in deltas[-14:] if d > 0] + losses = [-d for d in deltas[-14:] if d < 0] + avg_gain = sum(gains) / 14 if gains else 0.001 + avg_loss = sum(losses) / 14 if losses else 0.001 + rs = avg_gain / avg_loss if avg_loss else 99 + rsi = round(100 - (100 / (1 + rs)), 1) + + # MACD (12,26,9) + ema12 = sum(closes[-12:]) / 12 + ema26 = sum(closes[-26:]) / 26 + macd_line = ema12 - ema26 + signal = macd_line * 0.2 # simplified signal + + # Support/resistance via volume profile + vol_index = vols.index(max(vols)) + support = round(min(lows[:vol_index]) if vol_index > 0 else min(lows[-50:]), 2) + resistance = round(max(highs[vol_index:]) if vol_index < len(highs) - 1 else max(highs[-50:]), 2) + + # 4h change + change_4h = round((closes[-1] / closes[-5] - 1) * 100, 2) if len(closes) >= 5 else 0 + change_24h = round((closes[-1] / closes[-25] - 1) * 100, 2) if len(closes) >= 25 else 0 + change_7d = round((closes[-1] / closes[-169] - 1) * 100, 2) if len(closes) >= 169 else 0 + + result.update({ + "price": closes[-1], + "rsi": rsi, + "macd": round(macd_line, 2), + "macd_signal": round(signal, 2), + "macd_histogram": round(macd_line - signal, 2), + "support": support, + "resistance": resistance, + "change_4h": change_4h, + "change_24h": change_24h, + "change_7d": change_7d, + "volume_avg_ratio": round(sum(vols[-24:]) / sum(vols[-168:]) * 100, 1) if sum(vols[-168:]) > 0 else 100, + }) + except Exception as e: + pass + return result + + # ═══════════════════════════════════════════════════════ + # PILLAR 5 — MACROECONOMIC + # ═══════════════════════════════════════════════════════ + def _macro_etf_flows(self): + """ETF flow estimates via Farside Investors (public).""" + try: + r = requests.get("https://farside.io/bitcoin-etf-flow-all-data", timeout=10) + if r.status_code == 200: + text = r.text.lower() + # Look for latest date pattern and inflow/outflow indicator + import re + # Simple pattern: find most recent date and flow amount + matches = re.findall(r'(\d{4}-\d{2}-\d{2})[^$]*?(-?\$?[\d,]+)\s*(million|billion)?\s*(inflow|outflow)', text) + if matches: + latest = matches[-1] + return {"etf_date": latest[0], "etf_flow_raw": latest[1] + " " + (latest[2] or "")} + except: + pass + return {} + + def _macro_btc_dominance(self): + """BTC Dominance (Cap) — tracks altcoin season vs BTC season.""" + try: + r = requests.get( + "https://api.coingecko.com/api/v3/global", + timeout=10 + ) + if r.status_code == 200: + data = r.json() + global_data = data.get("data", {}) + btc_d = global_data.get("market_cap_percentage", {}).get("btc", 0) + return { + "btc_dominance_pct": btc_d, + "active_cryptos": global_data.get("active_cryptocurrencies", 0), + } + except: + pass + return {} + + def _macro_correlations(self): + """DXY Dollar Index via public API.""" + try: + r = requests.get( + "https://api.coingecko.com/api/v3/coins/us-dtate", + params={"id": "us-dtate"}, + timeout=8 + ) + # Fallback: approximate via BTC correlation with NASDAQ + # Use a simple proxy from Binance DXY futures + pass + except: + pass + return {} + + # ═══════════════════════════════════════════════════════ + # MAIN RUN + # ═══════════════════════════════════════════════════════ + def run(self): + all_data = {} + + # Gather all pillars + pillars = { + "On-Chain": self._onchain_bitflyer, + "Mempool": self._onchain_mempool_state, + "Arkham": self._arkham_trace, + "Derivatives": self._derivatives_binance, + "Coinglass": self._coinglass_funding, + "Liquidations": self._liquidations_map, + "FearGreed": self._fear_greed_index, + "Sentiment": self._sentiment_cryptopanic, + "Social": self._social_volume, + "Technical": self._technical_binance, + "MacroETF": self._macro_etf_flows, + "Dominance": self._macro_btc_dominance, + } + + for name, fn in pillars.items(): + try: + result = fn() + if result: + all_data[name] = result + except Exception as e: + all_data[name] = {"error": str(e)} + + tech = all_data.get("Technical", {}) + price = tech.get("price", 0) + if not price: + return {"summary": "Erro: não foi possível obter dados de mercado", "signal": "ERROR", "confidence": 0} + + # Build briefing string for Decio + fgi = all_data.get("FearGreed", {}) + sentiment_data = all_data.get("Sentiment", {}) + deriv = all_data.get("Derivatives", {}) + liq = all_data.get("Liquidations", {}) + onchain = all_data.get("On-Chain", {}) + btc_d = all_data.get("Dominance", {}) + + # Overall sentiment combining Fear&Greed + news + fg_class = fgi.get("fgi_class", "Neutral") + news_sent = sentiment_data.get("sentiment_label", "Incerto") + funding = deriv.get("funding_rate_pct", 0) + long_ratio = liq.get("long_ratio_pct", 50) + btc_dom = btc_d.get("btc_dominance_pct", 0) + rsi = tech.get("rsi", 50) + macd_h = tech.get("macd_histogram", 0) + + # Signal detection + bullish_signals = 0 + bearish_signals = 0 + + # RSI + if rsi < 30: bullish_signals += 1 + elif rsi > 70: bearish_signals += 1 + + # Funding rate + if funding > 0.01: bearish_signals += 1 # high funding = long squeeze risk + elif funding < -0.01: bullish_signals += 1 + + # Long ratio + if long_ratio > 60: bearish_signals += 1 # over-leveraged longs + elif long_ratio < 40: bullish_signals += 1 + + # Fear&Greed + if fg_class in ["Extreme Fear", "Fear"]: bullish_signals += 1 + elif fg_class in ["Extreme Greed", "Greed"]: bearish_signals += 1 + + # MACD histogram + if macd_h > 0: bullish_signals += 1 + elif macd_h < 0: bearish_signals += 1 + + # BTC dominance (high = BTC season, alt risk) + if btc_dom > 55: bullish_signals += 1 + elif btc_dom < 45: bearish_signals += 1 + + # Determine signal and confidence + net = bullish_signals - bearish_signals + if net >= 3: + signal = "ALTA" + confidence = min(50 + net * 8, 90) + elif net <= -3: + signal = "BAIXA" + confidence = min(50 + abs(net) * 8, 90) + else: + signal = "NEUTRO" + confidence = 40 + abs(net) * 5 + + # Change-based confirmation + change_4h = tech.get("change_4h", 0) + change_24h = tech.get("change_24h", 0) + if change_24h > 5 and signal == "NEUTRO": + signal = "ALTA"; confidence = min(confidence + 10, 85) + elif change_24h < -5 and signal == "NEUTRO": + signal = "BAIXA"; confidence = min(confidence + 10, 85) + + # 3-point briefing for Decio + support = tech.get("support", 0) + resistance = tech.get("resistance", 0) + sup_str = f"${support:,.0f}" if support else "?" + res_str = f"${resistance:,.0f}" if resistance else "?" + + briefing = ( + f"BTC ${price:,.0f} | " + f"4h: {change_4h:+.1f}% | 24h: {change_24h:+.1f}% | " + f"RSI: {rsi} | MACD hist: {macd_h:+.2f} | " + f"Sup: {sup_str} | Res: {res_str} | " + f"F&G: {fg_class} ({fgi.get('fgi_value', 0)}) | " + f"News: {news_sent} | " + f"Funding: {funding:+.4f}% | " + f"LongRatio: {long_ratio}% | " + f"BTC Dom: {btc_dom}% | " + f"Bullish={bullish_signals} Bearish={bearish_signals} | " + f"Sinal: {signal} ({confidence}% conf)" + ) + + result = { + "price": price, + "signal": signal, + "confidence": confidence, + "summary": f"BTC ${price:,.0f} | {signal} | conf {confidence}% | RSI {rsi} | F&G {fg_class}", + "briefing_for_decio": briefing, + "timestamp": datetime.now().isoformat(), + # Full data for Audit + "_all_pillars": all_data, + # Key metrics for display + "rsi": rsi, + "macd_histogram": macd_h, + "support": support, + "resistance": resistance, + "change_24h": change_24h, + "change_4h": change_4h, + "fear_greed": fgi.get("fgi_value", 0), + "fear_greed_class": fg_class, + "funding_rate": funding, + "long_ratio": long_ratio, + "news_sentiment": news_sent, + "btc_dominance": btc_dom, + "bullish_signals": bullish_signals, + "bearish_signals": bearish_signals, + "net_signal": net, + } + return result \ No newline at end of file diff --git a/agents/decio.py b/agents/decio.py new file mode 100644 index 0000000..e8acc16 --- /dev/null +++ b/agents/decio.py @@ -0,0 +1,182 @@ +""" +BrainSteel Fin — Decio Agent (v2) +Chief Strategist & Risk Officer +5-Pillar decision engine: Confiança < 75% → HOLD. BUY/SELL → SL 2% / TP 5%. +""" +import os, json, requests +from datetime import datetime + +class DecioAgent: + def __init__(self, brief_data): + self.name = "Decio" + self.brief_data = brief_data + self.openrouter_key = os.getenv("DECIO_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) + self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" + self.model = "deepseek/deepseek-v4-flash:free" + + # ── CORE RULE: confidence < 75% → always HOLD ───────────────────────── + def _risk_filter(self, confidence, signal): + """Décio is a mathematician: no confidence ≥75%, no operation.""" + return confidence < 75 or signal not in ("ALTA", "BAIXA") + + # ── Stop Loss / Take Profit ──────────────────────────────────────────────── + def _calc_levels(self, price, action): + if action == "BUY": + return round(price * 0.98, 2), round(price * 1.05, 2) + elif action == "SELL": + return round(price * 1.02, 2), round(price * 0.95, 2) + return 0, 0 + + # ── LLM Decision (with 5-pillar context) ───────────────────────────────── + def _decide_llm(self): + if not self.openrouter_key: + return None + + briefing = self.brief_data.get("briefing_for_decio", self.brief_data.get("summary", "")) + pillars = self.brief_data.get("_all_pillars", {}) + signal = self.brief_data.get("signal", "NEUTRO") + confidence = self.brief_data.get("confidence", 50) + price = self.brief_data.get("price", 0) + + # Build rich context for LLM + tech = pillars.get("Technical", {}) + deriv = pillars.get("Derivatives", {}) + liq = pillars.get("Liquidations", {}) + fgi = pillars.get("FearGreed", {}) + sent = pillars.get("Sentiment", {}) + dom = pillars.get("Dominance", {}) + + context = f"""Você é o Décio, estrategista soberano da BrainSteel Fin. +Analítico, frio, matemático. Preservação de capital > lucro rápido. + + BRIEFING DO BRIEF: +{briefing} + + PILARES DE ANÁLISE DO BRIEF: + Technical: RSI={tech.get('rsi','?')} | MACD hist={tech.get('macd_histogram','?')} | Var 24h={tech.get('change_24h','?')}% + Derivatives: Funding={deriv.get('funding_rate_pct','?')}%. Mark={deriv.get('mark_price','?')} + Liquidations: Long Ratio={liq.get('long_ratio_pct','?')}%. Sentiment={liq.get('sentiment','?')} + Fear&Greed: Index={fgi.get('fgi_value','?')} ({fgi.get('fgi_class','?')}) + Sentiment: {sent.get('sentiment_label','?')} + BTC Dominance: {dom.get('btc_dominance_pct','?')}% + Bullish signals: {self.brief_data.get('bullish_signals',0)} | Bearish: {self.brief_data.get('bearish_signals',0)} + + REGRAS OPERACIONAIS: + Confiança < 75% → decisão SEMPRE HOLD (nunca opera) + Confiança ≥ 75% + sinal=ALTA → BUY + Confiança ≥ 75% + sinal=BAIXA → SELL + BUY/SELL → Stop Loss = preço × 0.98 (-2%) | Take Profit = preço × 1.05 (+5%) + HOLD → stop_loss=0, take_profit=0 + + Responda EXCLUSIVAMENTE com JSON válido (sem texto fora do JSON): +{{"decision": "BUY|SELL|HOLD", "confidence": N, "justification": "frase curta", "stop_loss": N, "take_profit": N}}""" + + try: + headers = { + "Authorization": f"Bearer {self.openrouter_key}", + "Content-Type": "application/json", + "HTTP-Referer": "https://brainsteel.fin", + "X-Title": "BrainSteel Fin" + } + payload = { + "model": self.model, + "messages": [{"role": "user", "content": context}], + "max_tokens": 400 + } + r = requests.post(self.openrouter_url, json=payload, headers=headers, timeout=30) + if r.status_code == 200: + resp_data = r.json() + content = resp_data["choices"][0]["message"]["content"].strip() + content = content.strip("` \n") + if content.startswith("json"): + content = content[4:] + data = json.loads(content) + # Capture token usage + usage = resp_data.get("usage", {}) + if usage: + try: + from agents.token_tracker import log_tokens + log_tokens("Decio", self.model, usage) + except: + pass + # Apply risk filter + if self._risk_filter(data.get("confidence", 0), signal): + data["decision"] = "HOLD" + data["justification"] = "Confiança < 75% — preservando capital" + data["stop_loss"] = 0 + data["take_profit"] = 0 + return data + except: + pass + return None + + # ── Local decision (fallback) ──────────────────────────────────────────── + def _local_decision(self): + confidence = self.brief_data.get("confidence", 50) + signal = self.brief_data.get("signal", "NEUTRO") + price = self.brief_data.get("price", 0) + net = self.brief_data.get("net_signal", 0) + rsi = self.brief_data.get("rsi", 50) + + # Overbought/oversold confirmation + if signal == "ALTA" and rsi > 75: + # Extremely overbought — skip BUY even if signal says ALTA + decision = "HOLD" + sl, tp = 0, 0 + justification = "ALTA sinal cancelada — RSI overbought (75+)" + elif signal == "BAIXA" and rsi < 25: + decision = "HOLD" + sl, tp = 0, 0 + justification = "BAIXA sinal cancelada — RSI oversold (25-)" + elif self._risk_filter(confidence, signal): + decision = "HOLD" + sl, tp = 0, 0 + justification = f"Confiança {confidence}% < 75% — preservando capital" + elif signal == "ALTA": + decision = "BUY" + sl, tp = self._calc_levels(price, "BUY") + justification = f"Sinal ALTA conf {confidence}%, RSI {rsi}, net {net:+d}" + elif signal == "BAIXA": + decision = "SELL" + sl, tp = self._calc_levels(price, "SELL") + justification = f"Sinal BAIXA conf {confidence}%, RSI {rsi}, net {net:+d}" + else: + decision = "HOLD" + sl, tp = 0, 0 + justification = "Sinal neutro — aguardando clareza" + + return { + "decision": decision, + "confidence": confidence, + "justification": justification, + "stop_loss": sl, + "take_profit": tp, + "signal": signal + } + + # ── Main run ─────────────────────────────────────────────────────────── + def run(self): + llm_result = self._decide_llm() + result = llm_result if llm_result else self._local_decision() + + price = self.brief_data.get("price", 0) + output = { + "action": result["decision"], + "amount_pct": 25 if result["decision"] in ("BUY", "SELL") else 0, + "stop_loss": result.get("stop_loss", 0), + "take_profit": result.get("take_profit", 0), + "confidence": result.get("confidence", 50), + "justification": result.get("justification", "")[:200], + "signal": result.get("signal", self.brief_data.get("signal", "NEUTRO")), + "price": price, + "timestamp": datetime.now().isoformat() + } + + decision_str = f"{output['action']} | Conf: {output['confidence']}%" + if output["action"] in ("BUY", "SELL"): + decision_str += f" | SL: ${output['stop_loss']:,.0f} | TP: ${output['take_profit']:,.0f}" + decision_str += f" | {output['justification'][:80]}" + + output["decision"] = decision_str + output["agent"] = "Decio" + return output \ No newline at end of file diff --git a/agents/papert.py b/agents/papert.py new file mode 100644 index 0000000..d76f089 --- /dev/null +++ b/agents/papert.py @@ -0,0 +1,385 @@ +""" +BrainSteel Fin — PaperT Agent + Portfolio Simulator +Simulation Execution Officer + Virtual Portfolio Tracker +Saldo virtual: $10.000 USDT | tracking de P&L diário | gráfico de evolução +""" +import os, json, requests, sqlite3 +from datetime import datetime, date +from pathlib import Path + +# ── Portfolio Config ────────────────────────────────────────────────────────── +INITIAL_BALANCE = 200.0 # USDT virtual para simulação ( BrainSteel Fin ) +DATA_DIR = Path("/app/data") +PORTFOLIO_DB = DATA_DIR / "portfolio.db" + +class Portfolio: + """Portfolio simulation — tracking de saldo, trades e P&L.""" + + def __init__(self): + self._init_db() + + def _init_db(self): + DATA_DIR.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + c.execute(""" + CREATE TABLE IF NOT EXISTS portfolio ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT, + balance REAL, + btc_held REAL DEFAULT 0, + unrealized_pnl REAL DEFAULT 0, + total_trades INTEGER DEFAULT 0, + wins INTEGER DEFAULT 0, + losses INTEGER DEFAULT 0, + created_at TEXT, + UNIQUE(date) + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT, + action TEXT, + entry_price REAL, + amount_usdt REAL, + btc_amount REAL, + exit_price REAL, + pnl_usdt REAL, + pnl_pct REAL, + stop_loss REAL, + take_profit REAL, + fee_usdt REAL, + exit_reason TEXT, + result TEXT, + created_at TEXT + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS daily_snapshot ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT UNIQUE, + balance REAL, + btc_price REAL, + btc_held REAL, + open_trades INTEGER DEFAULT 0, + closed_trades INTEGER DEFAULT 0, + day_pnl REAL DEFAULT 0, + total_pnl REAL DEFAULT 0, + win_rate REAL DEFAULT 0, + created_at TEXT + ) + """) + # Ensure today's row exists + today = date.today().isoformat() + row = c.execute("SELECT balance FROM portfolio WHERE date=?", (today,)).fetchone() + if not row: + # Seed from last known balance or initial + last = c.execute("SELECT balance, btc_held FROM portfolio ORDER BY date DESC LIMIT 1").fetchone() + balance = last[0] if last else INITIAL_BALANCE + btc_held = last[1] if last else 0.0 + c.execute("INSERT INTO portfolio (date,balance,btc_held,created_at) VALUES (?,?,?,?)", + (today, balance, btc_held, datetime.now().isoformat())) + conn.commit() + conn.close() + + @property + def balance(self): + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + today = date.today().isoformat() + row = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone() + conn.close() + return {"balance": row[0] if row else INITIAL_BALANCE, "btc_held": row[1] if row else 0.0} + + def buy(self, price, amount_pct=25, stop_loss=0, take_profit=0): + """Executa BUY simulado.""" + return self._execute_trade("BUY", price, amount_pct, stop_loss, take_profit) + + def sell(self, price, amount_pct=25, stop_loss=0, take_profit=0): + """Executa SELL simulado (fecha posição).""" + return self._execute_trade("SELL", price, amount_pct, stop_loss, take_profit) + + def _execute_trade(self, action, price, amount_pct, stop_loss, take_profit): + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + today = date.today().isoformat() + now = datetime.now().isoformat() + + current = self.balance + balance = current["balance"] + btc_held = current["btc_held"] + fee_rate = 0.001 # 0.1% + + result = {} + pnl = 0.0 + pnl_pct = 0.0 + exit_reason = "" + btc_amount = 0.0 + amount_usdt = 0.0 + + if action == "BUY": + allocation = balance * (amount_pct / 100) + fee = allocation * fee_rate + net_cost = allocation - fee + btc_amount = net_cost / price + new_balance = balance - allocation + new_btc_held = btc_held + btc_amount + amount_usdt = allocation + exit_reason = "entry" + result_str = f"🟢 BUY | ${allocation:.2f} alocado | {btc_amount:.6f} BTC @ ${price:,.0f} | Fee: ${fee:.2f}" + + elif action == "SELL" and btc_held > 0: + # Sell portion of BTC held + sell_pct = amount_pct / 100 + btc_to_sell = btc_held * sell_pct + gross = btc_to_sell * price + fee = gross * fee_rate + net_proceeds = gross - fee + amount_usdt = gross + pnl = net_proceeds - (btc_to_sell * price) # simplified + pnl_pct = (pnl / (btc_to_sell * price)) * 100 if btc_to_sell > 0 else 0 + + new_balance = balance + net_proceeds + new_btc_held = btc_held - btc_to_sell + btc_amount = btc_to_sell + + # Determine exit reason + if stop_loss and price <= stop_loss: + exit_reason = "stop_loss_hit" + elif take_profit and price >= take_profit: + exit_reason = "take_profit_hit" + else: + exit_reason = "decio_signal" + + result_str = f"🔴 SELL | {btc_to_sell:.6f} BTC @ ${price:,.0f} | Net: ${net_proceeds:.2f} | Fee: ${fee:.2f}" + else: + new_balance = balance + new_btc_held = btc_held + result_str = f"⚪ SELL ignorado | BTC held: {btc_held:.6f}" + + # Record trade if it happened + if action in ("BUY", "SELL") and (action == "BUY" or btc_held > 0): + is_win = pnl > 0 if action == "SELL" and btc_held > 0 else None + c.execute(""" + INSERT INTO trades (date,action,entry_price,amount_usdt,btc_amount,exit_price,pnl_usdt,pnl_pct,stop_loss,take_profit,fee_usdt,exit_reason,result,created_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + """, (today, action, price, amount_usdt, btc_amount, + price if action == "SELL" else 0, + pnl, pnl_pct, stop_loss, take_profit, + amount_usdt * fee_rate if action == "BUY" else amount_usdt * fee_rate if action == "SELL" else 0, + exit_reason, "WIN" if is_win else ("LOSS" if is_win is False else "PENDING"), + now)) + + # Update daily stats + c.execute("UPDATE portfolio SET balance=?, btc_held=?, total_trades=total_trades+1 WHERE date=?", + (new_balance, new_btc_held, today)) + if is_win is True: + c.execute("UPDATE portfolio SET wins=wins+1 WHERE date=?", (today,)) + elif is_win is False: + c.execute("UPDATE portfolio SET losses=losses+1 WHERE date=?", (today,)) + + conn.commit() + conn.close() + + return { + "action": action, + "balance": new_balance if action != "SELL" or btc_held > 0 else balance, + "btc_held": new_btc_held, + "pnl": pnl, + "result_str": result_str + } + + def get_stats(self): + """Returns aggregate stats for dashboard.""" + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + + # All trades + trades = c.execute("SELECT action,pnl_usdt,pnl_pct,result,date FROM trades ORDER BY created_at DESC").fetchall() + + total_trades = len(trades) + wins = sum(1 for t in trades if t[3] == "WIN") + losses = sum(1 for t in trades if t[3] == "LOSS") + + # Current balance + today = date.today().isoformat() + cur = c.execute("SELECT balance, btc_held FROM portfolio WHERE date=?", (today,)).fetchone() + current_balance = cur[0] if cur else INITIAL_BALANCE + btc_held = cur[1] if cur else 0.0 + + # Portfolio value in USD (btc_held at current price) + try: + r = requests.get("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT", timeout=5) + btc_price = float(r.json()["price"]) if r.status_code == 200 else 0 + except: + btc_price = 0 + total_value = current_balance + (btc_held * btc_price) + + # Daily history for chart + days = c.execute("SELECT date,balance FROM portfolio ORDER BY date ASC").fetchall() + + conn.close() + + # Calculate metrics + total_pnl = total_value - INITIAL_BALANCE + total_pnl_pct = (total_pnl / INITIAL_BALANCE) * 100 + win_rate = (wins / total_trades * 100) if total_trades > 0 else 0 + + # Drawdown + peak = INITIAL_BALANCE + max_drawdown = 0 + for _, bal in days: + if bal > peak: + peak = bal + dd = (peak - bal) / peak * 100 + if dd > max_drawdown: + max_drawdown = dd + + return { + "initial_balance": INITIAL_BALANCE, + "current_balance": round(current_balance, 2), + "btc_held": round(btc_held, 8), + "btc_price": btc_price, + "total_value": round(total_value, 2), + "total_pnl": round(total_pnl, 2), + "total_pnl_pct": round(total_pnl_pct, 2), + "total_trades": total_trades, + "wins": wins, + "losses": losses, + "win_rate": round(win_rate, 1), + "max_drawdown": round(max_drawdown, 2), + "chart_data": [(d, round(b, 2)) for d, b in days], + "btc_value_usdt": round(btc_held * btc_price, 2) + } + + def snapshot(self): + """Save daily snapshot for chart history.""" + conn = sqlite3.connect(str(PORTFOLIO_DB)) + c = conn.cursor() + today = date.today().isoformat() + stats = self.get_stats() + + # Get closed trades today + today_trades = c.execute("SELECT COUNT(*),SUM(pnl_usdt) FROM trades WHERE date=? AND result IN ('WIN','LOSS')", (today,)).fetchone() + + c.execute(""" + INSERT OR REPLACE INTO daily_snapshot + (date,balance,btc_price,btc_held,open_trades,closed_trades,day_pnl,total_pnl,win_rate,created_at) + VALUES (?,?,?,?,?,?,?,?,?,?) + """, ( + today, + stats["current_balance"], + stats["btc_price"], + stats["btc_held"], + 0, # open_trades + today_trades[0] if today_trades else 0, + today_trades[1] if today_trades and today_trades[1] else 0, + stats["total_pnl"], + stats["win_rate"], + datetime.now().isoformat() + )) + conn.commit() + conn.close() + + +class PapertAgent: + """Agent executor que também atualiza portfolio simulado.""" + + def __init__(self, decio_data): + self.name = "PaperT" + self.decio_data = decio_data + self.portfolio = Portfolio() + self.openrouter_key = os.getenv("PAPERT_API_KEY", os.getenv("OPENROUTER_API_KEY", "")) + self.openrouter_url = "https://openrouter.ai/api/v1/chat/completions" + self.model = "deepseek/deepseek-v4-flash:free" + + def _validate_order(self): + action = self.decio_data.get("action", self.decio_data.get("decision", "HOLD")) + if isinstance(action, str): + for w in ["BUY", "SELL", "HOLD"]: + if w in action.upper(): + action = w + break + return True, action + + def _format_log_entry(self, action, entry_price, stop_loss, take_profit): + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + fee = round(entry_price * 0.001, 2) + entry = f"[{now}] BTC/USDT | {action} | Entry: ${entry_price:,.2f} | Fee: ${fee:.2f} | SL: ${stop_loss:,.2f} | TP: ${take_profit:,.2f}" + return entry + + def _write_log(self, log_line): + for path in ["/data/trading_history.log", "/app/data/trading_history.log"]: + try: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "a") as f: + f.write(log_line + "\n") + return True + except: + pass + return False + + def _current_price(self): + try: + r = requests.get("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT", timeout=10) + if r.status_code == 200: + return float(r.json()["price"]) + except: + return self.decio_data.get("price", 0) + + def run(self): + valid, action = self._validate_order() + entry_price = self._current_price() + stop_loss = self.decio_data.get("stop_loss", 0) or 0 + take_profit = self.decio_data.get("take_profit", 0) or 0 + amount_pct = self.decio_data.get("amount_pct", 0) or 25 + + # Execute portfolio trade + portfolio_result = self.portfolio._execute_trade(action, entry_price, amount_pct, stop_loss, take_profit) + + # Log entry + log_entry = self._format_log_entry(action, entry_price, stop_loss, take_profit) + logged = self._write_log(log_entry) + + # Save daily snapshot + try: + self.portfolio.snapshot() + except: + pass + + # Build result + stats = self.portfolio.get_stats() + fee = round(entry_price * 0.001, 2) + + result_str = f"[EXEC] {datetime.now().strftime('%Y-%m-%dT%H:%M:%S')} | {action} | ${entry_price:,.0f}" + if action in ("BUY", "SELL"): + result_str += f" | Bal: ${stats['current_balance']:.2f} | P&L total: ${stats['total_pnl']:+.2f} ({stats['total_pnl_pct']:+.1f}%)" + + return { + "status": "validated", + "exec_log": log_entry, + "action": action, + "entry_price": entry_price, + "amount_pct": amount_pct, + "stop_loss": stop_loss, + "take_profit": take_profit, + "fee_usdt": fee, + "log_appended": logged, + "result": result_str, + # Portfolio info for dashboard + "portfolio": { + "balance": stats["current_balance"], + "total_value": stats["total_value"], + "total_pnl": stats["total_pnl"], + "total_pnl_pct": stats["total_pnl_pct"], + "total_trades": stats["total_trades"], + "wins": stats["wins"], + "losses": stats["losses"], + "win_rate": stats["win_rate"], + "btc_held": stats["btc_held"], + "btc_price": stats["btc_price"] + }, + "agent": "PaperT", + "timestamp": datetime.now().isoformat() + } \ No newline at end of file diff --git a/agents/token_tracker.py b/agents/token_tracker.py new file mode 100644 index 0000000..2f8b3ad --- /dev/null +++ b/agents/token_tracker.py @@ -0,0 +1,78 @@ +""" +BrainSteel Fin — Token Tracker +Logs LLM token usage per agent per call. +""" +import sqlite3, os +from datetime import datetime, timedelta + +COSTS = { + "google/gemma-4-31b-it:free": 0.0, + "google/gemma-4-26b-a4b-it:free": 0.0, + "deepseek/deepseek-v4-flash:free": 0.0, + "anthropic/claude-sonnet-4": 3.0, + "default": 0.0, +} + +def log_tokens(agent: str, model: str, usage: dict): + prompt_t = usage.get("prompt_tokens", 0) + completion_t = usage.get("completion_tokens", 0) + total_t = usage.get("total_tokens", prompt_t + completion_t) + cost = COSTS.get(model, COSTS["default"]) * total_t / 1_000_000 + db_path = os.path.join(os.path.dirname(__file__), "..", "data", "executions.db") + conn = sqlite3.connect(db_path) + c = conn.cursor() + c.execute( + "INSERT INTO token_usage (timestamp, agent, model, prompt_tokens, completion_tokens, total_tokens, cost_usd) VALUES (?,?,?,?,?,?,?)", + (datetime.now().isoformat(), agent, model, prompt_t, completion_t, total_t, cost) + ) + conn.commit() + conn.close() + +def get_token_stats(days: int = 7) -> dict: + db_path = os.path.join(os.path.dirname(__file__), "..", "data", "executions.db") + conn = sqlite3.connect(db_path) + c = conn.cursor() + cutoff = (datetime.now() - timedelta(days=days)).isoformat() + + c.execute(""" + SELECT DATE(timestamp) as day, agent, + SUM(prompt_tokens) as p, SUM(completion_tokens) as c, SUM(total_tokens) as t + FROM token_usage + WHERE timestamp >= ? + GROUP BY day, agent + ORDER BY day, agent + """, (cutoff,)) + rows = c.fetchall() + + c.execute(""" + SELECT DATE(timestamp) as day, + SUM(prompt_tokens), SUM(completion_tokens), SUM(total_tokens), SUM(cost_usd) + FROM token_usage + WHERE timestamp >= ? + GROUP BY day ORDER BY day + """, (cutoff,)) + daily = c.fetchall() + + c.execute("SELECT SUM(total_tokens), SUM(cost_usd), COUNT(*) FROM token_usage") + total = c.fetchone() + conn.close() + + agents = ["Brief", "Decio", "PaperT", "Audit"] + days_set = sorted(set(r[0] for r in rows)) + chart_data = [] + for day in days_set: + entry = {"date": day} + for ag in agents: + row = next((r for r in rows if r[0] == day and r[1] == ag), None) + entry[ag] = row[4] if row else 0 + chart_data.append(entry) + + return { + "total_all_time": total[0] or 0, + "total_cost_usd": total[1] or 0.0, + "total_calls": total[2] or 0, + "daily_totals": [{"date": r[0], "prompt": r[1], "completion": r[2], "total": r[3], "cost": r[4]} + for r in daily], + "chart_data": chart_data, + "agents": agents, + } \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..6f933c8 --- /dev/null +++ b/app.py @@ -0,0 +1,702 @@ +""" +BrainSteel Fin — Flask Dashboard +Virtual Agent Office with anime-style agents +4-Agent BTC Trading Pipeline +""" +import os, json, sqlite3, threading, hashlib, sys +from pathlib import Path +_env_p = Path(__file__).parent / "data" / ".env" +if _env_p.exists(): + for line in _env_p.read_text().splitlines(): + line = line.strip() + if line and "=" in line and not line.startswith("#"): + k, v = line.split("=", 1) + os.environ[k.strip()] = v.strip() +from datetime import datetime, timedelta, date +from flask import Flask, render_template, jsonify, request, Response +from agents.brief import BriefAgent +from agents.decio import DecioAgent +from agents.papert import PapertAgent +from agents.audit import AuditAgent + +app = Flask(__name__) +app.secret_key = os.getenv("SECRET_KEY", "brainsteel-fin-secret-2025") + +# Paths +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DATA_DIR = os.path.join(BASE_DIR, "data") +LOG_DIR = os.path.join(BASE_DIR, "logs") +os.makedirs(DATA_DIR, exist_ok=True) +os.makedirs(LOG_DIR, exist_ok=True) + +DB_PATH = os.path.join(DATA_DIR, "executions.db") +AGENTS_DB = os.path.join(DATA_DIR, "agents.db") + +# ── Databases ───────────────────────────────────────────────────────────────── +def init_db(): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(""" + CREATE TABLE IF NOT EXISTS executions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT, + agent TEXT, + action TEXT, + result TEXT, + status TEXT + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS pipeline_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + started_at TEXT, + finished_at TEXT, + brief_result TEXT, + decio_decision TEXT, + papert_result TEXT, + audit_report TEXT, + status TEXT + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS token_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp TEXT, + agent TEXT, + model TEXT, + prompt_tokens INTEGER, + completion_tokens INTEGER, + total_tokens INTEGER, + cost_usd REAL + ) + """) + conn.commit() + conn.close() + +def init_agents_db(): + """Agent registry — onboarding + state.""" + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute(""" + CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, + name TEXT, + role TEXT, + intent TEXT, + model TEXT, + provider TEXT, + status TEXT DEFAULT 'idle', + last_run TEXT, + cycle_count INTEGER DEFAULT 0, + success_count INTEGER DEFAULT 0, + fail_count INTEGER DEFAULT 0, + avg_duration_ms INTEGER DEFAULT 0, + created_at TEXT, + updated_at TEXT + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS agent_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + agent_id TEXT, + timestamp TEXT, + event TEXT, + duration_ms INTEGER, + result TEXT, + FOREIGN KEY (agent_id) REFERENCES agents(id) + ) + """) + c.execute(""" + CREATE TABLE IF NOT EXISTS services ( + id TEXT PRIMARY KEY, + name TEXT, + type TEXT, + endpoint TEXT, + status TEXT DEFAULT 'unknown', + last_check TEXT, + metadata TEXT + ) + """) + conn.commit() + conn.close() + +def seed_agents(): + """Seed default agents on first run.""" + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + # Check if already seeded + row = c.execute("SELECT COUNT(*) FROM agents").fetchone()[0] + if row == 0: + defaults = [ + ("brief", "Brief", "Market Intelligence Analyst", + "Analisa dados de mercado BTC/CRIPTOS. Coleta preços, notícias, indicadores técnicos. Fornece resumo resumido ao Decio.", + "google/gemma-4-31b-it:free", "openrouter", "idle"), + ("decio", "Decio", "Estrategista Soberano (CEO)", + "CEO Estrategista. Recebe briefing do Brief e decide BUY/SELL/HOLD para BTC. Define stop_loss e take_profit.", + "deepseek/deepseek-v4-flash:free", "openrouter", "idle"), + ("papert", "PaperT", "Executor de Baixa Latência", + "Executor. Valida decisão do Decio e executa ordens no Binance (simulação). Gera log de execução.", + "google/gemma-4-26b-a4b-it:free", "openrouter", "idle"), + ("audit", "Audit", "Auditor & Compliance", + "Auditor. Revisa cada ciclo pipeline. Valida decisões, verifica compliance, gera relatório de risco.", + "deepseek/deepseek-v4-flash:free", "openrouter", "idle"), + ] + now = datetime.now().isoformat() + for a in defaults: + c.execute("INSERT INTO agents (id,name,role,intent,model,provider,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)", + (*a, now, now)) + conn.commit() + conn.close() + +init_db() +init_agents_db() +seed_agents() + +# ── Agent State ──────────────────────────────────────────────────────────────── +class AgentState: + def __init__(self): + self.lock = threading.Lock() + self.brief = {"status": "idle", "message": "Aguardando...", "last_run": None} + self.decio = {"status": "idle", "message": "Aguardando...", "last_run": None} + self.papert = {"status": "idle", "message": "Aguardando...", "last_run": None} + self.audit = {"status": "idle", "message": "Aguardando...", "last_run": None} + self.pipeline_running = False + self.last_pipeline = None + + def _ensure_agent(self, agent): + """Ensure instance attribute exists (getattr returns CLASS attr otherwise).""" + if not hasattr(self, agent) or not isinstance(getattr(self, agent), dict): + setattr(self, agent, {"status": "idle", "message": "Aguardando...", "last_run": None}) + + def update(self, agent, status, message): + with self.lock: + self._ensure_agent(agent) + d = getattr(self, agent) + d["status"] = status + d["message"] = message + d["last_run"] = datetime.now().isoformat() + + def set_pipeline_running(self, val): + with self.lock: + self.pipeline_running = val + if val: + self.last_pipeline = datetime.now().isoformat() + + def get_all(self): + with self.lock: + return { + "brief": self.brief.copy(), + "decio": self.decio.copy(), + "papert": self.papert.copy(), + "audit": self.audit.copy(), + "pipeline_running": self.pipeline_running, + "last_pipeline": self.last_pipeline + } + +state = AgentState() + +# ── Logging ─────────────────────────────────────────────────────────────────── +def log_execution(agent, action, result, status="success"): + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute("INSERT INTO executions (timestamp, agent, action, result, status) VALUES (?, ?, ?, ?, ?)", + (datetime.now().isoformat(), agent, action, result, status)) + conn.commit() + conn.close() + # Also log to agent_logs + _log_agent_event(agent, "execution", result) + +def _log_agent_event(agent_id, event, result, duration_ms=0): + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute("INSERT INTO agent_logs (agent_id, timestamp, event, duration_ms, result) VALUES (?, ?, ?, ?, ?)", + (agent_id, datetime.now().isoformat(), event, duration_ms, result)) + conn.commit() + conn.close() + +# ── Pipeline Execution ──────────────────────────────────────────────────────── +def run_pipeline(): + state.set_pipeline_running(True) + state.update("brief", "working", "Analisando mercado...") + state.update("decio", "waiting", "Aguardando análise...") + state.update("papert", "waiting", "Aguardando decisão...") + state.update("audit", "waiting", "Aguardando ciclo...") + + start = datetime.now() + brief_data, decio_data, papert_data, audit_data = {}, {}, {}, {} + + try: + # ── Step 1: Brief ── + t0 = datetime.now() + state.update("brief", "working", "Buscando dados da Binance...") + brief = BriefAgent() + brief_data = brief.run() + t1 = datetime.now() + state.update("brief", "done", brief_data.get("summary", "")[:80]) + # Also persist brief key metrics to state for /api/state + brief_summary = brief_data.get("summary", "") + state.brief.update({ + "price": brief_data.get("price"), + "signal": brief_data.get("signal"), + "confidence": brief_data.get("confidence"), + "rsi": brief_data.get("rsi"), + "fear_greed": brief_data.get("fear_greed"), + "fear_greed_class": brief_data.get("fear_greed_class"), + "change_4h": brief_data.get("change_4h"), + "change_24h": brief_data.get("change_24h"), + "funding_rate": brief_data.get("funding_rate"), + "long_ratio": brief_data.get("long_ratio"), + "support": brief_data.get("support"), + "resistance": brief_data.get("resistance"), + "bullish_signals": brief_data.get("bullish_signals"), + "bearish_signals": brief_data.get("bearish_signals"), + "net_signal": brief_data.get("net_signal"), + "news_sentiment": brief_data.get("news_sentiment"), + "macd_histogram": brief_data.get("macd_histogram"), + "btc_dominance": brief_data.get("btc_dominance"), + }) + log_execution("brief", "market_analysis", brief_summary, "success") + _update_agent_stats("brief", t1 - t0, True) + + # ── Step 2: Decio ── + t0 = datetime.now() + state.update("decio", "working", "Formando estratégia...") + decio = DecioAgent(brief_data) + decio_data = decio.run() + t1 = datetime.now() + state.update("decio", "done", decio_data.get("decision", "HOLD")) + state.decio.update({ + "action": decio_data.get("action"), + "confidence": decio_data.get("confidence"), + "stop_loss": decio_data.get("stop_loss"), + "take_profit": decio_data.get("take_profit"), + "justification": decio_data.get("justification", "")[:100], + }) + log_execution("decio", "strategy_decision", decio_data.get("decision", "HOLD"), "success") + _update_agent_stats("decio", t1 - t0, True) + + # ── Step 3: PaperT ── + t0 = datetime.now() + state.update("papert", "working", "Executando ordens...") + papert = PapertAgent(decio_data) + papert_data = papert.run() + t1 = datetime.now() + state.update("papert", "done", papert_data.get("result", "")[:80]) + log_execution("papert", "order_execution", papert_data.get("result", ""), "success") + _update_agent_stats("papert", t1 - t0, True) + + # ── Step 4: Audit ── + t0 = datetime.now() + state.update("audit", "working", "Auditando compliance...") + audit = AuditAgent() + audit_data = audit.audit_pipeline(brief_data, decio_data, papert_data) + t1 = datetime.now() + state.update("audit", "done", audit_data.get("summary", "")[:80]) + log_execution("audit", "compliance_check", audit_data.get("summary", ""), "success") + _update_agent_stats("audit", t1 - t0, True) + + # ── Save pipeline run ── + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(""" + INSERT INTO pipeline_runs (started_at, finished_at, brief_result, decio_decision, papert_result, audit_report, status) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + start.isoformat(), + datetime.now().isoformat(), + brief_data.get("summary", ""), + decio_data.get("decision", "HOLD"), + papert_data.get("result", ""), + json.dumps(audit_data), + "success" + )) + conn.commit() + conn.close() + + except Exception as e: + log_execution("pipeline", "error", str(e), "error") + state.update("brief", "error", f"Erro: {str(e)[:80]}") + _update_agent_stats("brief", datetime.now() - start, False) + + finally: + state.set_pipeline_running(False) + state.update("decio", "idle", "Aguardando...") + state.update("papert", "idle", "Aguardando...") + state.update("audit", "idle", "Aguardando...") + +def _update_agent_stats(agent_id, duration, success): + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + now = datetime.now().isoformat() + dur = int(duration.total_seconds() * 1000) + c.execute("UPDATE agents SET last_run=?, updated_at=? WHERE id=?", (now, now, agent_id)) + c.execute("UPDATE agents SET cycle_count=cycle_count+1 WHERE id=?", (agent_id,)) + if success: + c.execute("UPDATE agents SET success_count=success_count+1 WHERE id=?", (agent_id,)) + else: + c.execute("UPDATE agents SET fail_count=fail_count+1 WHERE id=?", (agent_id,)) + # Rolling avg duration + row = c.execute("SELECT avg_duration_ms, cycle_count FROM agents WHERE id=?", (agent_id,)).fetchone() + if row and row[1]: + old_avg = row[0] or 0 + count = row[1] + new_avg = int((old_avg * (count - 1) + dur) / count) + c.execute("UPDATE agents SET avg_duration_ms=? WHERE id=?", (new_avg, agent_id)) + conn.commit() + conn.close() + +# ── Routes ──────────────────────────────────────────────────────────────────── +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/api/state") +def api_state(): + return jsonify(state.get_all()) + +@app.route("/api/run", methods=["POST"]) +def api_run(): + if state.pipeline_running: + return jsonify({"error": "Pipeline já em execução"}), 409 + thread = threading.Thread(target=run_pipeline) + thread.start() + return jsonify({"status": "started", "message": "Pipeline iniciado"}) + +@app.route("/api/history") +def api_history(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + rows = c.execute("SELECT * FROM executions ORDER BY timestamp DESC LIMIT 50").fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + +@app.route("/api/pipeline_history") +def api_pipeline_history(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + rows = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 50").fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + +# ── Token Tracking ──────────────────────────────────────────────────────────── +@app.route("/api/token_stats", methods=["GET"]) +def api_token_stats(): + """Daily token bar chart data per agent — for dashboard.""" + days = int(request.args.get("days", 7)) + from agents.token_tracker import get_token_stats + return jsonify(get_token_stats(days=days)) + +@app.route("/api/token_totals", methods=["GET"]) +def api_token_totals(): + """All-time token totals per agent.""" + conn = sqlite3.connect(DB_PATH) + c = conn.cursor() + c.execute(""" + SELECT agent, SUM(prompt_tokens) as p, SUM(completion_tokens) as c, + SUM(total_tokens) as t, SUM(cost_usd) as cost, COUNT(*) as calls + FROM token_usage GROUP BY agent + """) + rows = c.fetchall() + conn.close() + return jsonify([{"agent": r[0], "prompt": r[1], "completion": r[2], + "total": r[3], "cost": r[4], "calls": r[5]} for r in rows]) + +# ── Item 1: Agent Registry ──────────────────────────────────────────────────── +@app.route("/api/agents", methods=["GET"]) +def get_agents(): + """List all registered agents with full status.""" + conn = sqlite3.connect(AGENTS_DB) + conn.row_factory = sqlite3.Row + c = conn.cursor() + rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + +@app.route("/api/agents/", methods=["GET"]) +def get_agent(agent_id): + conn = sqlite3.connect(AGENTS_DB) + conn.row_factory = sqlite3.Row + c = conn.cursor() + row = c.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone() + conn.close() + if not row: + return jsonify({"error": "Agent not found"}), 404 + return jsonify(dict(row)) + +@app.route("/api/agents/", methods=["PUT"]) +def update_agent(agent_id): + """Update agent config (model, provider, status, intent).""" + data = request.json + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + fields = [] + values = [] + for field in ("name", "role", "intent", "model", "provider", "status"): + if field in data: + fields.append(f"{field}=?") + values.append(data[field]) + if fields: + fields.append("updated_at=?") + values.append(datetime.now().isoformat()) + values.append(agent_id) + c.execute(f"UPDATE agents SET {','.join(fields)} WHERE id=?", values) + conn.commit() + conn.close() + return jsonify({"status": "updated", "id": agent_id}) + +@app.route("/api/agents/", methods=["DELETE"]) +def delete_agent(agent_id): + """Remove agent from registry.""" + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute("DELETE FROM agent_logs WHERE agent_id=?", (agent_id,)) + c.execute("DELETE FROM agents WHERE id=?", (agent_id,)) + conn.commit() + conn.close() + return jsonify({"status": "deleted", "id": agent_id}) + +@app.route("/api/agents", methods=["POST"]) +def register_agent(): + """Onboard a new agent.""" + data = request.json + if not data.get("id") or not data.get("name"): + return jsonify({"error": "id and name required"}), 400 + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + now = datetime.now().isoformat() + c.execute(""" + INSERT OR REPLACE INTO agents (id, name, role, intent, model, provider, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, COALESCE(?, 'idle'), ?, ?) + """, ( + data["id"], data["name"], data.get("role", ""), + data.get("intent", ""), data.get("model", "unknown"), + data.get("provider", "unknown"), data.get("status"), + now, now + )) + conn.commit() + conn.close() + return jsonify({"status": "registered", "id": data["id"]}), 201 + +# ── Item 2: Service Metrics ─────────────────────────────────────────────────── +@app.route("/api/agent_metrics", methods=["GET"]) +def get_agent_metrics(): + """Dashboard metrics for all agents.""" + conn = sqlite3.connect(AGENTS_DB) + conn.row_factory = sqlite3.Row + c = conn.cursor() + rows = c.execute("SELECT * FROM agents ORDER BY id").fetchall() + conn.close() + + # Merge with pipeline stats from execution DB + conn2 = sqlite3.connect(DB_PATH) + conn2.row_factory = sqlite3.Row + c2 = conn2.cursor() + + result = [] + for row in rows: + r = dict(row) + # Get last execution from executions table + last = c2.execute( + "SELECT timestamp, action, result FROM executions WHERE agent=? ORDER BY timestamp DESC LIMIT 1", + (r["id"],) + ).fetchone() + if last: + r["last_action"] = dict(last) + # Success rate + total = r.get("cycle_count", 0) or 1 + ok = r.get("success_count", 0) or 0 + r["success_rate"] = round(ok / total * 100, 1) + result.append(r) + conn2.close() + + return jsonify(result) + +@app.route("/api/agent_logs/", methods=["GET"]) +def get_agent_logs(agent_id): + """Event log for specific agent.""" + conn = sqlite3.connect(AGENTS_DB) + conn.row_factory = sqlite3.Row + c = conn.cursor() + limit = request.args.get("limit", 50, type=int) + rows = c.execute( + "SELECT * FROM agent_logs WHERE agent_id=? ORDER BY timestamp DESC LIMIT ?", + (agent_id, limit) + ).fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + +# ── Item 3: Auto-Discovery ───────────────────────────────────────────────────── +@app.route("/api/services", methods=["GET"]) +def get_services(): + """List all registered services.""" + conn = sqlite3.connect(AGENTS_DB) + conn.row_factory = sqlite3.Row + c = conn.cursor() + rows = c.execute("SELECT * FROM services ORDER BY name").fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + +@app.route("/api/services", methods=["POST"]) +def register_service(): + """Auto-discover / register a new service.""" + data = request.json + if not data.get("name"): + return jsonify({"error": "name required"}), 400 + + service_id = data.get("id") or hashlib.md5(data["name"].encode()).hexdigest()[:12] + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute(""" + INSERT OR REPLACE INTO services (id, name, type, endpoint, status, last_check, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + service_id, + data["name"], + data.get("type", "unknown"), + data.get("endpoint", ""), + data.get("status", "unknown"), + datetime.now().isoformat(), + json.dumps(data.get("metadata", {})) + )) + conn.commit() + conn.close() + return jsonify({"status": "registered", "id": service_id}), 201 + +@app.route("/api/services//ping", methods=["POST"]) +def ping_service(service_id): + """Health check a service endpoint.""" + import requests as req + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + row = c.execute("SELECT endpoint FROM services WHERE id=?", (service_id,)).fetchone() + conn.close() + if not row or not row[0]: + return jsonify({"error": "No endpoint for service"}), 404 + + endpoint = row[0] + try: + r = req.get(endpoint, timeout=5) + status = "healthy" if r.status_code < 500 else "degraded" + except Exception as e: + status = "unreachable" + + conn = sqlite3.connect(AGENTS_DB) + c = conn.cursor() + c.execute("UPDATE services SET status=?, last_check=? WHERE id=?", + (status, datetime.now().isoformat(), service_id)) + conn.commit() + conn.close() + return jsonify({"id": service_id, "status": status, "endpoint": endpoint}) + +# ── Item 4: Pipeline Visual ────────────────────────────────────────────────── +@app.route("/api/pipeline/visual") +def pipeline_visual(): + """Real-time pipeline flow with metrics.""" + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + c = conn.cursor() + + # Last pipeline run + last = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 1").fetchone() + last_5 = c.execute("SELECT * FROM pipeline_runs ORDER BY started_at DESC LIMIT 5").fetchall() + + # Execution summary stats + stats = c.execute(""" + SELECT agent, COUNT(*) as total, + SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) as ok, + MAX(timestamp) as last_ts + FROM executions GROUP BY agent + """).fetchall() + + conn.close() + + # Build pipeline nodes + nodes = [ + {"id": "brief", "label": "Brief", "role": "Market Intelligence", + "emoji": "📊", "color": "#4fc3f7"}, + {"id": "decio", "label": "Decio", "role": "Estrategista (CEO)", + "emoji": "🎯", "color": "#ffd54f"}, + {"id": "papert", "label": "PaperT", "role": "Executor", + "emoji": "⚡", "color": "#69f0ae"}, + {"id": "audit", "label": "Audit", "role": "Auditor Compliance", + "emoji": "🔍", "color": "#b388ff"}, + ] + + # Current state from state object + current = state.get_all() + + # Enrich nodes with current state and stats + for node in nodes: + aid = node["id"] + node.update(current.get(aid, {"status": "idle", "message": ""})) + # Stats from DB + for s in stats: + if dict(s)["agent"] == aid: + node["total_runs"] = dict(s)["total"] + node["success_runs"] = dict(s)["ok"] + node["last_execution"] = dict(s)["last_ts"] + + # Last pipeline decision + last_decision = None + if last: + ld = dict(last) + last_decision = ld.get("decio_decision", "") + last_brief = ld.get("brief_result", "")[:100] + + return jsonify({ + "nodes": nodes, + "last_decision": last_decision, + "last_brief": last_brief, + "last_pipeline_ts": last["started_at"] if last else None, + "recent_runs": [dict(r) for r in last_5], + "stats": [dict(s) for s in stats] + }) + +# ── Portfolio API ───────────────────────────────────────────────────────────── +@app.route("/api/portfolio", methods=["GET"]) +def get_portfolio(): + """Returns current portfolio stats + chart data.""" + sys.path.insert(0, "/app/agents") + try: + import papert as papert_module + pf = papert_module.Portfolio() + stats = pf.get_stats() + # Recent trades + conn = sqlite3.connect(str(papert_module.PORTFOLIO_DB)) + conn.row_factory = sqlite3.Row + c = conn.cursor() + recent_trades = c.execute( + "SELECT * FROM trades ORDER BY created_at DESC LIMIT 20" + ).fetchall() + conn.close() + stats["recent_trades"] = [dict(t) for t in recent_trades] + stats["initial_balance"] = papert_module.INITIAL_BALANCE + return jsonify(stats) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route("/api/portfolio/reset", methods=["POST"]) +def reset_portfolio(): + """Reset portfolio to initial $10.000.""" + sys.path.insert(0, "/app/agents") + try: + import papert as papert_module + pf = papert_module.Portfolio() + conn = sqlite3.connect(str(papert_module.PORTFOLIO_DB)) + c = conn.cursor() + c.execute("DELETE FROM trades") + c.execute("DELETE FROM daily_snapshot") + today = date.today().isoformat() + c.execute("DELETE FROM portfolio WHERE date=?", (today,)) + c.execute("INSERT INTO portfolio (date,balance,btc_held,created_at) VALUES (?,?,?,?)", + (today, papert_module.INITIAL_BALANCE, 0.0, datetime.now().isoformat())) + conn.commit() + conn.close() + return jsonify({"status": "reset", "balance": papert_module.INITIAL_BALANCE}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=3100, debug=False) \ No newline at end of file diff --git a/data/.env b/data/.env new file mode 100644 index 0000000..e016ff6 --- /dev/null +++ b/data/.env @@ -0,0 +1,3 @@ +DECIO_API_KEY=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 +BRIEF_API_KEY=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 +PAPERT_API_KEY=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 diff --git a/data/agents.db b/data/agents.db new file mode 100644 index 0000000000000000000000000000000000000000..e901a74cf58569962a42f758ea6684cfaa4c423d GIT binary patch literal 28672 zcmeI(OK;mo5CCA(lI=X4D3?O@A;1J^8XJ~mTYjW58nm>`q-x|xCAx@D%9>os3sYQX zNh(QuvI7+8FX%7mA?N-CANvP#&aH<6y*0OXNLg0Iyig!LR2>4c$R(HB{bnd&$9ChJ zng}_xr0TE`mb5FHu4`WcXquMA*Cf9BTLyRHw*IE~ci!=y)k@zzyp(&XOAnG0~b2?s3mz6Cq=>ZVcOgL~#);q0WxIy^Z&XbPYB=_c+uz7$@V zx-tk^7zMq%Z>4^#a+JY)_FS*qDqoQ2$J;%Xuu zcrw=6`H6{q<*Kd;&*6`Qhb}HTnk))&RCwI%PrJF;-<#aYPUPq2^i#B6hk!qfxQBSd zhvUP9!zX8Aq5}4uJ^nZ|nZI^TfBJnA2-fCa*mPw(80=+-?lfp04o7v%GVhx8S0e(( zuC-m=!tR^qmNl^J-QqNagXU0lIO=!&6dmv4m{M&{q*y4V*X#sv`pJVEDIP>_y2qXE zx>>!u6=w>}^hYoYb@Pr{H@7Ng1CqvIMmV#*=sexfC-c+O`VaPirpFSDy-pGgbNr+1 zRMJgnCno0Wv+hWTyD14@$*yp?N;SVRjfLa#bPlV?OLeuM*e-9{GD7u!RQpfD!azBQ z?D&a&|Aj3k-1lF_y6xs0q6%;L ziyRRe@sEn^_^x2M@P_1AsygqAfPsz6f3St^O7MlxUE%Q(tU^*N2-aX-3EqMwKMV?Z z-etI0Er8-}#o*@3^#U|%^VUuQc9_qV1-KH&eW}8hbcKZ80;l9~?gyM7&i8kU-Q{8n zOA-fbEycO?xu;|lauqN5$_wSy;=)RCan&j>t}QLEEi9LAt$cR#X8G-Bj=ap3>1p%m zR`qP=emtPCH!Slpx3Ns2;L}&ew%uxB2}ga)T!8>zH@3IHk+8vSAq&8h9t@;%xEDa9 zy1833HjD;N9SbRn3kH5X%adQ?X?{{(Sb*Keh6P=r!ic%0wv=s`&$oG}!-~tr^6Gw( zE$O9UUr1k#QFI{WxKhtleblkYh~%OB1duuu>G~zQ^1rNP-m% zta&-ikpVayR{FWU2HRe-a=?UFz+6zGi)EXx#FTGfc>@bRUWnOYojkKWG%Ua-vlUX_ z<@h)xA1u6fe7_}354psrHPW%jC}|Wrz6p|M*A$ zw2=S_kN^pg011!)36KB@kN^pg011q+0ImN=IJh)55+DH*AOR8}0TLhq5+DH*AOR9M z6`=J$r2+|%011!)36KB@kN^pg011!)35>o#{Qv*-_y40GXPO}ikN^pg011!)36KB@ MkN^pg!2es|Z>hYzH=UZRuT!7@hz= z_AL!*EkzujKGD4T+jJ;!TML2}BN zqN>VcLKH=b%kQB4wv#3^1MPy`t6lki*|?H^cQ_(j2kxCHiJ{RiBWE&$1OW&@00Izz z00bZa0SNqe0%zJlJUKb3o>v6jwU}@Ym~Hr+?ssPgSGP-gwM44=O1VV3jv>>-L?*LN z);6l8r=@MOwY^r+w|B^TX@}^qs+(&Y^4v;kqdFTNDa;1rf;L=IExm5l$!po7tK4wv z%<3w;p3`uJ5nh(JFLd2qO}5BEn~ojXEF3|t$d;PJg}lu=kvCQzo;k~E@#NH$`hGH0 za7~w4rp*k`w|P%tphu#osbvv(1nI|3u8$`~x_E-s4LJwA!D%<{nYPK_^lRPqOtxou zjQdv9$#qsU9V6IsNfpt%OI_xP{vF7n37>~2hL15A)y!7xPBN8JcS16LXvsOO!P%kD z>>9gO)Oy%%HBQ^K!Di)|Gp9n0CnqM<^IX^`d&_Em@NoG}F4oi2P5m$HN4=SC{M&D6 zr@j`9F2b|#lZb>2=H}YyS0(Ww@m%hZAOHafKmY;|fB*y_009U<00MuQ!0Cu~SJCgM zPE!xE^V!9@`TSgFv6@*fW;4ayQhISQpPgU04lX!eoq0xQ!hUseaY>$SmY)ks%8j9-dC8xnGk(2fh-E zD|+(Lc%RW^3YmNu3*o|}g6$u7Vj{svT79@`&_~LT?jyfYC=|kQOM`j`Oc>Or)-gBv zN_CaUAPKdK-Gp#EI^Z!`lo07Pa z8zcxo00Izz00bZa0SG_<0uX=z1pY#Sv6wbB)U#4#RMjTbP9%hD!TeuMWEJ^Af&c^{ z009U<00Izz00bZa0SG{#BXGK|4k^0skM~`8P{=JTM=wbzn${PZP(s35T6kL zB;vo|lYfTqJo)Oi-OsuugairXd)bfc{c7)XcTF$phxav8aN2UbHWPG%JR*uho>NK) zS(4uw`R#`V8AnEM`!*i0l|C>Dcy6`7`E+YsKfB*y_009U<00P%6 z@a=JGE|beC-C7B`q_9qYc$T>r=s<3}?u8)`*UsaAPWQ|WjfU74r7U>dYq*VH#^ zgYGq|yOqWPeXAbO%D(olT9-}is&y?tC~YwLjH#&~N0JVi#q1`h@@%zX!9&s1kGN$_ zNOnE*pW?z1%+fzIs~c~~wXLv?@DlRWak;Gvj~Uz#lj$q;1+?YelD?l=H>3buZ{}(6!MjO^?fDLzfTQfkp1LtjuMyS>^0$I4Z8=iI!uT&LEN) z8;e2me;$Tm7kZdoTXw`UPh?jn^*@s7VfQD=h`03d2a{(cT@HgUhi5{KINW0OGJB6} zUQ;*N{OPmgTxNM$IXeg^PDCC3rpBgBZxF7!Y=zn8zF=)Pwsu)ln2sG!Y&%_B=x$TQ z7Vo5m8?18{R)`gD@o6i0c$4tB-?gGOG)(k`WcFEN{0 zT~#7KzhlWgu@Ybd8p~X0?HO69e})>^Sleh%YWskpPbv9qmRxD_45MM3}q5P$##AOHafKmY;|fB*y_a2bKLlFTZDSBYkW{l9$a9{~ao zfB*y_009U<00Izz00bZafjcaK{r?>vTyzZr5P$##AOHafKmY;|fB*y_&=dFzf&p2K literal 0 HcmV?d00001 diff --git a/logs/watchdog.log b/logs/watchdog.log new file mode 100644 index 0000000..4849533 --- /dev/null +++ b/logs/watchdog.log @@ -0,0 +1,10 @@ +[2026-05-17T14:56:04Z] Watchdog started (PID=3477519) +[2026-05-17T14:56:04Z] Waiting 236s until next trigger +[2026-05-17T15:00:04Z] Pipeline triggered OK (HTTP 200) +[2026-05-17T15:00:04Z] Waiting 1796s until next trigger +[2026-05-17T15:04:52Z] Watchdog started (PID=3533452) +[2026-05-17T15:04:52Z] Waiting 1508s until next trigger +[2026-05-17T15:30:00Z] Pipeline triggered OK (HTTP 200) +[2026-05-17T15:30:00Z] Waiting 1800s until next trigger +[2026-05-17T16:00:02Z] Pipeline triggered OK (HTTP 200) +[2026-05-17T16:00:02Z] Waiting 1798s until next trigger diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30692b7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +requests diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..e7d86b0 --- /dev/null +++ b/static/script.js @@ -0,0 +1,608 @@ +/** + * BrainSteel Fin — Dashboard JavaScript + * 4-Agent Pipeline: Brief → Decio → PaperT → Audit + */ + +// ── State ───────────────────────────────────────────────────────────────────── +let autoMode = false; +let autoInterval = null; +let pollInterval = null; +const AGENT_EMOJIS = { brief: "📊", decio: "🎯", papert: "⚡", audit: "🔍" }; +const AGENT_COLORS = { brief: "#4fc3f7", decio: "#ffd54f", papert: "#69f0ae", audit: "#b388ff" }; + +// ── Init ───────────────────────────────────────────────────────────────────── +document.addEventListener('DOMContentLoaded', () => { + updateClock(); + setInterval(updateClock, 1000); + startPolling(); + loadAgentRegistry(); + loadMetrics(); + loadServices(); + loadPipelineVisual(); + loadTokenChart(); + updateStatus('Sistema operacional', 'online'); +}); + +// ── Clock ───────────────────────────────────────────────────────────────────── +function updateClock() { + const now = new Date(); + document.getElementById('clock').textContent = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); +} + +// ── Polling ─────────────────────────────────────────────────────────────────── +function startPolling() { + pollState(); + pollInterval = setInterval(pollState, 3000); +} + +function pollState() { + fetch('/api/state') + .then(r => r.json()) + .then(data => { + updateAgentUI('brief', data.brief); + updateAgentUI('decio', data.decio); + updateAgentUI('papert', data.papert); + updateAgentUI('audit', data.audit); + document.getElementById('btnRun').disabled = data.pipeline_running; + updateStatus(data.pipeline_running ? 'Pipeline em execução' : 'Aguardando', data.pipeline_running ? 'running' : 'online'); + if (!data.pipeline_running && data.last_pipeline) { + showDecisionBanner(data); + } + }).catch(err => console.error('Poll error:', err)); +} + +function updateAgentUI(agent, data) { + const node = document.getElementById(`node-${agent}`); + const statusEl = document.getElementById(`status-${agent}`); + const msgEl = document.getElementById(`msg-${agent}`); + if (!node) return; + node.className = `pipeline-node ${data.status || 'idle'}`; + if (statusEl) { + const labels = { idle: 'Aguardando', working: 'Trabalhando', done: 'Concluído', error: 'Erro', waiting: 'Aguardando' }; + statusEl.textContent = labels[data.status] || data.status; + statusEl.className = `agent-status ${data.status}`; + } + if (msgEl) msgEl.textContent = data.message || ''; +} + +// ── Status ──────────────────────────────────────────────────────────────────── +function updateStatus(text, type) { + const dot = document.getElementById('statusDot'); + const txt = document.getElementById('statusText'); + if (dot) dot.className = `status-dot ${type}`; + if (txt) txt.textContent = text; +} + +// ── Pipeline ───────────────────────────────────────────────────────────────── +function runPipeline() { + fetch('/api/run', { method: 'POST' }) + .then(r => r.json()) + .then(d => { + if (d.error) { alert(d.error); return; } + addLog('SYS', 'Pipeline iniciado'); + }).catch(err => console.error(err)); +} + +function toggleAuto() { + autoMode = !autoMode; + const el = document.getElementById('autoStatus'); + if (el) el.textContent = autoMode ? 'ON' : 'OFF'; + if (autoMode) { + addLog('SYS', 'Modo automático ativado — a cada 5 min'); + autoInterval = setInterval(runPipeline, 5 * 60 * 1000); + } else { + clearInterval(autoInterval); + addLog('SYS', 'Modo automático desativado'); + } +} + +// ── Decision Banner ──────────────────────────────────────────────────────────── +function showDecisionBanner(data) { + const banner = document.getElementById('lastDecisionBanner'); + if (!banner) return; + const decision = data.decio?.message || 'HOLD'; + const cls = decision.includes('BUY') ? 'buy' : decision.includes('SELL') ? 'sell' : 'hold'; + banner.className = `decision-banner ${cls}`; + banner.textContent = `Última decisão: ${decision}`; + banner.style.display = 'block'; +} + +// ── Item 1: Agent Registry ──────────────────────────────────────────────────── +function loadAgentRegistry() { + fetch('/api/agents') + .then(r => r.json()) + .then(agents => { + const container = document.getElementById('agentCards'); + if (!container) return; + container.innerHTML = agents.map(a => ` +

+
+ ${AGENT_EMOJIS[a.id] || '🤖'} +
+
${a.name}
+
${a.role || ''}
+
+
+
${a.intent || '—'}
+
+ ⏱ ${a.cycle_count || 0} ciclos + ✓ ${a.success_count || 0} + ✗ ${a.fail_count || 0} +
+
${a.model || '—'} @ ${a.provider || '?'}
+
+ + +
+
+ `).join(''); + }).catch(err => console.error('Agent registry error:', err)); +} + +function showAddAgent() { + document.getElementById('modalAgentTitle').textContent = 'Novo Agente'; + ['agentId','agentName','agentRole','agentIntent','agentModel','agentProvider'].forEach(id => { + const el = document.getElementById(id); + if (el) el.value = id === 'agentId' ? '' : ''; + if (id === 'agentModel') el.value = 'deepseek/deepseek-v4-flash:free'; + if (id === 'agentProvider') el.value = 'openrouter'; + }); + document.getElementById('modalAgent').style.display = 'flex'; +} + +function editAgent(id) { + fetch(`/api/agents/${id}`).then(r => r.json()).then(a => { + document.getElementById('modalAgentTitle').textContent = `Editar: ${a.name}`; + document.getElementById('agentId').value = a.id; + document.getElementById('agentId').readOnly = true; + document.getElementById('agentName').value = a.name || ''; + document.getElementById('agentRole').value = a.role || ''; + document.getElementById('agentIntent').value = a.intent || ''; + document.getElementById('agentModel').value = a.model || ''; + document.getElementById('agentProvider').value = a.provider || ''; + document.getElementById('modalAgent').style.display = 'flex'; + }); +} + +function saveAgent() { + const id = document.getElementById('agentId').value.trim(); + if (!id) { alert('ID é obrigatório'); return; } + const data = { + id, name: document.getElementById('agentName').value.trim(), + role: document.getElementById('agentRole').value.trim(), + intent: document.getElementById('agentIntent').value.trim(), + model: document.getElementById('agentModel').value.trim(), + provider: document.getElementById('agentProvider').value.trim() + }; + fetch(`/api/agents/${id}`, { + method: id ? 'PUT' : 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }).then(r => r.json()).then(() => { + closeAgent(); + loadAgentRegistry(); + addLog('SYS', `Agente ${id} atualizado`); + }); +} + +function deleteAgent(id) { + if (!confirm(`Remover agente ${id}?`)) return; + fetch(`/api/agents/${id}`, { method: 'DELETE' }).then(r => r.json()).then(() => { + loadAgentRegistry(); + addLog('SYS', `Agente ${id} removido`); + }); +} + +function closeAgent() { + document.getElementById('modalAgent').style.display = 'none'; + const el = document.getElementById('agentId'); + if (el) el.readOnly = false; +} + +// ── Item 2: Agent Metrics ──────────────────────────────────────────────────── +function loadMetrics() { + fetch('/api/agent_metrics') + .then(r => r.json()) + .then(metrics => { + const grid = document.getElementById('metricsGrid'); + if (!grid) return; + grid.innerHTML = metrics.map(m => { + const color = AGENT_COLORS[m.id] || '#888'; + const rate = m.success_rate || 0; + const avg = m.avg_duration_ms ? `${(m.avg_duration_ms/1000).toFixed(1)}s` : '—'; + return ` +
+
${AGENT_EMOJIS[m.id] || '🤖'} ${m.name}
+
${rate}%
+
${m.cycle_count || 0} ciclos · avg ${avg}
+
+
`; + }).join(''); + }).catch(err => console.error('Metrics error:', err)); +} + +// ── Item 3: Services ───────────────────────────────────────────────────────── +function loadServices() { + fetch('/api/services') + .then(r => r.json()) + .then(services => { + const list = document.getElementById('servicesList'); + if (!list) return; + if (!services.length) { + list.innerHTML = '
Nenhum serviço registado. Clique em + Novo Serviço.
'; + return; + } + list.innerHTML = services.map(s => ` +
+ + ${s.name} + ${s.type || ''} + ${s.endpoint ? s.endpoint.slice(0,40) : ''} +
+ + +
+
+ `).join(''); + }).catch(err => console.error('Services error:', err)); +} + +function showAddService() { + ['serviceName','serviceType','serviceEndpoint','serviceStatus'].forEach(id => { + const el = document.getElementById(id); + if (el) el.value = ''; + }); + document.getElementById('modalService').style.display = 'flex'; +} + +function saveService() { + const name = document.getElementById('serviceName').value.trim(); + if (!name) { alert('Nome é obrigatório'); return; } + const data = { + name, + type: document.getElementById('serviceType').value.trim(), + endpoint: document.getElementById('serviceEndpoint').value.trim(), + status: document.getElementById('serviceStatus').value.trim() || 'unknown' + }; + fetch('/api/services', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }).then(r => r.json()).then(() => { + closeService(); + loadServices(); + addLog('SYS', `Serviço ${name} registado`); + }); +} + +function pingService(id) { + fetch(`/api/services/${id}/ping`, { method: 'POST' }) + .then(r => r.json()) + .then(d => { + loadServices(); + addLog('SYS', `Ping ${id}: ${d.status}`); + }); +} + +function pingAllServices() { + fetch('/api/services').then(r => r.json()).then(svcs => { + svcs.forEach(s => pingService(s.id)); + addLog('SYS', 'Ping em todos os serviços'); + }); +} + +function deleteService(id) { + fetch(`/api/services/${id}`, { method: 'DELETE' }).then(() => { + loadServices(); + addLog('SYS', `Serviço ${id} removido`); + }); +} + +function closeService() { + document.getElementById('modalService').style.display = 'none'; +} + +// ── Item 4: Pipeline Visual ─────────────────────────────────────────────────── +function loadPipelineVisual() { + fetch('/api/pipeline/visual') + .then(r => r.json()) + .then(data => { + // Visual flow + const flow = document.getElementById('visualFlow'); + if (flow) { + flow.innerHTML = data.nodes.map((n, i) => ` +
+
${AGENT_EMOJIS[n.id] || '🤖'}
+
${n.label}
+
${n.status || 'idle'}
+ ${n.total_runs ? `
${n.total_runs}x
` : ''} +
+ ${i < data.nodes.length - 1 ? '' : ''} + `).join(''); + } + // Timeline + const tl = document.getElementById('visualTimeline'); + if (tl) { + tl.innerHTML = data.recent_runs.map(r => ` +
+ ${r.started_at ? r.started_at.slice(11,19) : '—'} + ${r.decio_decision || '?'} + ${r.status} +
+ `).join(''); + } + }).catch(err => console.error('Pipeline visual error:', err)); +} + +// ── History Modal ───────────────────────────────────────────────────────────── +function showHistory() { + fetch('/api/pipeline_history') + .then(r => r.json()) + .then(data => { + const content = document.getElementById('historyContent'); + if (!content) return; + content.innerHTML = data.map(r => ` +
+
+ ${r.decio_decision || '?'} + ${r.started_at ? r.started_at.slice(0,16).replace('T',' ') : '—'} +
+
${(r.brief_result || '').slice(0,80)}
+
+ `).join(''); + document.getElementById('modalHistory').style.display = 'flex'; + }); +} + +function closeHistory() { + document.getElementById('modalHistory').style.display = 'none'; +} + +// ── Log ─────────────────────────────────────────────────────────────────────── +function addLog(agent, message) { + const container = document.getElementById('logContainer'); + if (!container) return; + const now = new Date(); + const ts = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const div = document.createElement('div'); + div.className = 'log-entry'; + div.innerHTML = `${ts}${agent}${message}`; + container.prepend(div); + while (container.children.length > 50) container.removeChild(container.lastChild); +} + +// Refresh all sections periodically +setInterval(() => { + loadAgentRegistry(); + loadMetrics(); + loadServices(); + loadPipelineVisual(); + loadPortfolio(); +}, 15000); + +// ── Portfolio ─────────────────────────────────────────────────────────────── +function loadPortfolio() { + fetch('/api/portfolio') + .then(r => r.json()) + .then(data => { + if (data.error) return; + const bal = data.current_balance || 0; + const pnl = data.total_pnl || 0; + const pnlPct = data.total_pnl_pct || 0; + const trades = data.total_trades || 0; + const wins = data.wins || 0; + const losses = data.losses || 0; + const wr = data.win_rate || 0; + const init = data.initial_balance || 10000; + + document.getElementById('pfBalance').textContent = `$${bal.toLocaleString('pt-BR', {minimumFractionDigits:2})}`; + const pnlEl = document.getElementById('pfPnl'); + pnlEl.textContent = `P&L: ${pnl >= 0 ? '+' : ''}$${pnl.toLocaleString('pt-BR',{minimumFractionDigits:2})} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`; + pnlEl.style.color = pnl >= 0 ? '#69f0ae' : '#ff6b6b'; + document.getElementById('pfTrades').textContent = `${trades} trades | ${wins}W/${losses}L | WR: ${wr}%`; + + // Draw chart + drawChart(data.chart_data || [], init, bal); + + // Recent trades list + const tradesEl = document.getElementById('portfolioTrades'); + const recent = data.recent_trades || []; + if (recent.length === 0) { + tradesEl.innerHTML = '
Nenhum trade ainda. Execute o pipeline para começar.
'; + } else { + tradesEl.innerHTML = recent.slice(0, 10).map(t => { + const cls = t.action === 'BUY' ? '#69f0ae' : t.action === 'SELL' ? (t.result === 'WIN' ? '#69f0ae' : '#ff6b6b') : '#888'; + return `
+ ${t.date ? t.date.slice(5) : '—'} + ${t.action} + ${t.btc_amount ? t.btc_amount.toFixed(6)+' BTC' : '—'} + @ $${Number(t.entry_price).toLocaleString('pt-BR',{minimumFractionDigits:0})} + ${t.result || ''} +
`; + }).join(''); + } + }).catch(err => console.error('Portfolio error:', err)); +} + +function drawChart(chartData, initial, current) { + const canvas = document.getElementById('chartCanvas'); + if (!canvas || !chartData.length) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width; + const H = canvas.height; + ctx.clearRect(0, 0, W, H); + + // Draw grid + ctx.strokeStyle = 'rgba(255,255,255,0.05)'; + ctx.lineWidth = 1; + for (let i = 0; i < 5; i++) { + const y = (H / 4) * i; + ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); + } + + // Draw data line + if (chartData.length < 2) return; + const allBalances = chartData.map(d => d[1]); + const minBal = Math.min(...allBalances, initial) * 0.98; + const maxBal = Math.max(...allBalances, initial) * 1.02; + const range = maxBal - minBal || 1; + + const toX = (i) => (i / (chartData.length - 1)) * W; + const toY = (v) => H - ((v - minBal) / range) * H; + + // Area fill + ctx.beginPath(); + ctx.moveTo(toX(0), H); + chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal))); + ctx.lineTo(toX(chartData.length - 1), H); + ctx.closePath(); + ctx.fillStyle = 'rgba(79,195,247,0.15)'; + ctx.fill(); + + // Line + ctx.beginPath(); + ctx.moveTo(toX(0), toY(chartData[0][1])); + chartData.forEach(([_, bal], i) => ctx.lineTo(toX(i), toY(bal))); + ctx.strokeStyle = '#4fc3f7'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Initial balance reference + const y0 = toY(initial); + ctx.setLineDash([4, 4]); + ctx.strokeStyle = 'rgba(179,136,255,0.5)'; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(0, y0); ctx.lineTo(W, y0); ctx.stroke(); + ctx.setLineDash([]); + + // Current dot + const lastX = toX(chartData.length - 1); + const lastY = toY(chartData[chartData.length - 1][1]); + ctx.beginPath(); + ctx.arc(lastX, lastY, 4, 0, Math.PI * 2); + ctx.fillStyle = current >= initial ? '#69f0ae' : '#ff6b6b'; + ctx.fill(); +} + +function resetPortfolio() { + if (!confirm('Resetar portfolio para $10.000? Todos os trades serão apagados.')) return; + fetch('/api/portfolio/reset', { method: 'POST' }) + .then(r => r.json()) + .then(() => { + loadPortfolio(); + addLog('SYS', 'Portfolio resetado para $10.000'); + }); +} + +// ── Token Chart ───────────────────────────────────────────────────────────── +const TOKEN_AGENTS = [ + { key: 'Brief', color: '#4fc3f7' }, + { key: 'Decio', color: '#ffd54f' }, + { key: 'PaperT', color: '#69f0ae' }, + { key: 'Audit', color: '#b388ff' }, +]; + +function loadTokenChart() { + const days = parseInt(document.getElementById('tokenDays')?.value || 7); + Promise.all([ + fetch(`/api/token_stats?days=${days}`).then(r => r.json()), + fetch('/api/token_totals').then(r => r.json()), + ]).then(([stats, totals]) => { + drawTokenBarChart(stats); + renderTokenTotals(totals); + document.getElementById('tokenTotals').textContent = + `Total: ${(stats.total_all_time || 0).toLocaleString()} tok | $${(stats.total_cost_usd || 0).toFixed(4)}`; + }).catch(() => {}); +} + +function drawTokenBarChart(stats) { + const canvas = document.getElementById('tokenChart'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width = 800; + const H = canvas.height = 200; + ctx.clearRect(0, 0, W, H); + + const data = stats.chart_data || []; + if (!data.length) { + ctx.fillStyle = '#556'; + ctx.font = '13px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('Sem dados de tokens ainda. Os agentes precisam ser executados.', W / 2, H / 2); + return; + } + + const labels = data.map(d => d.date.slice(5)); // MM-DD + const days = data.length; + const groupW = (W - 80) / Math.max(days, 1); + const barW = Math.min((groupW - 8) / TOKEN_AGENTS.length, 28); + const maxVal = Math.max(...TOKEN_AGENTS.map(a => Math.max(...data.map(d => d[a.key] || 0))), 1); + + // Grid + ctx.strokeStyle = 'rgba(99,110,200,0.15)'; + ctx.lineWidth = 1; + ctx.font = '11px monospace'; + ctx.fillStyle = '#556'; + for (let i = 0; i <= 4; i++) { + const y = 10 + (H - 30) * (1 - i / 4); + ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(W - 10, y); ctx.stroke(); + ctx.textAlign = 'right'; + const val = Math.round(maxVal * i / 4); + ctx.fillText(val.toLocaleString(), 38, y + 4); + } + + // Bars + data.forEach((d, i) => { + const x = 50 + i * groupW; + TOKEN_AGENTS.forEach((ag, j) => { + const val = d[ag.key] || 0; + const barH = val / maxVal * (H - 30); + const bx = x + j * (barW + 2); + const by = H - 20 - barH; + ctx.fillStyle = ag.color; + ctx.fillRect(bx, by, barW - 1, barH); + // Value label + if (val > 0 && barH > 12) { + ctx.fillStyle = '#fff'; + ctx.font = '9px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(val > 999 ? Math.round(val/1000)+'k' : val, bx + barW/2, by + 10); + } + }); + // X label + ctx.fillStyle = '#778'; + ctx.font = '10px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(labels[i], x + groupW / 2, H - 4); + }); + + // Legend + TOKEN_AGENTS.forEach((ag, i) => { + const lx = W - 160 + (i % 2) * 80; + const ly = 16 + Math.floor(i / 2) * 14; + ctx.fillStyle = ag.color; + ctx.fillRect(lx, ly - 9, 10, 10); + ctx.fillStyle = '#aab'; + ctx.font = '10px monospace'; + ctx.textAlign = 'left'; + ctx.fillText(ag.key, lx + 14, ly); + }); +} + +function renderTokenTotals(totals) { + const tbody = document.getElementById('tokenTotalsBody'); + if (!tbody) return; + tbody.innerHTML = ''; + (totals || []).forEach(r => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${r.agent} + ${r.prompt ? r.prompt.toLocaleString() : '-'} + ${r.completion ? r.completion.toLocaleString() : '-'} + ${r.total ? r.total.toLocaleString() : '-'} + ${r.calls || 0} + $${r.cost ? r.cost.toFixed(4) : '0.0000'} + `; + tbody.appendChild(tr); + }); +} \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b32436f --- /dev/null +++ b/static/style.css @@ -0,0 +1,407 @@ +/* BrainSteel Fin — Anime-style Dashboard */ +/* ── Variables ─────────────────────────────────────────── */ +:root { + --bg-dark: #0f0f1a; + --bg-card: #1a1a2e; + --bg-card-hover: #222240; + --accent-blue: #4fc3f7; + --accent-red: #ff6b6b; + --accent-green: #69f0ae; + --accent-yellow: #ffd54f; + --accent-purple: #b388ff; + --text-primary: #e8e8f0; + --text-secondary: #8888aa; + --border-radius: 16px; + --shadow: 0 8px 32px rgba(0,0,0,0.4); +} + +/* ── Reset & Base ───────────────────────────────────────── */ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: 'Exo 2', 'Noto Sans JP', sans-serif; + background: var(--bg-dark); + color: var(--text-primary); + min-height: 100vh; + overflow-x: hidden; +} + +/* ── Dashboard Layout ───────────────────────────────────── */ +.dashboard { + max-width: 1400px; + margin: 0 auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* ── Header ──────────────────────────────────────────────── */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + background: var(--bg-card); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + border: 1px solid rgba(79, 195, 247, 0.1); +} +.logo { display: flex; align-items: center; gap: 12px; font-size: 1.5rem; } +.logo-icon { font-size: 2rem; } +.logo-text strong { color: var(--accent-blue); font-weight: 700; } +.header-status { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); } +.status-dot { + width: 12px; height: 12px; border-radius: 50%; + background: var(--accent-green); + box-shadow: 0 0 8px var(--accent-green); + animation: pulse 2s infinite; +} +.status-dot.running { background: var(--accent-yellow); box-shadow: 0 0 8px var(--accent-yellow); } +.status-dot.error { background: var(--accent-red); box-shadow: 0 0 8px var(--accent-red); } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } } +.header-time { font-variant-numeric: tabular-nums; color: var(--accent-blue); font-size: 1.1rem; } + +/* ── Buttons ─────────────────────────────────────────────── */ +.btn { + padding: 10px 20px; + border: none; + border-radius: 10px; + font-family: inherit; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} +.btn-primary { background: var(--accent-blue); color: #000; } +.btn-primary:hover { background: #7dd3fc; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(79,195,247,0.4); } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } +.btn-secondary { background: var(--bg-card-hover); color: var(--text-primary); border: 1px solid rgba(255,255,255,0.1); } +.btn-secondary:hover { background: #2a2a48; } +.btn-small { padding: 6px 14px; font-size: 0.8rem; background: var(--bg-card-hover); color: var(--text-primary); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; cursor: pointer; } +.btn-small:hover { background: #2a2a48; } + +/* ── Pipeline Section ────────────────────────────────────── */ +.pipeline-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); border: 1px solid rgba(79,195,247,0.1); } +.pipeline-section h2 { margin-bottom: 16px; color: var(--text-primary); } +.pipeline-flow { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; } +.pipeline-arrow { font-size: 2rem; color: var(--text-secondary); } + +/* ── Pipeline Node ────────────────────────────────────────── */ +.pipeline-node { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 16px; + border-radius: 14px; + background: var(--bg-card-hover); + border: 2px solid transparent; + min-width: 120px; + transition: all 0.3s; + cursor: pointer; + position: relative; +} +.pipeline-node:hover { transform: translateY(-4px); border-color: var(--accent-blue); } +.pipeline-node.idle { border-color: rgba(136,136,170,0.3); } +.pipeline-node.working { border-color: var(--accent-yellow); box-shadow: 0 0 20px rgba(255,213,79,0.3); animation: glow-yellow 1.5s infinite; } +.pipeline-node.waiting { border-color: rgba(136,136,170,0.5); } +.pipeline-node.done { border-color: var(--accent-green); box-shadow: 0 0 15px rgba(105,240,174,0.3); } +.pipeline-node.error { border-color: var(--accent-red); } +@keyframes glow-yellow { 0%,100% { box-shadow: 0 0 20px rgba(255,213,79,0.3); } 50% { box-shadow: 0 0 35px rgba(255,213,79,0.6); } } + +/* ── Agent Avatar ─────────────────────────────────────────── */ +.agent-avatar { + width: 56px; height: 56px; + border-radius: 50%; + display: flex; align-items: center; justify-content: center; + font-size: 1.8rem; + position: relative; +} +.avatar-ring { + position: absolute; inset: -3px; border-radius: 50%; + border: 2px solid; + opacity: 0.6; + animation: spin 4s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } +.pipeline-node.working .avatar-ring { border-color: var(--accent-yellow); } +.pipeline-node.done .avatar-ring { border-color: var(--accent-green); animation-duration: 8s; } +.pipeline-node.error .avatar-ring { border-color: var(--accent-red); animation-duration: 0.5s; } + +.agent-info { text-align: center; } +.agent-name { font-weight: 700; font-size: 1rem; } +.agent-role { font-size: 0.7rem; color: var(--text-secondary); display: block; } +.agent-status { + font-size: 0.7rem; + padding: 2px 10px; + border-radius: 20px; + background: rgba(255,255,255,0.1); +} +.agent-status.working { background: rgba(255,213,79,0.2); color: var(--accent-yellow); } +.agent-status.done { background: rgba(105,240,174,0.2); color: var(--accent-green); } +.agent-status.error { background: rgba(255,107,107,0.2); color: var(--accent-red); } +.agent-status.waiting { background: rgba(136,136,170,0.2); } +.agent-message { font-size: 0.65rem; color: var(--text-secondary); text-align: center; max-width: 100px; overflow: hidden; } + +/* ── Pipeline Controls ────────────────────────────────────── */ +.pipeline-controls { display: flex; gap: 10px; margin-top: 16px; justify-content: center; flex-wrap: wrap; } + +/* ── Decision Banner ─────────────────────────────────────── */ +.decision-banner { + margin-top: 12px; + padding: 10px 20px; + border-radius: 10px; + text-align: center; + font-weight: 700; + font-size: 1rem; +} +.decision-banner.buy { background: rgba(105,240,174,0.15); color: var(--accent-green); border: 1px solid var(--accent-green); } +.decision-banner.sell { background: rgba(255,107,107,0.15); color: var(--accent-red); border: 1px solid var(--accent-red); } +.decision-banner.hold { background: rgba(255,213,79,0.15); color: var(--accent-yellow); border: 1px solid var(--accent-yellow); } + +/* ── Office Section ───────────────────────────────────────── */ +.office-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.office-section h2 { margin-bottom: 16px; } +.office-floor { position: relative; } +.floor-grid { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; } +.agent-desk { + flex: 1; min-width: 140px; + padding: 16px; + background: var(--bg-card-hover); + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); + display: flex; flex-direction: column; align-items: center; gap: 8px; +} +.desk-icon { font-size: 2rem; } +.desk-status { font-size: 0.7rem; color: var(--text-secondary); text-align: center; } +.chat-bubble { + margin-top: 16px; padding: 12px 20px; + background: var(--bg-card-hover); + border-radius: 12px; border: 1px solid rgba(79,195,247,0.2); + font-size: 0.85rem; color: var(--text-secondary); +} + +/* ── Registry Section ─────────────────────────────────────── */ +.registry-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.registry-section h2 { margin-bottom: 12px; } +.registry-controls { display: flex; gap: 10px; margin-bottom: 16px; } +.agent-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; } +.agent-card { + padding: 16px; + background: var(--bg-card-hover); + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.05); + transition: all 0.2s; +} +.agent-card:hover { border-color: var(--accent-blue); transform: translateY(-2px); } +.agent-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } +.agent-card-emoji { font-size: 1.5rem; } +.agent-card-name { font-weight: 700; font-size: 1rem; } +.agent-card-role { font-size: 0.75rem; color: var(--text-secondary); } +.agent-card-intent { font-size: 0.8rem; color: var(--text-secondary); margin: 8px 0; line-height: 1.4; } +.agent-card-stats { display: flex; gap: 12px; font-size: 0.75rem; margin-top: 8px; } +.stat-badge { padding: 2px 8px; border-radius: 6px; background: rgba(255,255,255,0.05); } +.stat-badge.runs { color: var(--accent-blue); } +.stat-badge.ok { color: var(--accent-green); } +.stat-badge.fail { color: var(--accent-red); } +.agent-card-model { font-size: 0.65rem; color: var(--accent-purple); margin-top: 4px; font-family: monospace; } +.agent-card-actions { display: flex; gap: 6px; margin-top: 10px; } +.agent-card-actions button { flex: 1; padding: 5px; font-size: 0.7rem; border-radius: 6px; border: none; cursor: pointer; } + +/* ── Two Column Layout ────────────────────────────────────── */ +.two-col-section { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; } +@media (max-width: 900px) { .two-col-section { grid-template-columns: 1fr; } } + +/* ── Metrics Section ─────────────────────────────────────── */ +.metrics-section, .services-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.metrics-section h2, .services-section h2 { margin-bottom: 12px; } +.metrics-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; } +.metric-card { + padding: 14px; + background: var(--bg-card-hover); + border-radius: 10px; + border-left: 3px solid; +} +.metric-card.brief { border-left-color: var(--accent-blue); } +.metric-card.decio { border-left-color: var(--accent-yellow); } +.metric-card.papert { border-left-color: var(--accent-green); } +.metric-card.audit { border-left-color: var(--accent-purple); } +.metric-name { font-weight: 600; font-size: 0.85rem; margin-bottom: 6px; } +.metric-value { font-size: 1.2rem; font-weight: 700; } +.metric-sub { font-size: 0.7rem; color: var(--text-secondary); } +.metric-bar { height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 6px; overflow: hidden; } +.metric-bar-fill { height: 100%; border-radius: 2px; transition: width 0.5s; } + +/* ── Services Section ─────────────────────────────────────── */ +.services-controls { display: flex; gap: 10px; margin-bottom: 12px; } +.services-list { display: flex; flex-direction: column; gap: 8px; max-height: 300px; overflow-y: auto; } +.service-item { + display: flex; align-items: center; gap: 10px; + padding: 10px 14px; + background: var(--bg-card-hover); + border-radius: 8px; + font-size: 0.85rem; +} +.service-status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } +.service-status-dot.healthy { background: var(--accent-green); } +.service-status-dot.degraded { background: var(--accent-yellow); } +.service-status-dot.unknown { background: var(--text-secondary); } +.service-status-dot.unreachable { background: var(--accent-red); } +.service-name { font-weight: 600; flex: 1; } +.service-type { font-size: 0.7rem; color: var(--text-secondary); } +.service-endpoint { font-size: 0.7rem; color: var(--accent-purple); font-family: monospace; } +.service-actions { display: flex; gap: 4px; } +.service-actions button { padding: 3px 8px; font-size: 0.65rem; border-radius: 4px; border: none; cursor: pointer; background: rgba(255,255,255,0.1); color: var(--text-secondary); } +.service-actions button:hover { background: rgba(255,255,255,0.2); } + +/* ── Visual Pipeline ─────────────────────────────────────── */ +.visual-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.visual-section h2 { margin-bottom: 16px; } +.visual-container { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; } +@media (max-width: 900px) { .visual-container { grid-template-columns: 1fr; } } +.visual-flow { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.visual-node { + display: flex; flex-direction: column; align-items: center; gap: 4px; + padding: 12px 16px; + background: var(--bg-card-hover); + border-radius: 10px; + min-width: 90px; + position: relative; +} +.visual-node-label { font-size: 0.8rem; font-weight: 700; } +.visual-node-status { font-size: 0.65rem; color: var(--text-secondary); } +.visual-arrow { color: var(--text-secondary); font-size: 1.2rem; } +.visual-timeline { display: flex; flex-direction: column; gap: 4px; max-height: 200px; overflow-y: auto; } +.timeline-item { + padding: 8px 12px; + background: var(--bg-card-hover); + border-radius: 6px; + font-size: 0.75rem; + display: flex; justify-content: space-between; gap: 8px; +} +.timeline-ts { color: var(--text-secondary); flex-shrink: 0; } +.timeline-decision { font-weight: 700; } +.timeline-decision.BUY { color: var(--accent-green); } +.timeline-decision.SELL { color: var(--accent-red); } +.timeline-decision.HOLD { color: var(--accent-yellow); } + +/* ── Log Section ──────────────────────────────────────────── */ +.log-section { background: var(--bg-card); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--shadow); } +.log-section h2 { margin-bottom: 12px; } +.log-container { max-height: 200px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px; } +.log-entry { + display: flex; gap: 10px; align-items: baseline; + padding: 6px 10px; + background: var(--bg-card-hover); + border-radius: 6px; + font-size: 0.78rem; +} +.log-time { color: var(--text-secondary); flex-shrink: 0; font-variant-numeric: tabular-nums; } +.log-agent { font-weight: 700; min-width: 40px; color: var(--accent-blue); } +.log-message { color: var(--text-primary); flex: 1; } +.log-entry.system .log-agent { color: var(--accent-purple); } +.log-entry.error .log-agent { color: var(--accent-red); } + +/* ── Modals ──────────────────────────────────────────────── */ +.modal { + position: fixed; inset: 0; + background: rgba(0,0,0,0.7); + display: flex; align-items: center; justify-content: center; + z-index: 1000; + backdrop-filter: blur(4px); +} +.modal-content { + background: var(--bg-card); + border-radius: 16px; + padding: 24px; + width: 90%; max-width: 480px; + max-height: 90vh; + overflow-y: auto; + border: 1px solid rgba(79,195,247,0.2); + box-shadow: 0 20px 60px rgba(0,0,0,0.6); +} +.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } +.modal-header h3 { color: var(--text-primary); } +.modal-close { background: none; border: none; color: var(--text-secondary); font-size: 1.5rem; cursor: pointer; } +.modal-body { display: flex; flex-direction: column; gap: 10px; } +.modal-body label { font-size: 0.8rem; color: var(--text-secondary); font-weight: 600; } +.modal-body input, .modal-body textarea { + background: var(--bg-card-hover); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + padding: 10px 12px; + color: var(--text-primary); + font-family: inherit; + font-size: 0.9rem; + outline: none; +} +.modal-body input:focus, .modal-body textarea:focus { border-color: var(--accent-blue); } +.modal-footer { display: flex; gap: 10px; margin-top: 16px; justify-content: flex-end; } + +/* ── Scrollbar ───────────────────────────────────────────── */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: var(--bg-dark); } +::-webkit-scrollbar-thumb { background: var(--bg-card-hover); border-radius: 3px; } + +/* ── Portfolio Section ─────────────────────────────────────── */ +.portfolio-section { + background: var(--bg-card); + border-radius: var(--border-radius); + padding: 20px; + box-shadow: var(--shadow); + border: 1px solid rgba(105,240,174,0.2); +} +.portfolio-header { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 16px; +} +.portfolio-header h2 { margin: 0; flex: 1; } +.portfolio-summary { + display: flex; + gap: 20px; + align-items: center; + font-size: 0.85rem; +} +.portfolio-balance { font-size: 1.2rem; font-weight: 700; color: var(--accent-blue); } +.portfolio-pnl { font-weight: 600; } +.portfolio-trades { color: var(--text-secondary); } +.portfolio-actions { display: flex; gap: 8px; } +.portfolio-chart { + background: var(--bg-card-hover); + border-radius: 10px; + padding: 8px; + margin-bottom: 12px; +} +.portfolio-chart canvas { width: 100%; height: 180px; display: block; } +.portfolio-trades-list { max-height: 200px; overflow-y: auto; } +::-webkit-scrollbar-thumb:hover { background: #333; } + +/* ── Token Tracker ──────────────────────────────────────────── */ +.tokens-section { + background: rgba(20, 25, 50, 0.95); + border: 1px solid rgba(99, 110, 200, 0.3); + border-radius: 12px; + padding: 18px; + margin-bottom: 16px; +} +.tokens-controls { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 12px; +} +.tokens-controls label { color: #b0b8d0; font-size: 13px; } +.tokens-chart-wrap { + background: rgba(10, 12, 28, 0.8); + border-radius: 8px; + padding: 10px; + margin-bottom: 12px; +} +.tokens-table-wrap { overflow-x: auto; } +.tokens-table { width: 100%; border-collapse: collapse; font-size: 13px; color: #c8d0e8; } +.tokens-table th { background: rgba(99, 110, 200, 0.2); color: #a0aaff; padding: 8px 10px; text-align: left; } +.tokens-table td { padding: 6px 10px; border-bottom: 1px solid rgba(99, 110, 200, 0.1); } +.tokens-table tr:hover td { background: rgba(99, 110, 200, 0.08); } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..3166d48 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,213 @@ + + + + + + BrainSteel Fin — Agent Office + + + + +
+ +
+ +
+ + Sistema online +
+
+
+ + +
+

📊 Pipeline de Trading

+
+ +
+
+ + + +
+ +
+ + +
+

🏠 Escritório Virtual

+
+
+ +
+
+ Olá! Eu sou o escritório virtual da BrainSteel Fin. +
+
+
+ + +
+

👥 Cadastro de Agentes

+
+ + +
+
+ +
+
+ + +
+
+

📈 Métricas de Agentes

+
+ +
+
+
+

🔌 Serviços

+
+ + +
+
+ +
+
+
+ + +
+

🔭 Pipeline Visual em Tempo Real

+
+
+ +
+
+ +
+
+
+ + +
+
+

💹 Portfolio Simulado

+
+ $10.000,00 + P&L: $0,00 (0%) + 0 trades | 0% Win Rate +
+
+ +
+
+
+ +
+
+ +
+
+ + +
+

🧮 Tokens por Agente

+
+ + +
+
+ +
+
+ + + +
AgentePromptCompletionTotalChamadasCusto
+
+
+ + +
+

📝 Log de Atividades

+
+
+ --:-- + SYS + Aguardando primeira execução... +
+
+
+ + + + + + + +
+ + + + \ No newline at end of file