From f99f5a11d98fc4ae43be11e2324a944d464a3c14 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 10:02:14 +0800 Subject: [PATCH 1/9] init exchanges --- .../modals/create-strategy-modal.tsx | 284 +++++++++++++----- .../src/assets/png/exchanges/blockchain.png | Bin 0 -> 19784 bytes .../src/assets/png/exchanges/coinbase.png | Bin 0 -> 3468 bytes frontend/src/assets/png/exchanges/gate.png | Bin 0 -> 54514 bytes .../src/assets/png/exchanges/hyperliquid.png | Bin 0 -> 4768 bytes frontend/src/assets/png/exchanges/mexc.png | Bin 0 -> 23238 bytes frontend/src/assets/png/index.ts | 5 + frontend/src/constants/agent.ts | 8 +- frontend/src/constants/icons.ts | 11 + .../valuecell/agents/strategy_agent/core.py | 9 +- .../agents/strategy_agent/data/market.py | 44 ++- .../strategy_agent/execution/ccxt_trading.py | 39 ++- .../strategy_agent/execution/exchanges.py | 161 ++++++++++ .../strategy_agent/execution/factory.py | 28 +- .../valuecell/agents/strategy_agent/models.py | 8 + .../agents/strategy_agent/runtime.py | 25 +- 16 files changed, 514 insertions(+), 108 deletions(-) create mode 100644 frontend/src/assets/png/exchanges/blockchain.png create mode 100644 frontend/src/assets/png/exchanges/coinbase.png create mode 100644 frontend/src/assets/png/exchanges/gate.png create mode 100644 frontend/src/assets/png/exchanges/hyperliquid.png create mode 100644 frontend/src/assets/png/exchanges/mexc.png diff --git a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx index 60a9f6643..5a78a5db8 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx @@ -67,45 +67,70 @@ const step2Schema = z api_key: z.string(), secret_key: z.string(), passphrase: z.string(), // Required string, but can be empty for non-OKX exchanges + wallet_address: z.string().optional(), // For Hyperliquid + private_key: z.string().optional(), // For Hyperliquid }) .superRefine((data, ctx) => { // Only validate exchange credentials when live trading is selected if (data.trading_mode === "live") { - const fields = [ - { - name: "exchange_id", - label: "Exchange", - value: data.exchange_id, - }, - { - name: "api_key", - label: "API key", - value: data.api_key, - }, - { - name: "secret_key", - label: "Secret key", - value: data.secret_key, - }, - ]; - - for (const field of fields) { - if (!field.value?.trim()) { + // Hyperliquid uses different authentication + if (data.exchange_id === "hyperliquid") { + if (!data.wallet_address?.trim()) { ctx.addIssue({ code: "custom", - message: `${field.label} is required for live trading`, - path: [field.name], + message: "Wallet Address is required for Hyperliquid", + path: ["wallet_address"], }); } - } + if (!data.private_key?.trim()) { + ctx.addIssue({ + code: "custom", + message: "Private Key is required for Hyperliquid", + path: ["private_key"], + }); + } + } else { + // Standard exchanges require API key and secret + const fields = [ + { + name: "exchange_id", + label: "Exchange", + value: data.exchange_id, + }, + { + name: "api_key", + label: "API key", + value: data.api_key, + }, + { + name: "secret_key", + label: "Secret key", + value: data.secret_key, + }, + ]; + + for (const field of fields) { + if (!field.value?.trim()) { + ctx.addIssue({ + code: "custom", + message: `${field.label} is required for live trading`, + path: [field.name], + }); + } + } - // OKX requires passphrase - if (data.exchange_id === "okx" && !data.passphrase?.trim()) { - ctx.addIssue({ - code: "custom", - message: "Password is required for OKX", - path: ["passphrase"], - }); + // OKX and Coinbase require passphrase + if ( + (data.exchange_id === "okx" || + data.exchange_id === "coinbaseexchange") && + !data.passphrase?.trim() + ) { + ctx.addIssue({ + code: "custom", + message: `Passphrase is required for ${data.exchange_id === "okx" ? "OKX" : "Coinbase Exchange"}`, + path: ["passphrase"], + }); + } } } // Virtual trading mode: no validation needed for exchange fields @@ -240,6 +265,8 @@ const CreateStrategyModal: FC = ({ children }) => { api_key: "", secret_key: "", passphrase: "", + wallet_address: "", + private_key: "", }, validators: { onSubmit: step2Schema, @@ -513,6 +540,48 @@ const CreateStrategyModal: FC = ({ children }) => { Binance + +
+ + Blockchain.com +
+
+ +
+ + Coinbase Exchange +
+
+ +
+ + Gate.io +
+
+ +
+ + Hyperliquid +
+
+ +
+ + MEXC +
+
= ({ children }) => { )} - - {(field) => ( - - - API key - - - field.handleChange(e.target.value) - } - onBlur={field.handleBlur} - placeholder="Enter API Key" - /> - - - )} - - - - {(field) => ( - - - Secret Key - - - field.handleChange(e.target.value) - } - onBlur={field.handleBlur} - placeholder="Enter Secret Key" - /> - - + {/* Show different fields based on exchange type */} + + {(exchangeField) => ( + <> + {exchangeField.state.value === + "hyperliquid" ? ( + <> + {/* Hyperliquid: Wallet Address */} + + {(field) => ( + + + Wallet Address + + + field.handleChange( + e.target.value, + ) + } + onBlur={field.handleBlur} + placeholder="Enter API Wallet Address (0x...)" + /> + + + )} + + + {/* Hyperliquid: Private Key */} + + {(field) => ( + + + Private Key + + + field.handleChange( + e.target.value, + ) + } + onBlur={field.handleBlur} + placeholder="Enter API Wallet Private Key (0x...)" + /> + + + )} + + + ) : ( + <> + {/* Standard exchanges: API Key */} + + {(field) => ( + + + API key + + + field.handleChange( + e.target.value, + ) + } + onBlur={field.handleBlur} + placeholder="Enter API Key" + /> + + + )} + + + {/* Standard exchanges: Secret Key */} + + {(field) => ( + + + Secret Key + + + field.handleChange( + e.target.value, + ) + } + onBlur={field.handleBlur} + placeholder="Enter Secret Key" + /> + + + )} + + + )} + )} - {/* Password field - only shown for OKX */} + {/* Passphrase field - only shown for OKX and Coinbase Exchange */} {(exchangeField) => - exchangeField.state.value === "okx" && ( + (exchangeField.state.value === "okx" || + exchangeField.state.value === + "coinbaseexchange") && ( {(field) => ( @@ -580,7 +720,7 @@ const CreateStrategyModal: FC = ({ children }) => { field.handleChange(e.target.value) } onBlur={field.handleBlur} - placeholder="Enter Passphrase (Required for OKX)" + placeholder={`Enter Passphrase (Required for ${exchangeField.state.value === "okx" ? "OKX" : "Coinbase Exchange"})`} /> acW`(Fx@@9Uu2aZ%w*?WNlF7kxQ|@ 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 Pi3md*rmIn_W*Pb)MD)n2 literal 0 HcmV?d00001 diff --git a/frontend/src/assets/png/exchanges/hyperliquid.png b/frontend/src/assets/png/exchanges/hyperliquid.png new file mode 100644 index 0000000000000000000000000000000000000000..4a48daa34d9e95354fa6c91f71f19d7715027c7d GIT binary patch literal 4768 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 uCi~^uT0ZY`Gx+i8S~imE>*|H6?D;yl# zDH9ckBUz7w#&#l+ox<;WQ8Vw)_xHy<-jBD}dEKx3y07hdJ+JHT_h0nS@NV6`6(PiX z_UDsE2yJ2^gvW2@g1^K(bFhH_+2Z-Lxer47l(GMCr(%xU!5?uxMrX89L9@tv_&=MR zPUxLL=*it}U+g&%vamdR@`Ujf+y|CrjImRo^w?$XCm!>^e(>BTQbJ>Ja9_N1o9bh- z?Li504!v}GggmhY33|9j1~4LgbuEC4z`J9 zuT?h8g)J=3PKrsbsVvpbwVavj*R)t=t=%_0o_7b2b{cac>A{s}Y?(RxEJ^-Tll*6L zBYw%|KN~i`%9wqJX8S%z7z|pkjOR8+w6;zc8jK~6wi_aL?a0cY-kuyn_8`_bfU`tsa}OF3oPFZQ?6TNa$&^v^9}l6A_HCs6Q7 zHW%%67QAT!|1D=9pQ?H-k@&e~c{E_vNbF{4$347RW#+Sa&2C{Un!icz-jD6aZd6*V z<$cF3YoV<~KDw-Ir%Oz#d|j-OD_fwV&SRY$Cipkbs`>LD6V|H=^q_NLVqJNSW7Szh z{o?{8>8p4l{#0QaqdGaRO(ZgB*xuPHD#Anc+-mF3oo%amTu_}hbM)%@adouA@ zGrLMcI#ci4A7%Ot$i9tfw|9OtqBUC;&uu3ezYx>eU0BP%YH8AW+PpU1adAYQ+fD#p z2pN_w;iX5XwA-@-_x#=c3pWovD~e*?l}SJgv1+YUF7}~RVXfrNJ=77;0N%M%bCM{e z5y{mK+QhedRqGh zGT(PpoLk%2%@+Pv6`}i1J)3DgTbT7Eo3-Mw!@>A2X;6e>`9Beg97TN36*WrZKNFj9 zG!u|O@x8vwNT~vy;$nA77+0=lFKm0+cSpI zjvFQH^TnGTvKJoBTH;EKS`d63g~li){Smh(tUKL5N8H< zpp{T_L}^C)?mv4^23_@_y|!(Xzvb9P5p%x#i3_>@=!O(l^yA0ler~a?2m*@8KY3C&?a@^MQiIxvAUhA&i)wRh~ zA7}BCFTVV+>Dg{kQ+<23{NrwE{s3gL*B7rW!@ZCCEc${_PJ7zbtS8YACT9NN9{3(C zV)WhKlM4@@;dS1n^0EG!Z2BE9pyVPNf+i18lImS{e9Kj7F6l@~PR9=XW@{;+-}3rpb_8)>%j6Pa;Pl<| zvB|eU8V;J03l$gKY^(c&jr;_~JSCaWM~WXllfEuh%IeWAvxd>8OS-EgAzTBEu)VO7 z3-e>02R(fR(k89&zZJL?++TfkX0C7^zw)o9rVbUvas=m9=*NHA9xhxruv@_QY+D+s z;QrQLp*t?|EqZ-UNJF=cH>FkNs{R;%)Bz=Y-4IpWtxbQaCNxBZ-%qHZ&Z>naV$y)K z12GvqPeaSx%JlGc#7c3uI?_^V=wlxNA&p5nB<5AUU8s!s$6VzwA=$-lF`jc@Z4YUy z@S7$Nrnu&oUZFonLPi|-$hUBiGlgY;rGKGFJm;>@-h#6+=EvtXT-r9@c;#`OS+v^Y zekW0GyT7lJXp4D@4GPS&3mk*L?((6cyod$V@&ZlN7|N_JiTuNUS{Ja#GH(mTLB7!Ye48%81* ze^>r2ATPdsw+HQKXDY6)bSvK@&hi^8UyN3S1{(d(70cRd(gIwnaOq%+k!J_E+y$zh z#cu8$c)H1hHn4?7e)nDZiG)oaQ)f7ooNo>A3dzf{$oB9z=A*bzTw)a$L{GsxM+f0S zs<5nxs-p~tk{S0iVfpxsErgHU&iCOJ$xhvH`^AFZCRNLG_H-)+$0|4RYksP)=Xzh; z`-zal!9iC#)pl7Ab#CspW8LvNP&TtLMzA}-iBzZZqh3+SL8w_v>8+pssjv68^?KWO^A%8eio_4TXu|fGmjQJjpDxM;9g6L?# z++L)f^`2|C^K9r%8ZX6u$Cb$)ycZ&K1Bx!hOv=$RqkCJDrgc!DKAUT{Eos_0fD_jh z;}W{_XW3s)@1)HWju#ylW~-(OGc@U1<2j)mTiuv>t255?e06W+m7}D|1y`zif8l3b z&{j&Z^?&^+yo0eX_lTCZXXKz?{1NvyWX+JAT6)DHmQr|?q!pH@;i=BckRpgmaX0Jq zyBO*0ROqY@S;Yswnm0-jiyzmh9qn`$(=y#NYC-CDKIfsJH}>$t<|M~-afVJjNlWv~ zBO>Xoyq2k6jPNyg`HZ_JdY5Llq&CKTaS0wl>JVzE9+xYQg=%V*DF|QdmmIY}QEQfn zLOyi`E2S8~Ia2eN>`GsH;Fi;ePS_aV#fNxisU2(J4IDn)>2u_IyBAW?hqEjo49p+X zsx@BP*B_k_UQl^qo|D8-y^wz?MFxbpEEOa6I1{%_ymxt|vekn4v3%TZ4KQCJiPEfsvTb3vH;B%W4*LY5K`ak?BEeFkqT#!oB zkDMt%{^#Tx+y(76-|>+uoA&YB&gR$_XlEuliBmZbTZ&2Y1_KlNUT+ii#c!fQF(dw0 zT{E@LfSrCc3$at#EGu8!E3SC*X$Za4$fwuNS*;hR(l}?7q6*8t?fG&@=wRphZt2c* z_FYzToGL%VijnOUqi|s!)3%dWws#5s`-IKqmh7G(D;PwHd%B|_`nga!ajWkg25(o* zDUnD!HBraH^Bf6p)Vjn+u08+x;l8cDH{n@9y8?LyIUJ=6(@hj#mJ)?xN9M0ySJgVR znMIyB=D=N_H53Zz20u=;IefeI@47uHM<9^Q=ke*Zp}`sb&o_8wWwzI62b33E5^-I} zQiY55#2lh^(e)!PoAEdPVAp%IC5|=^!cHi^TJRG3%}eg`b-yQSIA_t1T@QKP;$RDE z*Z0{u#&7ibZ<}NHN2uLTcI) zt-3w3&0&T&c8Sx2n&eTK@v#3R(;4LUN2YMmaSIYHmL4N@p!{)rt!5hv*8g;TdzTtS zDY=qfvd1^ySC)}d8n8U~aRh@i!fEfh-0FYSIwE5$?I1`zXSpYm z#5G_UFXfXf)!^wL?dm{_yHbvKd%)l|)4aZK;KRWYO>gO3dRZGvymnoc#)ZAqBSn7~ zu$x@W)Z{f9ki1$#3&38%;_h-@%%LywBXzvArKTBOKh-G(uRDy~+SX z^=-6x2)5%-UN$1D=D%Z(*StJ`aNudYqixWOd>%V@@Q=?Xrq45R<&u^oi%-pwwA0u- zz9M}H9wE{5I7U3Nu{WL80LI(%5B&W!{N1`TV@q!)&bh^ArP@*O%H+8kel1n_K}S6} zje4X7eq63arhS6Eu-k4=PH*$fB~h{vR}^h;ULHF z$uVHbjXs+>6ko(tc;!xe;LODMFOy9wY_?kI&&W?LeAx{~d;I!R#~F&|Z`P(l1)Q zTEKPPjsxcr1{ZlDB@!5(AD7g8|00hGe*iegcKECyZ01&6bi&+tD5AiIK6#dI@2VPk zcL!`H1AA5xpUhEYO=*g{mYYFSG+ugEw`brOEVqe(QEI#Va0>+DP71>qcfSS1F6GYzKxHheDEg2{TxN=^tz-h zTseZx`{y3f;Ks3cTaHA!36dw`1IqDMR~fv@mDhBn80M5F-qx`{TyGp_aGu_UMi2Vp zn~8o?-TYdn-N%$p79-yiuk_$EGvKOD7n+_Tm6n)_cT1hf7c=GKV9Fj4eH{Wg7~HT@ zL!L&LAgJg9=)d_Cn=9Db=SqH)H$Jc~rXqDwm&65uwdDnPxYK@Le9g!G{bw3^**jrl zDHmms^$^LDUl^H}>J%Cu)lY`h3LB4z~54v02`0<%uGAU>~*#pE+ zF1rx#s{R?rK4LkttQ)b3O}&=@-{=R7T=RFEYRV-1KKF9WVWd9rj&F(=)@k|pV8!ea z;yDo=L}{A)B|3~(Ye8?iURe;skF>1#I+x)(ysXdihUzTJK);CU#je~tZJF_t6ZXuhV0*d`$b zctpq;=DOXtzc78G;{f4m3e8hqxLCb6-0%nLGogtGL68yb=FqPS{r99j0Tsub?YqL@ zbvOuHGv6bvpo;n|A6B;QYePI|vo$Y=nYs}p|NNf%tP3LIc{~kN<@fCTpn%Zh%ZkK0 zvn?#U^AP7&(9{{elT3fQnvz|ST5mY^!b3?B37kgm5b@)7)5)@IPz~-#z@==C+fWSE z|D)esXuH5c8&%NVA##I~b!-_4m1IYC%W7yV3Eg@)lk$zLVO`raCDD{(=}V4f5mHrH*PR{w}60@P=Jx?H+^SF4Kbrk#wec&Vbe?THty2rpR}2yuT2L zr;yc~zr#0^OS8RO>h=gJrV0Zm07ss}OUK`C4?d5anfq%K_HEUH15b6~yN=1zA=%Wh z$q`C&!RX8|Gq2TaF;J9@imC|BjnmCWA5qG#Np$?)ybw4lrc&hMh z_m47on&SC!{GYiiDDOIGe6zL^JGZ^9&;@nzai{qcj&DVidNVz}L<~>Q5*$15v=|5r z3%{a{zh0{g7pL}LB{9xaW*i^V!_)i~nCW|N^rXJ$nmTj8EyG32U<)h7Bfif1)d|Fo zyq2*w|4Qs280Tk2qD~n0NNs|H9Uq4`+$I6sg42%VwNyadj6=Cc&VR<>ne6G8^=@o2*xZY;_Z{-c_d+qDC!GDy z2;m3nmOiB6aj)}}CwJg!I5vjJKN7_vNQHDysm9dyCT}LPH5q;s)ym}(%R%e>(EX5iWuI?Efn|Bl zCWf3GciOf$@CxOhU=#v>mQIi=JUqco4iyjB-Q02>$bg4Q(&l&-s9CK26*M`?8u1<6 zX%U_p9y1as*fRPIoXG;5Nv?@OVJ0pm8L$)I$x*q&I~+9g^V2E|c;9#iFZJ>9zX(JH zG}{ZtB2!CFM^o;Q)rjwRGt5DTG?3vj$gqaV(72SC$O_v8-@RxbdDs{5CN(p_g5WKH zBHvkyUMns>j~h^PYl`IifqJ4-(&`k4kPvs;%;6Yul&I?!N-p@GdLlND&+#+P&g|y6 zrV}wjoEAX$&N9nEdmt1_dmY9L<+z0GRoUE6>9GqGLCpo`&xsC-81AM*K)E}-1=Y2? z#rf=2NjS&k9Y{PFu*Y4AEtD$kd!kq^_1G<(-N3x1rtn?y}l6>)7!V& zC1wI$v^ThWhv|8obe{Pe5X3Es!Alw55M*#&kmaYqq7Hn&E(iwr-E=TP8hP!8K#=Nz zTO;j;O@8?D@{y|6L{)M=Y)SEp$H!BRyySbIVqWe2fPTC=n8#P0iXm3C?Y@LG>52}~_G||+P5v_~a&y(r7gT}up zk##RA5-VNsmLLT^a64Ko&X)4Aoocp%C$2FPfS!d9!Ivu5!3eT78h(Utb*gH#N9B^j z#rE5-l8UzE4sYojLW{e((^}$8&lEi5C`svl$V${DM`2%GPcq21P2`3oVKH%SGm0%M zGR7$axid})j4u&G%D2)c=6B5?1{W+C0Sl70T|b1HUxc&oLUpk`cNkgOKxEg0#Z>ZR z4t?N8&FRMl%yXu)B{aFn5O{oUMs*kAw3W;bHU5CgAG+8ygtHwE1qh;^)7g3cS|_?CkkxxRlQpm;JCcHO98({a-i6ETC-;+rzdeKd+7BafWJnrKlw)Z znPIsdNpF1dS>^DKwn4k7qDaKMf=es1#6QoC+n^LZdiYDDYtf_aReXImoMZ~f(R6~`0dML>AJIa2 zFXRiQ*h@FAZAI=+j|z8av$@>6#TR}J;6UU5Xw5!5E83vVNmhjT;R>2lgj06ji;DE2 zW+SoRTuOa@7nhjLDra!&l_Sz;u2=zERD3(C*latYyw6J3f5cXD&RLmh%q|SHpNUbql8{CPJdA9Gc5Y2H5Yv9ND;VqCV z5#`do{+Yw_?n=ch)MMh^Z^T(Hk}7QV_m37NP`65!U*Qtpo_dT)y##n_M`Q zG@|vE;g2`lDO~(6@hZtv@YZmt(F6`ro5LS_{R&570Y@_Wqi=+Viv@|kj|(l+!ViOA zWCQ?jK!^=1fnt~D3FoT+Mv|b+hbAt?v_yqC zCROHklhWD8+~p5c<288x74;yZAk@gQRiof*LLLxDL$Q#Onuyy$faGNNR0!& z5s^BZ(O}V})&pYpa~6bSh6b$mgSOJc=mZzOeMpvz?^MnBTWiX<1?E3P2zCCS-_E4TlZ3x{PZ`vRpBZ2as{ax>7_8s-84c$K7 z`vn)-FkVHV9auCe0>U(Ef0kW;r8>KRb4f1}I>Mc{0Mhn!x;2Gv7Xo}V_*UfDIX(_F zsPLq^a=t3Qv+z2oJ4lfz=dZ0)|7sr|?FPRrEejCP-GO?X929>#IT_TNpP;KC%{brinzRK*z1JouO4RyP01 znX&Rv*@eBR#Qj$XMTY~v4r<_U(MROb)GkVxaq>dlP^9PreQ!@j55r?8!1Nd?)W7^KZ!wSP6uPpjdn~;R&1x-?q+kxB;Gd zksFshVvAV-CY=+ax^LfRM7?8*2VsRe*~4e=r5Fj3jMw)OgF`J>G$%s1YN#H`w zB!B8eP_O=Ud8qnU%z&7f0j2Nrb?7p=9tm<5=ZN>+aw*=k$93ayIqRhh2RqKs{=;mJrnHjVIsA9$PTvPKyLLQjwgNm-W5FDlYW?@@vsm63?Gm2N2^?_j|=DFV$CrSmG+lHP4ZqkLu=$VMN)R z@7bD(Lp>MT`Ah0C+oI3C;a+!qIWzdIL#f)c9YV-C8E-fkJ65!s2MK-RQnMHAN^9e> z{ce;JLil)GKuVnlO;`@O*xA8H>&66%#E_l!!MX64y>B6#gsvO~or9(8fbxU{3!*t_ zTpx}LwD(CD_K0$m{Q|6l|LTaj|g$43a_+F8ZhA4-5(D5m9Rpy z`Nms*1ph8Ds%_|cNJsE)Vf*w|P%Awb(y;S;d*s~z+=z48fPGpsUt?&J8+FC}4go(v zq0knb%elX)-fdYQ9!gCgMl=cp>%NhcP<0435~3d&9@a*FXl(>mHL%4152WZ4;be@i-B&BHy$fM4epedEq2~dQV}V+R^rTfL&;K`Q#G;h- zkqhGH^yyE(g?>+M6$4!J=r6q1!ujc*pjqsUFcZt}cfbf;pxo0n1sk)3+Yn;4S*@9? z?-i~D#}1j4Y)cQjN}8=3)A<6nUl9M6|D{`;YY*v(G0u+Htts;|_V9Q9Pl$mBKP)@` z*!C`7tMaDLzLg(+(?4V3KVe)FX)WxiD=>(afCN|KVUf(w&!KL}NhM`{5iIUSDVpa* zhfd8YJB-Tl&{RZ514%{3nkLLKjvJAHn- z9)0>~Qu_PTWgi4Vk#^2GUj~*9;tT6o5Jt}SndN9H!I$rVOoqH%WL$jt>V+V1<^gv| zB5{mtOT(fi?A_B5>|4h8`hErRK;$)v8hi;p+`CGcnh?V_-Wy8bG z32w=Zs0p)>mwo(5*5;NPy^Efku0k#`2BR!)%Lpk_fHxifjK!gP(g>U*#j@D=In9Gi ziQD%&W(%vUs=W;YF8a00?;GwoPZw}Z$f0FUJisVF)ORG{kDcv7_ICSn5o ztitw)V$KaB-Or(^C>VSf+ENHz59CDHFuO|D^wU)f07ePl`cP(adP*JRdLNd$b#H)j*nHjKsySnFNG-qiH65YrEOqq-jeacae1N z>xp57#-j<*XA3fy+#H+tXjR_vQCIRdbJ<=j7*8tkm@Bc{jmV7>J*2qK>(7YkN058M zkl#n{j+1Rs{tCsWKy;)EBSy?Y&p<=<=B|mD*tyPMp)UHSIju%{mUHLXn!7Y!G3x8xlAbdaD14<5N1d50~L zJwEhFJ5}ylb+i4;wR+bHljo%$6JOWI*Uixe0N*bl?QOz)S&;)Z) zAyk9}(!HyQj`wn1G=k!MxETe5RxME?Cc9shXtzZ*0uBC?Cmt!LI#)W{h;cPe$BUzh zcr3p-M`a4fwl$fUV+<{#Wy*)Q-5T?%CzVy@)e06vsWE8PnE%CU4<~dYq8gX_dvAf5 zU4KbvUMwuPh*RT%!+N;yoaI%Ll~tQ}t}#I5sDz;?DW9z3iBRlelJvH#y5CW~ytaw? z8Y657$Qp_ji_>rU^VG)>QhdoX+l$AR1jw`SjxSbz-o4nq*yx$(u{ zQ9n8AHPWG+ObN4EbIBtzS1^#;NQ&dEIl-=vm2yLP6qvK#jg|BFXmjh4n-X6CHkY=N zhQ~UJKp}I@_`ipZuB`*^%(;rC(19b8MguecuRV@7@*;MnM43UM`JK)@nAoeWcTy!& z_I%B~BTz;y76YEpcz{DX1IsO+DNIOHigG8HfR#+PkTUq?Pfiq>Rmg7|({nrXZ2D@EF!Wh~b8_3PmXL#awdzB}x)l!$bn6t+2hmgAvz! zMUl|sk>$I1!)Zls7p^coLmIxulPY}V^6StQ>P|#Xg}9KVy90}f0PDhxICp20r zV}upxduV+o_K*hm`wHL3s0S1_vfT0-g2S#ayjqp;tplk{v#<06nhGdblXov0Tpq_1 zOy_C+F6X(j8J-`wR{}{UcI)k=mOE(NGjY7)&$@HzSN6-Z>g~$p4cxPEklp}ZW_v3E zz`%$SsGZ!|>i{7EkJVNyS*;~2IZasRp$qKcPh~QsnhxTSa;yZ@p_kUtj(sHCc{3|A z&!s{^Tm`|H*|LGd|DqlHDM=%tV|Y&-l5RG-wrpkw@o7XZV?+${jkkK4Btb|~uPl|i z5s1r`JHz5BOs^HcE8t<>NyY95QS+QXMp{<@FjIaXzESt1?|5Zxj0*-3Xa~zz z+nk9^0zhrbgi~45^Szr{%lcdS5X>okbg69D_g1144g>uZ$%g7`+07*W`0md*WOqM4 zfC>fo%-CbTy~$Ye)Q>&z#z#?>o-Wr>~x}0ZtDUSs%DOlNK2BgefI6Uid;1 zmAqH(^gaRsH#HU(VM8|=5KvJFA+*TY{w$L>CFH(jlfZ;rs%5{tR2lR$5QFnVXV@jW zTcrdPdz5Mz!(tzRr;<&45U3wWEX44iZuq$Wa+}pP4lL@;$1w5@Oy19f`q=k==T3J4 za^PQJckEPP{g2l)y2a@#^8iQLkN%GibB|v}6DsRd3>k*KKgE+mo}4QMA4y4opx;Ak znCpB8mc{&=r**Z}cRYf4`sI%@FdT*0l=Uf%#{WMAVr+v&rl9#84i>yXSlSo#qW$#{ zq!mj!omm$)Z>nXdf>hbLYui!Ep-iX*Oo}J?Ft#8|R}ktV%g*kroQNR65Dmi3{%5LB6Ba8NW68jGG_rUqDrQE{OpCy4grHEE_eojQk!38wnL zsm0>I)bdeQudD!kZ?J9)(h95is!EuWfo$+_Uk}W7SifyT8yER|&Nr!6dMrb`7uA`Rx|kNT}-QR3Yf9N-`&D;)(@ z&ayic6s>WHM>jgCkvrF60rE|OHjY6#lY0Lu$@;0qBVZs<Ty{OW4NXvEgqJE*wkD;#@8Hbc zB?e9{>qfxlwLrji+Q%0{CetJ#ZyuaM2%N!;PWg2xGPif?cczGUtbD=g%eyKT$LhL;Lz>=3-08O z{+Cld{>)@jp#Sjc9S5?jdhH$8QBIG&{tUxD_p3f>6uZZhn2e?=vvnAY2LE3bRu&AO z#^Q~MGu6HB*EmJYrOOEc52}xC;m2!zh7!W5OYIk;|F+E#30I@nt?(Pq)wyjcKIRySMvS z3IhUrB|IsYkolgng?{_aM!cbQK2X8xk^gZgiMV!|ANUU)Y#SPg=E?#|(gYyzo%>6gkMKS3G5@Pb|0^z{oibi8MtXxYxy-JA zGfmgO-czJ2u}%e&E|84%G8`s>XYBW=YROB;WU9!^DGM$s&};vO>}Gg?e>yEN5sRcz z?l`lEBNfrLriITfoT>SsIl*nc2$d)1tXfr=1{bJh*PTH<3(|L=_iSeMP3L1|8>4!a}s>r?=8Zuu*;=(9XV zNsJ>SELLC0ADb)sIaRMC4v&hY38c{gg_f?RCYTJ{m|~N^Y5CXtju_S(NvzL>I#MNL z{r;1%M6WHMmwr3yRA^LG210okUPS6s@){LMMOqgwp6A<; zt%nqw{@&jZcnN7`$m4Di_MJzfH%h9>fbG(!+9t3L)UDw!n-S`IbdMXR`2MFv&LD({ z$CGwGnKJ$kk@+5vwPXan_%ZyeTs7NMnE{X%E@}dR2>GF|pbZ)zIkw4ImIk?=Un{o> zwBah{Knw+}bdib3T90|sXK2o=xRP<0xotGMHq+*-dRNs^lS2t1D)Il3sYXbztoRbq z@yjDyEPtikf_e#ZqSJf=E=8}QBE~Y^gOGeblWAW4s(Le+6}#X}>J~9EtnVWkt)>Yt zJI5T7jG5VoP)g;wJ<8SpkJdDH!cYDffgN%aRw_z&-8$xfr~>@ebm*tieZHS#%w0xK zK)r{k9zY0fM0J;Sz?-Fe@HoCxx7IOBzVjSpUUE%&jBvV`zxt8;R@QfJw0^-mcN!Lq zn(*ZgP7&fsw#DZ>uwQ9x5I9iOVjZpXr8zC9+a`3OmRpA)T4KBWtAEYrz-TfHQdA9L zg%@7+zBh?g{wcMh*MPv+`g|}(KNLJ}G|z%m3pccu^VUb1<@_O9ji(Y;;A_c0<#Qst z%{j|H6E;tN9{2`1PK8b@XqRdBNEKuKZ^Q`urZN1>LlA^vFyujSE8 z#i}u?mA~>454weMe{FtbV_tZ>GYCLvu^-DZHH z?mPA&@C5{6KWrj$cS!CsyxI48gM}c!3*5dN_C1ks@m*0qIvVXsa$cE7iOrqG993d{Qx{DL)b-TTrsEy+D_V5@kU#cGk@Xc{ z*yY`%Ftag^_lNwkCy^w*#(X2cO~~$TW~6neiF+*uL$S#AlVUT&-QM8`gx>O}g}ua7 zHsMWSvblkGD7dW}w#$}H*#H-IFc9`L#?XG{{~lVl4_f|JbRoWU0Q7~9bb32{s4I{I z8>AO{nSkhC1)Y;}*W1~0<|g87ug%B^M zYTEw@hF5H2xF0RpQ37F4kP!OPChj7}`|Ef5+baEvnO@ljnkbn_`Xtpzv7E*_ znML_y@8nrn)LwTn2v`4V?832sk>G1GPu6?N4Jb)bUL))V!GLbBg?q`Za3P zsPc6M{Kx!rDaqe>m1BGBHna5A{Gh5X&GnXno0(67zH$+rG>i5Xe%8kaA#wnt?0(`> z{jh8WQ85||W;{6r*4lq6OMu%f*l1||df90^D9P~+}G{5Hz}+_3mX=@*lbEc)1ukA3fUKzmtMg(G?TUTUm1zyekCn zOjwZfb+sV9f>Mg?zUyt#_%_SZ6|4u_56C@6blized1-({h?4ud`a|^D`j6jmk-7(A zLBTHfVIx%CKQ|SU3CZcj zv38iTM_M}->I$R?KkH-4Eu1Z!=(x+scd0l4B@?1vipP3X2{KMh^KVE<{mtF+46b_| zNK0(izsoOooilEhjSqvGFsI*%G&dZQ`B|^*YffcH^zl`a+M$v#!SF02*Tiojfn)*k zjUf{wk*C~<@^m~b+F9U7F(cyLz)EU`&3bmtSr}u9up`05?{Web9bR}t6^|H7J)|R* z!;X8jo54{7r!?AFnat;HMld2;x6SWgl4VL`K``GlN@zQvp z_wQi=HHk#Hocq3`DR@!67AxLSoqhK6o5Jd7oY8IKR~Aq5@RsSj3N~sMDElGsCkd>E zN)h)|KgaC{MLz@t$x#sqg*xdI6fA)68-f6y8!;)za-mA8t#BanGl>AI$RRoZb>gV? znTem0cA0sFdF@941423a{FQwVVI*N=62vj-7As2&1&{p*VuxsADwZw{dvjuGabF&4T6@r|RL(g90NLH7AA6ZZ z_b-V3_+Z51Js1V;;Rea8()Er7E;oOSuJ~`xL{**P_MJ&~-$2gr=-6RlN_Qr6qx*su&A212FJE29Ip%IzgzAb+ zX3p@uB`9n4gU3>aN>dk>dm02S+7nzsSJhyXHSDiaC5%XHM-x5Txq))2O#SW+!KMN% z_xQ7ReeFnK^idG0iTIyj$kuR6t8A7B~C$q61c~MOChRt z1*%xzwZz?Xmd!VRd?qm>&NqE~h;>jJ!XP5m{72i!&1do7iUj2s(-Z)gDSfRMbq z{G6b{x!#CS%$>{4TL+y1=0GT)o+({>RGcG+aP^r0Yh=F`IB-JfMcX!$_T1C zr+fWMav!K|BXjmel7(>`jDlAxmhBV&qqGu7d|g41J#>`n%D|?+aer@~`h(in`z)v= z!|cs^_GJFmT>Oa0+q3Ok0tI?F%fSsQ;rVZDgKi@{6?~)t@y*q!eKe~2*FS0Bx=9wv z`+D@*lyi8@D2qhf{P-$AZZMl0$Y4a@WiBK$2@*|)xve_*65MEV@A4~bS}(>EpP`-& zb>>8${xl4`>)zT1GUj#uZI*x$&+F4Oc5jHe%hP&FSRS#vpVO;hEg?(7f?5q78lM)f zmDUCCGocrc+$>uCRWpDPtvTCQGNa*Bk(q#{(<0t)RH1mwSmz|nEdZc&bh=?v_4PH~ zuX6`w$A9+Ov6bv!DyuX5PpW0c8`!TKy@YPxS*-x@w747V;;wutJ@2z+=QbAf1t5I^ zIq{S`?S%5z(V6jk?q#>Gkxa*wV6$+&rUV)X3nPuZLHxj=Nw@o}hOsG@MDdDU*i)wM zBg6WSv1799jg?!s?jBPPW1G0u!$GV3 z_$`2$%#NqTBpy9!B$ysEr$=__%) zk33Q)Kl2ee*Lj6zufCV8{UoVZ76#E%tM+_z+i9UZnCZX<;?6g#sLqb3GTj0Uu!0GN zTo~^27Vqs!8uD6!8Q~tov4vQ<^698GUm0uz@~?h`z_-yTijk8tUJPWLR9HYqWVWZz zyb#B0QEZ%wby@81DfQRvS{`P&`;{8Efl08-?ulvp&}Si>pIP!ZoM=pCwnGyVDm1v% z3GJnTxt!2a7+~1bU-j#9>39a*+k%V-^KIrMlRSM)k+(29IWS%0^UKR5uWlieC6(s`2)aS_S=o5M*~;IueUo1|DEhX-LVZj zuV--=|2&hLZ40xau?w2E%?u4ODgiEL!Rsx3)5Ml8m|&Qz@PGlbQC0xt#<;(Ri10A- z@zC`@5{FKp@;n*A&Q%erb*UCupHPW8uxqzjRe^+BiyY*6{JxCBW6jIev$+X7;jO^J z*XiGIndCz>@J~&V@Vk~&%i&AnkkYQ)uWH8LRVm=V#gC`G2(eevo_l4hFwQ$d-$|(jqWpFhLj^U{I(!Tu|2#B4^dhu6sB)3jz5a1y$GE;?w*HIDcJrYgrfP2uv6MtAhoKHuV#*cq0= z4T|0IGw&jhr%m#QbBh90xTOW+1NZ0n0mgS#t|V}iXC7XhPd!?_l|^g}PmP3uZuH=b zz`gsR#!;V%xwqhijAa{VjBsNy^?_x^$=DJCPc#GH9hXZ(h@4+ys z7gElB|JgQAy9}FtL5nZ3GFTCawV5=)F5N2*hVMT!=p#n3hLZX4em!~h=7>T=}IxAjEZjXAipr>>TYz;2d0tMZOS=9PWjMI~*?t;YC zYnc#gX``@xC^4{k7+P8Mf?nC=O)Lb%C0p$Nglyp*BPgmCsrSN2p;V(aZ6Hn2fNRc| zsZiPZ$4P&g=RiAaN}>Jpc!oA6v_~8^F##tZ5*aLnYCg!jR!*-z0wY7VttG5gi_Be> zJKG%qE!JG>sj|vmOJ3iDIm}GT1DL5HnqzVsNm?&J-9b-r>t-(>#%+!`KN8`F>221`C7W8XtuO|rvhu_;`vNKxCx~!_uZ=OZ?73^>(#F^0GlScgP^g*| z8`~G22L>#_s#cT}?gT(U+=kuIC=2jQ1X@bV-N-TP0k-jgjY)*{{t90|5hrw14?A7T z#yOE&f56r&ar&m!x$(O=p(h7@i{C(&N>RV74Xt{wzD=jkV|!s_{(Cyv)BDj+!-1d< zEoRP_KdO(t`WkA<->vTuu#*y42=oG#fl;zcEO={RcL$Cg7=w?ag^ux0f(+GjTasqb z?c3{mqki4bljBq)&5)%+*m@CPG4$o?KVqYmVLY@Oa9bJUb6fo_0~3hpFy?yt`FTu~ zx%d(Nud{&x-;=$RsrN2Fl85*62wzkC{q;dgSq@Wg0wzTf@qSfyL>UY{| zP+SCbPo9&gNQM^Zx2!BgS*yAC19r5u({SZXc}^DTm<-p(Ph`M97y{s+9xGU%U;}(G zjVo)_V5&d)c3U5O7#sYNs@5xRZ8zU8l$o=IWm2n*{Zmf~vh*ZxO^43yT5!LDCJ-%R zeSGCb80?5le>|--|9Zc}<$Dct7532R6 zT# zX9)}JLssCxTKkuht>9nS$f*H9Wo!bDg>z|4eZIz z6_j7;E$7fFNqC%S5Zt1(zfH}|;*uvnhHC}t!qv1|n2?_A% zQ}TmP!{nA;=w>~_5D6_0@K=6c5Ihc6hkbJ)w(QS!^K8kULB7^YChU3XHBop*nS`5W zLnoQM?C^VQi;FUAkXu_Z_h#Q%*8%i8#!Pj0kV%!xE}14F^GXvB*7W6c;7zi->T8+y zsN1Z(^J94TV&iA#JJ@N1%RzIRAzzdVg6GTP#xXzK(bg-@4Pe7B5lU=!=Gx+5|6(wV zePUtYvN7l-?cE%7buytsS4cKroX%iO$inKrFZO#(>p1gKAS$tD;u!hM|9JF?)2){v z~*>GTXw{m*h{Bxlf&{-&`il)aeN z&!I|#ux00`4{BaLNDdYtyDz=gzgl>mq-R|={TAd&@j3U=5qy01>4B}e{oW=pY6!ZQjvtBw_8b0n3qRFmlYtJv-xR7C zKZvsmx0|a_b1uZE<0GFd+2v zl1|x(%L(rhW7sh^m$;y|mDe5qk%r0U(9#mGRYv~1n^?N&oUqLKW1;xfjx*%OK>rTT zt&`T0zOFSc*=)?rEMM{O3rOIGtXzJ3EZ9_67>X?uV_(=2)jcNZX)Hu-agrNur;?^S z{ZZo!9VK@x1!!Ekn62F>!}-=9-&~kjJKRxzzAb;(-pi?LsjykMrVT5Tm%v2JsV6qx%hBJP%C4?8)$=CScQIm2O$@?ulz8 zdi5fdR`8znfi*#xWqM+GmgZDCTb+6?!M`zY)%JrUJY*Ma@YC8pcS6jQ9h`7x$FX2i zl2V;%2{+6@sh|6ZGF4d3HN7{-Yy28y8vR4q3^k?1*BmW6J78b-#Zso;}wlK5kGmkb26+1Ofa!OeM z>(Rq8vln*cdtWLu*&_PP9gf?rhJBPUS0D9M%k&DoYc}Xw%ap|<248)B+gm6j^L>wZ zWB!vG*i+j20U6#8Hr?bQ1{42>@6x(N+Qa6>oOnBgsq|b^h+eR?@hi#Tv5VdQa_J)V z|G1N!^x&aF?=2wx*qQgjwj@6_#?@bx>O9wP8O9#5QG@R(5k{yX9W!Go|6dD78VKdO zwj}$Zw8&gbNhwMlV^A*PwBVv-3E57^){M_Clidm3>)^yG$4o+s8%Bf~Mhxa0ZkUTK zQ^pueBy*8v$PDH@Q}^%pe&6>#`}@4lmYA^L_dgJm$2Ph0$NAQjOOXcVPeiPYvVDV& zANQQuqgtPv!NoNhVN|Hlz@b%L@YMt!h4CUgRR!0o%_OXO`rzHU*D=zNZ;O4xpyq^u z3bQG~2?e`IQk1rxh4DeE5wy?&K#%TNQ4DT(YgZR^^qbw-Xn4DU0bswaW$T5vtYHb* zvghW{B&|Omr8#H3|CNz}R34WS-E+lQZubz*8d!4>fP@%lX=J0$lBmR1`v!1Y|AM{) ziwv-QeGd>3^P&)>{7tpidg{zx zY6QxYQB-&lJ}IgSuxgR(m%bdmSl_T5z*1zZ%{GG!Kx_KS{Y|7lq^rjiv7f}+ku|AC z-ul=?3-~J8UPYtO%@9=)ft5c;!Ue)+R7hHkU%Ge@?Dycw5>^__dE+q`{gM|?@QN6q zdpLKXhteur_U`3X7q z)U(8s{-ekpU~F1Ql-h-3<*8FatW1@if5(AXxmQgfz~(lp`p25{(nI6?Fl-NVFZtH~ z#87;Ahh{Y79U4(!n`$7U9uka%ybqFN)s8xkHZ29R0aKbY^n8%;u;q;mA!>9Ghc-Gb zi>cHm+FF#eN<623+I-eF2q-0!Png#CH6DHU*kvN-*X;Skxd2@q(YCrKRn~^5Z9ebj z4llQn21H7tW{W?cDl$;(?`~4fD@|t{q24pES{lhpyoV^Zi*%1Ti^;EkZ!o-lLs=$7 zu)Nmi`jH3d%ZMDw(Ul^^f0{3ck}?6@nXdmxiK{t z{%zN?CPF?BTIk_oVV2FMb1l*squ)YakKL9eD3^qg=!Fq#aCi9a5epOay8YtX_hHyI zRVs2nW}TNJT2hannu6xCBT);qklPdiVQQuYrX zrMJ@U(0xdxnXlVL``7~)CY1igC@37dr*P@mS){VoPZG#L$5C7El%pqEr9eC16e}j_ z(Jo9Dg_yEzQYA@gj-H5loN_2&aG?cyLS`NtJA!s4*XkjX(cXrYTlp2HbV75N37V4+ zfhNo!ZKSjRD`Uk~mP;8-CxTd=aqvJAre_b(V_x4p0wpt##8ENutKq@aS;fvlppkEU z{TRQCaVnh~w7In21_Us%qHGYPGTDy7N44{{u-+3n)xZ7*R$19!fzUE zYcuU3?^LVXq!)!sW=C8V)@>c`ckGIm;GOBK>^k%yJRQk$FBod~TvZYX05gTbx-BQR zBK?e>u_J}(AenG&#>HWUQd`e5$5eBlYhp3Cwwx9WiumY%>VaI=pd<5oBT%^;Onai< z-F_v<`L>01RRAxRh-gN#!R)CLPAYZ6v?^y&RaH@VBs)rN?nx=1)7Q+~D8|FpZ~g3@ z4*;T{Nw2{$r=!M5Dwp;lI3c@GiaKpGKI@C8hrzJHBpCk)@iW%IirmzdmyubyBPez; zdtG|E@YA{u12qUI(Tpn`LdiRLtuRzUz0&1 zHAcjHEmaLj#+Vn~uM-n=aRRdvmiw17SdA(&MXDf#ob)Eau*bc0dPdQ*$D`GD4#uvE zKNPeEgNUm~)xRF~4>Q$_jLYQJ=uZA}5Bb=@FN?k^oEH%gSM;ZBd?CxTzTM?*qzct* z2q}2}Avj*dMDpc{&iAsMMpaK6gDZ0}?q7e$Ivh5=W&Vo;)lE2j^t1mg*(=0Q4^D!z_mfY)oR?hY z>_Y01{jKbmNxabTY;}|$iy*@uA<1&!lB{4G00=ZT9>)_UGkzSY5Iy|prjt8~B|m8G z$>HT#bKFL(=g24OB39R4q;!Lm7iR9Fp$fIdCmtwVS0Vd1Y58f4*v^rIpFoO7ERfk7 zimlFTmBVH3eWQ%FCu?%errdWH1#ik^*yLbJ@BH!Ig5@kwg)fFon@VL(b=1`D5icZUROAgW9rhy@lUDB`3B&q+vH8UEQ;oA~70Te!@!O4@I?D^hDFpw;W6Vu7$H2SrwDs?- zpT-QVC?&C)9WFh`W$a~)K_dIr83)>5ja@l+kbL$o<4{a=Cf6q|tmsA>PDl8HF_HxD z-(Gwx^gqLnS!4UXW`lauSOB`j)kN}?&hQiKKq zoGZ4loM*8~EcY9Gwu4t=z$oWNeZ~DjJ%S@|GPIDls{^)-jf_&hFnxogon7yp0gYDD zj92M2{UY6})JDCYB4TKekh5Y#?-tM~{08?3QFL@*rJ+Kp&wrCE^w@cQ&W!d-K^!x& z-4wmQ)1FIP?NgWgFfNU~yu6(P3Cb@*Rx?FK?1Hi!{r!;znv3>OT>r(RKLn+Gp&eZK zK2;iIE!pRbUv7M!S1|Lbi&Zz)+{x#urxj7)M&k01#h7nc3p$6?o=RQ%4nv DecisionCycleResult: self._request.exchange_config.trading_mode == TradingMode.LIVE and hasattr(self._execution_gateway, "fetch_balance") ): + logger.debug("Syncing portfolio balance from exchange in LIVE mode") balance = await self._execution_gateway.fetch_balance() free_map = {} free_section = ( @@ -163,12 +164,18 @@ async def run_once(self) -> DecisionCycleResult: else: for q in ("USDT", "USD", "USDC"): free_cash += float(free_map.get(q, 0.0) or 0.0) + logger.debug( + f"Synced balance from exchange: free_cash={free_cash}, quotes={quotes}" + ) portfolio.account_balance = float(free_cash) if self._request.exchange_config.market_type == MarketType.SPOT: portfolio.buying_power = max(0.0, float(portfolio.account_balance)) except Exception: # If syncing fails, continue with existing portfolio view - pass + logger.warning( + "Failed to sync balance from exchange in LIVE mode, using cached portfolio view", + exc_info=True, + ) # VIRTUAL mode: cash-only for spot; derivatives keep margin-based buying power if self._request.exchange_config.trading_mode == TradingMode.VIRTUAL: if self._request.exchange_config.market_type == MarketType.SPOT: diff --git a/python/valuecell/agents/strategy_agent/data/market.py b/python/valuecell/agents/strategy_agent/data/market.py index 6134d9baa..ec70f8d44 100644 --- a/python/valuecell/agents/strategy_agent/data/market.py +++ b/python/valuecell/agents/strategy_agent/data/market.py @@ -29,17 +29,41 @@ def __init__( self._exchange_id = exchange_id or "binance" self._ccxt_options = ccxt_options or {} + def _normalize_symbol(self, symbol: str) -> str: + """Normalize symbol format for specific exchanges. + + For Hyperliquid: converts BTC-USDC to BTC/USDC:USDC (swap format) + For other exchanges: converts BTC-USDC to BTC/USDC:USDC + + Args: + symbol: Symbol in format 'BTC-USDC', 'ETH-USDT', etc. + + Returns: + Normalized CCXT symbol for the specific exchange + """ + # Replace dash with slash + base_symbol = symbol.replace("-", "/") + + # For most exchanges (especially those requiring settlement currency) + if ":" not in base_symbol: + parts = base_symbol.split("/") + if len(parts) == 2: + # Add settlement currency (e.g., BTC/USDC -> BTC/USDC:USDC) + base_symbol = f"{parts[0]}/{parts[1]}:{parts[1]}" + + return base_symbol + async def get_recent_candles( self, symbols: List[str], interval: str, lookback: int ) -> List[Candle]: - async def _fetch(symbol: str) -> List[List]: + async def _fetch(symbol: str, normalized_symbol: str) -> List[List]: # instantiate exchange class by name (e.g., ccxtpro.kraken) exchange_cls = get_exchange_cls(self._exchange_id) exchange = exchange_cls({"newUpdates": False, **self._ccxt_options}) try: - # ccxt.pro uses async fetch_ohlcv + # ccxt.pro uses async fetch_ohlcv with normalized symbol data = await exchange.fetch_ohlcv( - symbol, timeframe=interval, since=None, limit=lookback + normalized_symbol, timeframe=interval, since=None, limit=lookback ) return data finally: @@ -52,7 +76,9 @@ async def _fetch(symbol: str) -> List[List]: # Run fetch for each symbol sequentially for symbol in symbols: try: - raw = await _fetch(symbol) + # Normalize symbol format for the exchange (e.g., BTC-USDC -> BTC/USDC:USDC) + normalized_symbol = self._normalize_symbol(symbol) + raw = await _fetch(symbol, normalized_symbol) # raw is list of [ts, open, high, low, close, volume] for row in raw: ts, open_v, high_v, low_v, close_v, vol = row @@ -73,12 +99,17 @@ async def _fetch(symbol: str) -> List[List]: ) ) except Exception as exc: - logger.error( - "Failed to fetch candles for {} from {}, return empty candles. Error: {}", + logger.warning( + "Failed to fetch candles for {} (normalized: {}) from {}, data interval is {}, return empty candles. Error: {}", symbol, + normalized_symbol, self._exchange_id, + interval, exc, ) + logger.debug( + f"Fetch candles for {len(candles)} symbols: {symbols}, interval: {interval}, lookback: {lookback}" + ) return candles async def get_market_snapshot(self, symbols: List[str]) -> MarketSnapShotType: @@ -193,6 +224,7 @@ async def get_market_snapshot(self, symbols: List[str]) -> MarketSnapShotType: symbol, self._exchange_id, ) + logger.debug(f"Fetch market snapshot for {sym} data: {snapshot}") except Exception: logger.exception( "Failed to fetch market snapshot for {} at {}", diff --git a/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py b/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py index f41a00120..8f2528899 100644 --- a/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py +++ b/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py @@ -41,9 +41,11 @@ class CCXTExecutionGateway(ExecutionGateway): def __init__( self, exchange_id: str, - api_key: str, - secret_key: str, + api_key: str = "", + secret_key: str = "", passphrase: Optional[str] = None, + wallet_address: Optional[str] = None, + private_key: Optional[str] = None, testnet: bool = False, default_type: str = "swap", margin_mode: str = "cross", @@ -53,10 +55,12 @@ def __init__( """Initialize CCXT exchange gateway. Args: - exchange_id: Exchange identifier (e.g., 'binance', 'okx', 'bybit') - api_key: API key for authentication - secret_key: Secret key for authentication - passphrase: Optional passphrase (required for OKX) + exchange_id: Exchange identifier (e.g., 'binance', 'okx', 'bybit', 'hyperliquid') + api_key: API key for authentication (not required for Hyperliquid) + secret_key: Secret key for authentication (not required for Hyperliquid) + passphrase: Optional passphrase (required for OKX, Coinbase Exchange) + wallet_address: Wallet address (required for Hyperliquid) + private_key: Private key (required for Hyperliquid) testnet: Whether to use testnet/sandbox mode default_type: Default market type ('spot', 'future', 'swap', "margin") margin_mode: Default margin mode ('isolated' or 'cross') @@ -67,6 +71,8 @@ def __init__( self.api_key = api_key self.secret_key = secret_key self.passphrase = passphrase + self.wallet_address = wallet_address + self.private_key = private_key self.testnet = testnet self.default_type = default_type self.margin_mode = margin_mode @@ -104,10 +110,8 @@ async def _get_exchange(self) -> ccxt.Exchange: f"Available: {', '.join(ccxt.exchanges)}" ) - # Build configuration + # Build configuration based on exchange type config = { - "apiKey": self.api_key, - "secret": self.secret_key, "enableRateLimit": True, # Respect rate limits "options": { "defaultType": self._choose_default_type_for_exchange(), @@ -115,9 +119,20 @@ async def _get_exchange(self) -> ccxt.Exchange: }, } - # Add passphrase if provided (required for OKX) - if self.passphrase: - config["password"] = self.passphrase + # Hyperliquid uses wallet-based authentication + if self.exchange_id == "hyperliquid": + if self.wallet_address: + config["walletAddress"] = self.wallet_address + if self.private_key: + config["privateKey"] = self.private_key + else: + # Standard API key/secret authentication + config["apiKey"] = self.api_key + config["secret"] = self.secret_key + + # Add passphrase if provided (required for OKX, Coinbase Exchange) + if self.passphrase: + config["password"] = self.passphrase # Create exchange instance self._exchange = exchange_class(config) diff --git a/python/valuecell/agents/strategy_agent/execution/exchanges.py b/python/valuecell/agents/strategy_agent/execution/exchanges.py index e69de29bb..ad7f4beec 100644 --- a/python/valuecell/agents/strategy_agent/execution/exchanges.py +++ b/python/valuecell/agents/strategy_agent/execution/exchanges.py @@ -0,0 +1,161 @@ +"""Exchange metadata and special configurations for CCXT. + +This module provides metadata about supported exchanges, including +authentication requirements and special handling notes. +""" + +from __future__ import annotations + +from typing import Dict, List, Optional + +from pydantic import BaseModel + + +class ExchangeMetadata(BaseModel): + """Metadata for a supported exchange. + + Attributes: + id: CCXT exchange identifier + name: Display name + requires_passphrase: Whether the exchange requires a passphrase/password + requires_api_key: Whether the exchange requires an API key + requires_secret: Whether the exchange requires a secret key + special_auth: Special authentication requirements (e.g., privateKey, walletAddress) + testnet_supported: Whether testnet/sandbox mode is supported + notes: Additional notes or warnings + """ + + id: str + name: str + requires_passphrase: bool = False + requires_api_key: bool = True + requires_secret: bool = True + special_auth: Optional[List[str]] = None + testnet_supported: bool = False + notes: Optional[str] = None + + +# Supported exchanges metadata +SUPPORTED_EXCHANGES: Dict[str, ExchangeMetadata] = { + "binance": ExchangeMetadata( + id="binance", + name="Binance", + testnet_supported=True, + notes="Most popular exchange with comprehensive CCXT support", + ), + "okx": ExchangeMetadata( + id="okx", + name="OKX", + requires_passphrase=True, + testnet_supported=True, + notes="Requires passphrase for authentication", + ), + "blockchaincom": ExchangeMetadata( + id="blockchaincom", + name="Blockchain.com", + requires_api_key=False, + notes="Only requires secret key (no API key needed)", + ), + "coinbaseexchange": ExchangeMetadata( + id="coinbaseexchange", + name="Coinbase Exchange", + requires_passphrase=True, + testnet_supported=True, + notes="Main Coinbase exchange (not Coinbase International). Requires passphrase.", + ), + "gate": ExchangeMetadata( + id="gate", + name="Gate.io", + testnet_supported=True, + notes="Gate.io main exchange with standard API key/secret authentication", + ), + "hyperliquid": ExchangeMetadata( + id="hyperliquid", + name="Hyperliquid", + requires_api_key=False, + requires_secret=False, + special_auth=["privateKey", "walletAddress"], + notes="Uses wallet-based authentication (privateKey + walletAddress). Not standard API key/secret.", + ), + "mexc": ExchangeMetadata( + id="mexc", + name="MEXC Global", + testnet_supported=False, + notes="MEXC main exchange with standard API key/secret authentication", + ), +} + + +def get_exchange_metadata(exchange_id: str) -> Optional[ExchangeMetadata]: + """Get metadata for a specific exchange. + + Args: + exchange_id: CCXT exchange identifier + + Returns: + ExchangeMetadata if exchange is supported, None otherwise + """ + return SUPPORTED_EXCHANGES.get(exchange_id.lower()) + + +def requires_passphrase(exchange_id: str) -> bool: + """Check if an exchange requires a passphrase. + + Args: + exchange_id: CCXT exchange identifier + + Returns: + True if passphrase is required, False otherwise + """ + metadata = get_exchange_metadata(exchange_id) + return metadata.requires_passphrase if metadata else False + + +def get_supported_exchange_ids() -> List[str]: + """Get list of supported exchange IDs. + + Returns: + List of CCXT exchange identifiers + """ + return list(SUPPORTED_EXCHANGES.keys()) + + +def validate_exchange_credentials( + exchange_id: str, + api_key: Optional[str] = None, + secret_key: Optional[str] = None, + passphrase: Optional[str] = None, +) -> tuple[bool, Optional[str]]: + """Validate that provided credentials match exchange requirements. + + Args: + exchange_id: CCXT exchange identifier + api_key: API key + secret_key: Secret key + passphrase: Passphrase/password + + Returns: + Tuple of (is_valid, error_message) + """ + metadata = get_exchange_metadata(exchange_id) + if not metadata: + return False, f"Exchange '{exchange_id}' is not supported" + + # Check for special authentication requirements + if metadata.special_auth: + return ( + False, + f"Exchange '{exchange_id}' requires special authentication: {', '.join(metadata.special_auth)}", + ) + + # Check standard credentials + if metadata.requires_api_key and not api_key: + return False, f"Exchange '{exchange_id}' requires an API key" + + if metadata.requires_secret and not secret_key: + return False, f"Exchange '{exchange_id}' requires a secret key" + + if metadata.requires_passphrase and not passphrase: + return False, f"Exchange '{exchange_id}' requires a passphrase" + + return True, None diff --git a/python/valuecell/agents/strategy_agent/execution/factory.py b/python/valuecell/agents/strategy_agent/execution/factory.py index 06c12dbdf..62606f2f8 100644 --- a/python/valuecell/agents/strategy_agent/execution/factory.py +++ b/python/valuecell/agents/strategy_agent/execution/factory.py @@ -35,21 +35,33 @@ async def create_execution_gateway(config: ExchangeConfig) -> ExecutionGateway: if not config.exchange_id: raise ValueError( "exchange_id is required for live trading mode. " - "Please specify an exchange (e.g., 'binance', 'okx', 'bybit')" + "Please specify an exchange (e.g., 'binance', 'okx', 'bybit', 'hyperliquid')" ) - if not config.api_key or not config.secret_key: - raise ValueError( - f"API credentials are required for live trading on {config.exchange_id}. " - "Please provide api_key and secret_key in ExchangeConfig." - ) + # Validate credentials based on exchange type + if config.exchange_id.lower() == "hyperliquid": + # Hyperliquid requires wallet_address and private_key + if not config.wallet_address or not config.private_key: + raise ValueError( + "Hyperliquid requires wallet_address and private_key. " + "Please provide both in ExchangeConfig." + ) + else: + # Standard exchanges require api_key and secret_key + if not config.api_key or not config.secret_key: + raise ValueError( + f"API credentials are required for live trading on {config.exchange_id}. " + "Please provide api_key and secret_key in ExchangeConfig." + ) # Create CCXT gateway with full configuration gateway = CCXTExecutionGateway( exchange_id=config.exchange_id, - api_key=config.api_key, - secret_key=config.secret_key, + api_key=config.api_key or "", + secret_key=config.secret_key or "", passphrase=config.passphrase, + wallet_address=config.wallet_address, + private_key=config.private_key, testnet=config.testnet, default_type=config.market_type.value, margin_mode=config.margin_mode.value, diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index 08468a467..36d49f254 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -155,6 +155,14 @@ class ExchangeConfig(BaseModel): default=None, description="API passphrase (required for some exchanges like OKX)", ) + wallet_address: Optional[str] = Field( + default=None, + description="Wallet address (required for Hyperliquid)", + ) + private_key: Optional[str] = Field( + default=None, + description="Private key (required for Hyperliquid)", + ) testnet: bool = Field( default=False, description="Use testnet/sandbox mode for testing" ) diff --git a/python/valuecell/agents/strategy_agent/runtime.py b/python/valuecell/agents/strategy_agent/runtime.py index d48d8f50f..e492258c8 100644 --- a/python/valuecell/agents/strategy_agent/runtime.py +++ b/python/valuecell/agents/strategy_agent/runtime.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from typing import Optional +from loguru import logger + from valuecell.utils.uuid import generate_uuid from .core import DecisionCycleResult, DefaultDecisionCoordinator @@ -168,7 +170,9 @@ async def create_strategy_runtime_async(request: UserRequest) -> StrategyRuntime if request.exchange_config.trading_mode == TradingMode.LIVE and hasattr( execution_gateway, "fetch_balance" ): + logger.info("Fetching exchange balance for LIVE trading mode") balance = await execution_gateway.fetch_balance() + logger.info(f"Raw balance response: {balance}") free_map = {} # ccxt balance may be shaped as: {'free': {...}, 'used': {...}, 'total': {...}} try: @@ -189,6 +193,7 @@ async def create_strategy_runtime_async(request: UserRequest) -> StrategyRuntime free_map[str(k).upper()] = float(v.get("free") or 0.0) except Exception: continue + logger.info(f"Parsed free balance map: {free_map}") # collect quote currencies from configured symbols quotes: list[str] = [] for sym in request.trading_config.symbols or []: @@ -202,6 +207,7 @@ async def create_strategy_runtime_async(request: UserRequest) -> StrategyRuntime if len(parts) == 2: quotes.append(parts[1]) quotes = list(dict.fromkeys(quotes)) # unique order-preserving + logger.info(f"Quote currencies from symbols: {quotes}") free_cash = 0.0 if quotes: for q in quotes: @@ -211,10 +217,25 @@ async def create_strategy_runtime_async(request: UserRequest) -> StrategyRuntime for q in ("USDT", "USD", "USDC"): free_cash += float(free_map.get(q, 0.0) or 0.0) # Set initial capital to exchange free cash + logger.info( + f"Setting initial_capital to {free_cash} (from exchange balance)" + ) request.trading_config.initial_capital = float(free_cash) except Exception: - # Do not fail runtime creation if balance fetch or parsing fails - pass + # Log the error but continue - user might have set initial_capital manually + logger.exception( + "Failed to fetch exchange balance for LIVE mode. Will use configured initial_capital instead." + ) + + # Validate initial capital for LIVE mode + if request.exchange_config.trading_mode == TradingMode.LIVE: + initial_cap = request.trading_config.initial_capital or 0.0 + if initial_cap <= 0: + logger.error( + f"LIVE trading mode has initial_capital={initial_cap}. " + "This usually means balance fetch failed or account has no funds. " + "Strategy will not be able to trade without capital." + ) # Use the sync function with the pre-initialized gateway return create_strategy_runtime(request, execution_gateway=execution_gateway) From 6c4dc8198f504b6201c96a1956108ea5580088f9 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 17:54:59 +0800 Subject: [PATCH 2/9] fix hyperliquid trade --- .../strategy_agent/execution/ccxt_trading.py | 126 ++++++++++++++++-- 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py b/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py index 8f2528899..293513cd3 100644 --- a/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py +++ b/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py @@ -125,6 +125,11 @@ async def _get_exchange(self) -> ccxt.Exchange: config["walletAddress"] = self.wallet_address if self.private_key: config["privateKey"] = self.private_key + # Disable builder fees by default (can be overridden in ccxt_options) + if "builderFee" not in config["options"]: + config["options"]["builderFee"] = False + if "approvedBuilderFee" not in config["options"]: + config["options"]["approvedBuilderFee"] = False else: # Standard API key/secret authentication config["apiKey"] = self.api_key @@ -272,14 +277,16 @@ def _build_order_params(self, inst: TradeInstruction, order_type: str) -> Dict: exid = self.exchange_id # Idempotency / client order id (sanitize for OKX) - raw_client_id = params.get("clientOrderId", inst.instruction_id) - if raw_client_id: - client_id = ( - self._sanitize_client_order_id(raw_client_id) - if exid == "okx" - else raw_client_id - ) - params["clientOrderId"] = client_id + # Hyperliquid doesn't support clientOrderId + if exid != "hyperliquid": + raw_client_id = params.get("clientOrderId", inst.instruction_id) + if raw_client_id: + client_id = ( + self._sanitize_client_order_id(raw_client_id) + if exid == "okx" + else raw_client_id + ) + params["clientOrderId"] = client_id # Default tdMode for OKX on all orders if exid == "okx": @@ -299,6 +306,9 @@ def _build_order_params(self, inst: TradeInstruction, order_type: str) -> Dict: params.setdefault("reduceOnly", False) elif exid == "bybit": params.setdefault("reduce_only", False) + elif exid == "hyperliquid": + # Hyperliquid only uses 'reduceOnly' (not 'reduce_only') + params.setdefault("reduceOnly", False) # Enforce single-sided mode: strip positionSide/posSide if present try: @@ -795,11 +805,69 @@ async def _submit_order( except Exception: pass + # Hyperliquid special handling for market orders + # Hyperliquid doesn't have true market orders; use IoC (Immediate or Cancel) to simulate + if self.exchange_id == "hyperliquid" and order_type == "market": + try: + logger.debug( + " 📊 Hyperliquid: Converting market order to IoC limit order" + ) + + # Fetch current market price + if price is None: + ticker = await exchange.fetch_ticker(symbol) + price = float(ticker.get("last") or ticker.get("close") or 0.0) + + if price > 0: + # Calculate slippage price based on direction + slippage_pct = ( + inst.max_slippage_bps or 50.0 + ) / 10000.0 # default 50 bps = 0.5% + if side == "buy": + # For buy orders, set price higher to ensure execution + price = price * (1 + slippage_pct) + else: + # For sell orders, set price lower to ensure execution + price = price * (1 - slippage_pct) + + # Apply CCXT price precision (critical for Hyperliquid) + # This handles both integer prices (BTC) and decimal prices (WIF, ENA, etc.) + try: + price = float(exchange.price_to_precision(symbol, price)) + logger.debug(f" 🔢 Price after precision: {price}") + except Exception as e: + logger.warning(f" ⚠️ Could not apply price precision: {e}") + # Fallback: try integer conversion for high-value assets + try: + market = (getattr(exchange, "markets", {}) or {}).get( + symbol + ) or {} + price_precision = market.get("precision", {}).get("price") + if price_precision is not None and price_precision == 0: + price = float(int(price)) + logger.debug(f" 🔢 Fallback: rounded to integer {price}") + except Exception: + pass + + # Use IoC (Immediate or Cancel) to simulate market execution + params["timeInForce"] = "Ioc" + logger.debug( + f" 💰 Using IoC limit order: {side} @ {price} (slippage: {slippage_pct:.2%})" + ) + else: + logger.warning( + f" ⚠️ Could not determine market price for {symbol}, will try without price" + ) + except Exception as e: + logger.warning(f" ⚠️ Could not setup Hyperliquid market order: {e}") + # Fallback: let exchange handle it + # Create order try: logger.info( f" 🔨 Creating {order_type} order: {side} {amount} {symbol} @ {price if price else 'market'}" ) + logger.debug(f" 📋 Order params: {params}") order = await exchange.create_order( symbol=symbol, type=order_type, @@ -812,8 +880,22 @@ async def _submit_order( f" ✓ Order created: id={order.get('id')}, status={order.get('status')}, filled={order.get('filled')}" ) except Exception as e: - logger.error(f" ❌ ERROR creating order for {symbol}: {e}") - raise RuntimeError(f"Failed to create order for {symbol}: {e}") from e + error_msg = str(e) + logger.error(f" ❌ ERROR creating order for {symbol}: {error_msg}") + logger.error(f" 📋 Failed order details: side={side}, amount={amount}, price={price}, type={order_type}") + logger.error(f" 📋 Failed order params: {params}") + + # Return error result instead of raising to allow other orders to proceed + return TxResult( + instruction_id=inst.instruction_id, + instrument=inst.instrument, + side=local_side, + requested_qty=amount, + filled_qty=0.0, + status=TxStatus.ERROR, + reason=f"create_order_failed: {error_msg}", + meta=inst.meta, + ) # For market orders, wait for fill and fetch final order status # Many exchanges don't immediately return filled quantities for market orders @@ -885,26 +967,42 @@ async def _exec_open_long( self, inst: TradeInstruction, exchange: ccxt.Exchange ) -> TxResult: # Ensure we do not mark reduceOnly on open - overrides = {"reduceOnly": False, "reduce_only": False} + # Use exchange-specific param name + if self.exchange_id == "bybit": + overrides = {"reduce_only": False} + else: + overrides = {"reduceOnly": False} return await self._submit_order(inst, exchange, overrides) async def _exec_open_short( self, inst: TradeInstruction, exchange: ccxt.Exchange ) -> TxResult: - overrides = {"reduceOnly": False, "reduce_only": False} + # Use exchange-specific param name + if self.exchange_id == "bybit": + overrides = {"reduce_only": False} + else: + overrides = {"reduceOnly": False} return await self._submit_order(inst, exchange, overrides) async def _exec_close_long( self, inst: TradeInstruction, exchange: ccxt.Exchange ) -> TxResult: # Force reduceOnly flags for closes - overrides = {"reduceOnly": True, "reduce_only": True} + # Use exchange-specific param name + if self.exchange_id == "bybit": + overrides = {"reduce_only": True} + else: + overrides = {"reduceOnly": True} return await self._submit_order(inst, exchange, overrides) async def _exec_close_short( self, inst: TradeInstruction, exchange: ccxt.Exchange ) -> TxResult: - overrides = {"reduceOnly": True, "reduce_only": True} + # Use exchange-specific param name + if self.exchange_id == "bybit": + overrides = {"reduce_only": True} + else: + overrides = {"reduceOnly": True} return await self._submit_order(inst, exchange, overrides) async def _exec_noop(self, inst: TradeInstruction) -> TxResult: From 0c487553a6f28eec8f972b8dc6da0d6edeb50d2e Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 19:18:01 +0800 Subject: [PATCH 3/9] fix wrong portfolio number --- .../valuecell/agents/strategy_agent/core.py | 51 +++++++++++++++---- .../strategy_agent/portfolio/in_memory.py | 29 ++++++++--- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py index 009959d44..6c0ccac44 100644 --- a/python/valuecell/agents/strategy_agent/core.py +++ b/python/valuecell/agents/strategy_agent/core.py @@ -159,19 +159,54 @@ async def run_once(self) -> DecisionCycleResult: quotes.append(s.split("-")[1]) # Deduplicate preserving order quotes = list(dict.fromkeys(quotes)) + free_cash = 0.0 + total_cash = 0.0 + + # Sum up free and total cash from relevant quote currencies if quotes: for q in quotes: free_cash += float(free_map.get(q, 0.0) or 0.0) + # Try to find total/equity in balance if available (often 'total' dict in CCXT) + # Hyperliquid/CCXT structure: balance[q]['total'] + q_data = balance.get(q) + if isinstance(q_data, dict): + total_cash += float(q_data.get("total", 0.0) or 0.0) + else: + # Fallback if structure is flat or missing + total_cash += float(free_map.get(q, 0.0) or 0.0) else: for q in ("USDT", "USD", "USDC"): free_cash += float(free_map.get(q, 0.0) or 0.0) + q_data = balance.get(q) + if isinstance(q_data, dict): + total_cash += float(q_data.get("total", 0.0) or 0.0) + else: + total_cash += float(free_map.get(q, 0.0) or 0.0) + logger.debug( - f"Synced balance from exchange: free_cash={free_cash}, quotes={quotes}" + f"Synced balance from exchange: free_cash={free_cash}, total_cash={total_cash}, quotes={quotes}" ) - portfolio.account_balance = float(free_cash) + if self._request.exchange_config.market_type == MarketType.SPOT: + # Spot: Account Balance is Cash (Free). Buying Power is Cash. + portfolio.account_balance = float(free_cash) portfolio.buying_power = max(0.0, float(portfolio.account_balance)) + else: + # Derivatives: Account Balance should be Wallet Balance or Equity. + # We use total_cash (Equity) as the best approximation for account_balance + # to ensure InMemoryPortfolioService calculates Equity correctly (Equity + Unrealized). + # Note: If total_cash IS Equity, adding Unrealized PnL again in InMemoryService + # (Equity = Balance + Unreal) would double count PnL. + # However, separating Wallet Balance from Equity is exchange-specific. + # For now, we set account_balance = total_cash and rely on the fixed + # InMemoryPortfolioService to handle it (assuming Balance ~= Equity for initial sync). + portfolio.account_balance = float(total_cash) + # Buying Power is explicit Free Margin + portfolio.buying_power = float(free_cash) + # Also update free_cash field in view if it exists + portfolio.free_cash = float(free_cash) + except Exception: # If syncing fails, continue with existing portfolio view logger.warning( @@ -496,15 +531,9 @@ def _build_summary( try: view = self._portfolio_service.get_view() unrealized = float(view.total_unrealized_pnl or 0.0) - # In LIVE mode, treat equity as available cash (disallow financing) - try: - mode = getattr(self._request.exchange_config, "trading_mode", None) - except Exception: - mode = None - if str(mode).upper() == "LIVE": - equity = float(getattr(view, "cash", None) or 0.0) - else: - equity = float(view.total_value or 0.0) + # Use the portfolio view's total_value which now correctly reflects Equity + # (whether simulated or synced from exchange) + equity = float(view.total_value or 0.0) except Exception: # Fallback to internal tracking if portfolio service is unavailable unrealized = float(self._unrealized_pnl or 0.0) diff --git a/python/valuecell/agents/strategy_agent/portfolio/in_memory.py b/python/valuecell/agents/strategy_agent/portfolio/in_memory.py index 07f632564..f870f12a3 100644 --- a/python/valuecell/agents/strategy_agent/portfolio/in_memory.py +++ b/python/valuecell/agents/strategy_agent/portfolio/in_memory.py @@ -192,14 +192,21 @@ def apply_trades( notional = price * delta # Deduct fees from cash as well. Trade may include fee_cost (in quote ccy). fee = trade.fee_cost or 0.0 - if trade.side == TradeSide.BUY: - # buying reduces cash by notional plus fees - self._view.account_balance -= notional - self._view.account_balance -= fee + + if self._market_type == MarketType.SPOT: + if trade.side == TradeSide.BUY: + # buying reduces cash by notional plus fees + self._view.account_balance -= notional + self._view.account_balance -= fee + else: + # selling increases cash by notional minus fees + self._view.account_balance += notional + self._view.account_balance -= fee else: - # selling increases cash by notional minus fees - self._view.account_balance += notional + # Derivatives: Cash (Wallet Balance) only changes by Realized PnL and Fees + # Notional is not deducted from cash. self._view.account_balance -= fee + self._view.account_balance += realized_delta total_realized += realized_delta @@ -260,8 +267,14 @@ def apply_trades( self._view.net_exposure = net self._view.total_unrealized_pnl = unreal self._view.total_realized_pnl = total_realized - # Equity is cash plus net exposure (correct for both long and short) - equity = self._view.account_balance + net + + if self._market_type == MarketType.SPOT: + # Equity is cash plus net exposure (market value of assets) + equity = self._view.account_balance + net + else: + # Derivatives: Equity is Wallet Balance + Unrealized PnL + equity = self._view.account_balance + unreal + self._view.total_value = equity # Approximate buying power using market type policy From ed1176c3cb82a1dd16a6d15912d8096be8d26d89 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 19:49:16 +0800 Subject: [PATCH 4/9] fix failed order --- .../valuecell/agents/strategy_agent/core.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py index 6c0ccac44..0fc1aa0d8 100644 --- a/python/valuecell/agents/strategy_agent/core.py +++ b/python/valuecell/agents/strategy_agent/core.py @@ -273,10 +273,32 @@ async def run_once(self) -> DecisionCycleResult: instructions, market_snapshot ) logger.info(f"✅ ExecutionGateway returned {len(tx_results)} results") + + # Filter out failed instructions and append reasons to rationale + failed_ids = set() + failure_msgs = [] for idx, tx in enumerate(tx_results): logger.info( f" 📊 TxResult {idx}: {tx.instrument.symbol} status={tx.status.value} filled_qty={tx.filled_qty}" ) + if tx.status in (TxStatus.REJECTED, TxStatus.ERROR): + failed_ids.add(tx.instruction_id) + reason = tx.reason or "Unknown error" + # Format failure message with clear details + msg = f"❌ Skipped {tx.instrument.symbol} {tx.side.value} qty={tx.requested_qty}: {reason}" + failure_msgs.append(msg) + logger.warning(f" ⚠️ Order rejected: {msg}") + + if failure_msgs: + # Append failure reasons to AI rationale for frontend display + prefix = "\n\n**Execution Warnings:**\n" + rationale = (rationale or "") + prefix + "\n".join(f"- {msg}" for msg in failure_msgs) + + if failed_ids: + # Remove failed instructions so they don't appear in history/UI + instructions = [ + inst for inst in instructions if inst.instruction_id not in failed_ids + ] trades = self._create_trades(tx_results, compose_id, timestamp_ms) self._portfolio_service.apply_trades(trades, market_snapshot) From e9af2b0e29f72ff92d7bdfda5458c3ac10423c17 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 19:49:45 +0800 Subject: [PATCH 5/9] lint --- .../valuecell/agents/strategy_agent/core.py | 26 +++++++++++-------- .../strategy_agent/execution/ccxt_trading.py | 10 ++++--- .../strategy_agent/portfolio/in_memory.py | 4 +-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/core.py b/python/valuecell/agents/strategy_agent/core.py index 0fc1aa0d8..63daee496 100644 --- a/python/valuecell/agents/strategy_agent/core.py +++ b/python/valuecell/agents/strategy_agent/core.py @@ -159,10 +159,10 @@ async def run_once(self) -> DecisionCycleResult: quotes.append(s.split("-")[1]) # Deduplicate preserving order quotes = list(dict.fromkeys(quotes)) - + free_cash = 0.0 total_cash = 0.0 - + # Sum up free and total cash from relevant quote currencies if quotes: for q in quotes: @@ -187,19 +187,19 @@ async def run_once(self) -> DecisionCycleResult: logger.debug( f"Synced balance from exchange: free_cash={free_cash}, total_cash={total_cash}, quotes={quotes}" ) - + if self._request.exchange_config.market_type == MarketType.SPOT: # Spot: Account Balance is Cash (Free). Buying Power is Cash. portfolio.account_balance = float(free_cash) portfolio.buying_power = max(0.0, float(portfolio.account_balance)) else: - # Derivatives: Account Balance should be Wallet Balance or Equity. - # We use total_cash (Equity) as the best approximation for account_balance + # Derivatives: Account Balance should be Wallet Balance or Equity. + # We use total_cash (Equity) as the best approximation for account_balance # to ensure InMemoryPortfolioService calculates Equity correctly (Equity + Unrealized). - # Note: If total_cash IS Equity, adding Unrealized PnL again in InMemoryService - # (Equity = Balance + Unreal) would double count PnL. + # Note: If total_cash IS Equity, adding Unrealized PnL again in InMemoryService + # (Equity = Balance + Unreal) would double count PnL. # However, separating Wallet Balance from Equity is exchange-specific. - # For now, we set account_balance = total_cash and rely on the fixed + # For now, we set account_balance = total_cash and rely on the fixed # InMemoryPortfolioService to handle it (assuming Balance ~= Equity for initial sync). portfolio.account_balance = float(total_cash) # Buying Power is explicit Free Margin @@ -273,7 +273,7 @@ async def run_once(self) -> DecisionCycleResult: instructions, market_snapshot ) logger.info(f"✅ ExecutionGateway returned {len(tx_results)} results") - + # Filter out failed instructions and append reasons to rationale failed_ids = set() failure_msgs = [] @@ -292,8 +292,12 @@ async def run_once(self) -> DecisionCycleResult: if failure_msgs: # Append failure reasons to AI rationale for frontend display prefix = "\n\n**Execution Warnings:**\n" - rationale = (rationale or "") + prefix + "\n".join(f"- {msg}" for msg in failure_msgs) - + rationale = ( + (rationale or "") + + prefix + + "\n".join(f"- {msg}" for msg in failure_msgs) + ) + if failed_ids: # Remove failed instructions so they don't appear in history/UI instructions = [ diff --git a/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py b/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py index 293513cd3..783254dae 100644 --- a/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py +++ b/python/valuecell/agents/strategy_agent/execution/ccxt_trading.py @@ -845,7 +845,9 @@ async def _submit_order( price_precision = market.get("precision", {}).get("price") if price_precision is not None and price_precision == 0: price = float(int(price)) - logger.debug(f" 🔢 Fallback: rounded to integer {price}") + logger.debug( + f" 🔢 Fallback: rounded to integer {price}" + ) except Exception: pass @@ -882,9 +884,11 @@ async def _submit_order( except Exception as e: error_msg = str(e) logger.error(f" ❌ ERROR creating order for {symbol}: {error_msg}") - logger.error(f" 📋 Failed order details: side={side}, amount={amount}, price={price}, type={order_type}") + logger.error( + f" 📋 Failed order details: side={side}, amount={amount}, price={price}, type={order_type}" + ) logger.error(f" 📋 Failed order params: {params}") - + # Return error result instead of raising to allow other orders to proceed return TxResult( instruction_id=inst.instruction_id, diff --git a/python/valuecell/agents/strategy_agent/portfolio/in_memory.py b/python/valuecell/agents/strategy_agent/portfolio/in_memory.py index f870f12a3..c65c0aba2 100644 --- a/python/valuecell/agents/strategy_agent/portfolio/in_memory.py +++ b/python/valuecell/agents/strategy_agent/portfolio/in_memory.py @@ -192,7 +192,7 @@ def apply_trades( notional = price * delta # Deduct fees from cash as well. Trade may include fee_cost (in quote ccy). fee = trade.fee_cost or 0.0 - + if self._market_type == MarketType.SPOT: if trade.side == TradeSide.BUY: # buying reduces cash by notional plus fees @@ -267,7 +267,7 @@ def apply_trades( self._view.net_exposure = net self._view.total_unrealized_pnl = unreal self._view.total_realized_pnl = total_realized - + if self._market_type == MarketType.SPOT: # Equity is cash plus net exposure (market value of assets) equity = self._view.account_balance + net From 6f394f446baf7422c94009afc88c366441f1e0c7 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 19:51:02 +0800 Subject: [PATCH 6/9] rename Hyperliquid wallet_address placeholder --- .../components/strategy-items/modals/create-strategy-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx index cf6d0ea78..4fdd48893 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx @@ -613,7 +613,7 @@ const CreateStrategyModal: FC = ({ children }) => { ) } onBlur={field.handleBlur} - placeholder="Enter API Wallet Address (0x...)" + placeholder="Enter Main Wallet Address (0x...)" /> Date: Wed, 19 Nov 2025 20:17:03 +0800 Subject: [PATCH 7/9] support add symbols in MultiSelectProps --- .../modals/create-strategy-modal.tsx | 3 +- .../src/components/valuecell/multi-select.tsx | 38 +++++++++++++++++-- frontend/src/constants/agent.ts | 8 +++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx index 4fdd48893..ba76e6c52 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx @@ -830,9 +830,10 @@ const CreateStrategyModal: FC = ({ children }) => { field.handleChange(value) } placeholder="Select trading symbols..." - searchPlaceholder="Search symbols..." + searchPlaceholder="Search or add symbols..." emptyText="No symbols found." maxDisplayed={5} + creatable /> diff --git a/frontend/src/components/valuecell/multi-select.tsx b/frontend/src/components/valuecell/multi-select.tsx index 0dd87ed5b..c2d07ad3c 100644 --- a/frontend/src/components/valuecell/multi-select.tsx +++ b/frontend/src/components/valuecell/multi-select.tsx @@ -35,6 +35,7 @@ export interface MultiSelectProps { disabled?: boolean; maxSelected?: number; maxDisplayed?: number; + creatable?: boolean; } export const MultiSelect = React.forwardRef< @@ -54,12 +55,14 @@ export const MultiSelect = React.forwardRef< disabled = false, maxSelected, maxDisplayed = 3, + creatable = false, }, ref, ) => { const [open, setOpen] = React.useState(false); const [internalValue, setInternalValue] = React.useState(defaultValue); + const [inputValue, setInputValue] = React.useState(""); // Normalize options to MultiSelectOption[] const normalizedOptions = React.useMemo(() => { @@ -105,9 +108,16 @@ export const MultiSelect = React.forwardRef< onValueChange?.([]); }; - const selectedOptions = normalizedOptions.filter((opt) => - selectedValues.includes(opt.value), - ); + const selectedOptions = React.useMemo(() => { + const opts = [...normalizedOptions]; + // Add selected values that are not in options (for custom values) + selectedValues.forEach((val) => { + if (!opts.find((o) => o.value === val)) { + opts.push({ value: val, label: val }); + } + }); + return opts.filter((opt) => selectedValues.includes(opt.value)); + }, [normalizedOptions, selectedValues]); // Get displayed badges and count for remaining const displayedOptions = selectedOptions.slice(0, maxDisplayed); @@ -200,7 +210,12 @@ export const MultiSelect = React.forwardRef< sideOffset={4} > - + {emptyText} e.stopPropagation()}> @@ -247,6 +262,21 @@ export const MultiSelect = React.forwardRef< ); })} + {creatable && + inputValue.length > 0 && + !normalizedOptions.some((o) => o.value === inputValue) && + !selectedValues.includes(inputValue) && ( + { + handleSelect(inputValue); + setInputValue(""); + }} + className="cursor-pointer py-2 text-muted-foreground" + > + Create "{inputValue}" + + )} diff --git a/frontend/src/constants/agent.ts b/frontend/src/constants/agent.ts index 7b532bbc4..280cb6666 100644 --- a/frontend/src/constants/agent.ts +++ b/frontend/src/constants/agent.ts @@ -129,4 +129,10 @@ export const MODEL_PROVIDER_MAP: Record< }; // Trading symbols options -export const TRADING_SYMBOLS: string[] = ["ENA/USDC", "WIF/USDC"]; +export const TRADING_SYMBOLS: string[] = [ + "BTC/USDT", + "ETH/USDT", + "SOL/USDT", + "DOGE/USDT", + "XRP/USDT", +]; From 0a81ecf572b91de55af63c7bd7a18a42f91f496c Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 20:25:50 +0800 Subject: [PATCH 8/9] lint --- frontend/src/constants/icons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/constants/icons.ts b/frontend/src/constants/icons.ts index ad683239d..736475c80 100644 --- a/frontend/src/constants/icons.ts +++ b/frontend/src/constants/icons.ts @@ -8,9 +8,9 @@ import { DogePng, EthPng, GatePng, + GooglePng, HyperliquidPng, MexcPng, - GooglePng, OkxPng, OpenAiCompatiblePng, OpenAiPng, From 581981bd7ced6d6d1f81f88fc443cd0356fc1e03 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Wed, 19 Nov 2025 20:28:47 +0800 Subject: [PATCH 9/9] fix type error --- .../strategy-items/modals/create-strategy-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx index ba76e6c52..da5d5f111 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx @@ -67,8 +67,8 @@ const step2Schema = z api_key: z.string(), secret_key: z.string(), passphrase: z.string(), // Required string, but can be empty for non-OKX exchanges - wallet_address: z.string().optional(), // For Hyperliquid - private_key: z.string().optional(), // For Hyperliquid + wallet_address: z.string(), // For Hyperliquid + private_key: z.string(), // For Hyperliquid }) .superRefine((data, ctx) => { // Only validate exchange credentials when live trading is selected