From 6c2eca8fdc7862853fb0986a93ce366a1df72285 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Sat, 5 Sep 2026 00:23:55 +0800 Subject: [PATCH] fix bug --- api/qmt_rest_new.py | 1 + docs/bug.md | 13 ----- .../__pycache__/collector.cpython-311.pyc | Bin 2802 -> 2802 bytes .../libs/__pycache__/market.cpython-311.pyc | Bin 2878 -> 2753 bytes py-client/requirements.txt | 1 + .../sdk/__pycache__/models.cpython-311.pyc | Bin 17421 -> 17717 bytes .../sdk/__pycache__/trade.cpython-311.pyc | Bin 2394 -> 3296 bytes py-client/sdk/models.py | 3 ++ .../ipo/__pycache__/boot.cpython-311.pyc | Bin 2893 -> 3879 bytes .../trend/__pycache__/boot.cpython-311.pyc | Bin 10262 -> 10699 bytes .../trend/__pycache__/open.cpython-311.pyc | Bin 7376 -> 7406 bytes .../trend/__pycache__/order.cpython-311.pyc | Bin 8093 -> 9144 bytes .../__pycache__/positions.cpython-311.pyc | Bin 9633 -> 9660 bytes .../trend/__pycache__/state.cpython-311.pyc | Bin 12182 -> 12553 bytes .../trend/__pycache__/watch.cpython-311.pyc | Bin 5037 -> 5030 bytes py-client/strategy/trend/order.py | 46 ++++++++++-------- py-client/strategy/trend/state.py | 35 +++++++------ py-client/tests/test_trend.py | 11 ++++- 18 files changed, 59 insertions(+), 51 deletions(-) diff --git a/api/qmt_rest_new.py b/api/qmt_rest_new.py index d5152d3..ae337fc 100644 --- a/api/qmt_rest_new.py +++ b/api/qmt_rest_new.py @@ -313,6 +313,7 @@ def format_holding(positions): stock = position.m_strInstrumentID + '.' + position.m_strExchangeID holding[stock] = { 'StockCode': stock, + 'TradeID':position.m_strTradeID, 'StockName': position.m_strInstrumentName, 'Direction': position.m_nDirection, 'Volume': position.m_nVolume, diff --git a/docs/bug.md b/docs/bug.md index 890d60a..d0d0008 100644 --- a/docs/bug.md +++ b/docs/bug.md @@ -1,9 +1,3 @@ -### S1. 券商快照延迟时,本地委托锁会被清空并可能重复下单 - -- 位置:`strategy/trend/order.py:54-92`、`strategy/trend/boot.py:127-130,157-162`、`strategy/trend/open.py:23-25` -- 证据:下单成功后 `OrderBook.place()` 会立即加入本地 pending 和方向锁,但下一轮 `refresh()` 会完全用券商快照重建 `data` 与 `lock`。若券商快照尚未出现刚提交的订单,本地 pending 会直接丢失。开仓候选只排除当前持仓,不排除 `State` 中的待成交底仓,最终仅依赖已经被清掉的 `busy()` 锁。 -- 影响:接口存在可见性延迟时,同一证券可能在连续轮次重复提交买单;卖单和补仓也存在相同锁丢失窗口。 -- 建议:刷新时合并尚未超时且券商未确认的本地 pending,而不是覆盖;按本地订单号查询确认后才能移除。开仓筛选同时检查状态机中的 `base_status`。 ### H3. 撤单、拒单、废单和部分成交不能驱动状态机正确收敛 @@ -14,13 +8,6 @@ - 建议:建立完整 QMT 委托状态映射,按实际成交数量处理全成、部成、已撤、废单、拒单和未知;消失订单需二次查询确认。 -### M1. IPO 仍只依赖本地锁,存在重复申购窗口 - -- 位置:`strategy/ipo/boot.py:49-65` -- 证据:下单成功后才写锁;未查询券商当日委托或成交记录。进程若在下单成功与写锁之间退出,或锁文件被删除,下一次调度会再次申购。 -- 影响:同一证券可能重复发送申购请求,安全性依赖券商端是否拒绝重复申购。 -- 建议:本地锁只作为快速缓存,下单前以券商委托/成交记录做最终幂等校验。 - ### M6. 策略成本使用提交时行情价,而非实际成交价 - 位置:`strategy/trend/open.py:80-86`、`strategy/trend/positions.py:186-192` diff --git a/py-client/libs/__pycache__/collector.cpython-311.pyc b/py-client/libs/__pycache__/collector.cpython-311.pyc index 1c038d18f70a977d4b2291b4b047ca33c4a513d5..bb236122f48a096a1916fcd89f6be5e590936a5e 100644 GIT binary patch delta 19 acmew)`bm^)IWI340|Ntt-NubvFSr0X^#y+m{bO4a<&NtP2<5DI8ek)(h z_ry_Z7eRA|;Ivh!C0C>zELNe1blcbTGAU!9=1?Z3j@C9X4T1w{RkN57e`E=w@Nc$7 z7e;k{q%889vg98F5CX7U1D{os^OQxaX7{DfjV>&I9qPm9c|cuL}U^?OYs4J51;fi*68~Mg&<|v delta 551 zcmZXP&1(~35XPT(-+gyCSvO_VcAH8i!B}DhX(1`nB7trXf(H?L>Pgfs!8XmpCM6yc zb2Lb?hZjrzD0t|{A(s>cdlC}=fd+C2g`6a{2M>D@7EiJ%0kJc~?-^$1IZSi>o1q

5nUp{NgMz*=C~TdYjV^pFB|I-C)# zz>+HT70oM`S|TO9q$?U^~7crz{T6I{q3Fp z{?X5myXRZ4&)+rr`^Ohw4i;k5h1hiAQSus}l^&1CIXjzukO@j)J>AjMHK`_fJ$1~5z?R6r?CRHRQcoG#(oT(g zC!;6N+ltvy%+o97d7kGo-|l(dZwW!e1!1uPx|7jb$fZ0lKZ(xM=0s#MO`B=9I8R%Y znqk@si>4vBCW&eAU-Ea7nW}G?xBe0!NN!W!-!n?}@Y0!cEk175>c>6DQgphFn diff --git a/py-client/requirements.txt b/py-client/requirements.txt index bef0bdb..fbbb499 100644 --- a/py-client/requirements.txt +++ b/py-client/requirements.txt @@ -1,3 +1,4 @@ httpx>=0.27,<1 PyYAML>=6.0 APScheduler>=3.10,<4 +CacheLib>=0.13,<1 diff --git a/py-client/sdk/__pycache__/models.cpython-311.pyc b/py-client/sdk/__pycache__/models.cpython-311.pyc index e17059cae1f5b2e0e74b30129cf186247ca50b87..ae084b37f1a3d334143b818cf2ff8e99d0c2c392 100644 GIT binary patch delta 2976 zcmb7GZERat89vu>ZtQC(cKj88rj9>SCF!CpT}sj>O}b=Rx6*cN)2{Vksh4|8yf|@s z>`ZO9l2KKo+F)aS$66I_{TR@(u4_wH7(%e30owR69ASx3KExjch#$BD1OjcK=e5(8 zu9`F}dh|T+dCz;^bI)^b?w1cMm!4AU&(zmzLI|;{eEgB47wh*mnz|JFd29Q;uB24m zWSI5Zej$Xert0)M!>n6aSyL8WGt9b;l|5zE>kYHsP*oZ$M^)KaIaw(wJJ)PvWlhy{ z6<1X?uyV7Sx27B{J=H-cD=#a1s*#KOs>;QxiItjibEAG%O3K3v2A~YD-lE6!Wru3N zDTFwSZSybXGrW3Sj~+5#kJ~YlYSPuMqAfwMySlV1mV!*{#&||w_?^q6*o$T%jtY}_ zT<8h<(0z<9Ta$F&(g7Ue?Q{0=iIdJJ=vIN5zM^%-TWXiL)P{VV3{#?aV3BXXM% z;)-=7BaMkdCO0D^(CYSFc5d2`+j(&FtTA(+%uE>a0chK3HkU5kCv&?q1-XMG8`E;i zC~Qw>=M1@%BU=l(iK+FutZqn|Lc~5QbNh{%YT@|@IX0Z0*_Jn|vCnX5^URi9TCava zi;!cdkuMlhPw%TnA41gn>2$tO>1rHWt2DGFT`=S>o;EsHn3D$Yb8O$NA=8|8*WTHT zH0XF>R|LDs?=uPUg!sL~bvR!Znp0Z>Y6`{`jn)K{cWF-T4_^N<&k6r=|H-z+z=~2} zMKREQ!L;aIS@NzddRLZfF;f*yp`(*WCXYR~=<6u?I*PuIa-HVX{-KJVemW2gsjMvY zQZS@iF<_y$g3ZgBY9iQj(sRat+W*RJi!HsSmfpocUn$U64D^+4$koUkN~lcj)ESDX z^@!T({!pSq8|YL#qB;<5pqE043U%^ojfgtw&!OfhXxSx}#ZOEgpDZr#U2N_vHTM;p z`^s)4xoCMf(FIx-npKnQg%A^_s_XH?)vpmk`P)rqUQPcEyXl?qhHIw&ApFTp{XzJX zn)05GeZ6}Jn)4wZYL;Y}$4Fr~}`U_@B}YzEBHUs{G@Xfr>~ zW-P11>4qI$ID{qmE$xXdGu60gCKexYv8#N6k2Pf-rdn=cN-L&obUqeQZJ>5|#8mGh zXFS@9fb(O_*oXud^~9s98`J|cn!GN0F&?W$zz-j`2S9_OIeBvI%#PDLik*Xt$)Qqm z=qK^T$Y?1tT8xaAL&yoUK_j5eWKD#k%g}c8Rt9ZBQjA6t(Yko$vCveaJ^Zi`qRlMZ z@G#o}HULNcbTN@o`OG8q>%f0V+>R%I^@C%c_)?-T2bW^d(h(d?x{^6IFq zRx6ADgR3<{Z9Mc~*@S3NsEuWl2T4nCZG0(E7M_K(tsg2o9>crGK0U6ldo-QNrXS84 z@^N%+k{DUyPVhScd*KG$x@c57xLYB_e&@P;-YDcdcgx)LxSp9P93-Xfiya40$s+GT z-o1bg04}@{3@a_|xh3h=_Co^>P_a!ngOSqdqfPBY%6@vbJ*OQ+2Hr?q{=|OA@pi)T)vFW3a|E86>O!4ZCw2nt=$__HQvGR6QgY?P_<^3;i2< ze~9EH;4Q#qzDv1Te6Ebo@^{IP5Dx?12K*SUOI}I)DFT3B(5itinZbC0-wYhK-EdTt zTN_l)(Nk+f-W#Hr`!hi00N(`#yidPedq8=YwhcagKi?+4FvA#T*Rg;2xcMabQSg?z z8!p4Ie*AOg`~7cA|8!2D%NlFtdvyCyscV&@mBmNlDq?>JT&)qNrjp_-n~)_Jde*&f F`X87j?x_F( delta 2708 zcmb7`YiyHM7{|{k-MjVey0%-_jdktH)s zdCqg*^MBv&#G~?w?b7k0!=VcyMDzE1)|VW2Y;s#FCAHpAGi2mMG($73MsAZ3!pMu{ z8aC5v=&bCKJR{$<8jh^8u`0+aot2Z7jMxo}X*FD|@*??M#hq0SR)twrz^aJVP+r8z z(!pl};V9&5bFuI~7axE58upqSld!5B8aA1%z$!fZ4x9#kx(^0d1cb zPefBoRAOB$rS9g;qNwUMQytOxps7}JW_c>n-Mb(WH%z5gA(KB(B{rM=*}@S{&58D} zNSYmqb-dYX&Mod=mWUeJY4;%ITxBLxrZS?Nve`~#ZFfbJsZ3XEp!G~c%c3b$b@8%! zgQ-DfwyUUNwr?6R)mr*vVy_=ZNG`PqF)S`Q3%4iJ!sgP)L64MGNgEw03(vB-^h@5d z-90;d_H4P}tsV2$j(Tg;IX0L6U47%*bI&e#XTf_bFEq7{HMM=VWch{q_Obf*(fW3t zaYduGpGDwpa;tXL4;|SsHocC^K8%Pl$jR-4P$|Z(Lh7mjY%##TOP_qm=-qrsoeix zmUGi0|C;}2yKnvf-*(^nJ$V`14Ytq@yO*v^svGJSLZ|?+958@(zy#n&wGMz8)qMc& zQ1t+?pNavnomvmTZmJi6%TyeIw^SbhOR0VUHc|;OMF@6~8i0m%)cwF(Kmm(^Bz;ji zyAr)78`wQ*O}L8SA2*)vIKH9IstU^lH$|$d0!V72#CN@ma@}l!b5(xLhEV5gwIj@j zM=TCE$xtu{2^Y-|1vNLqLUu|KLJ#a=DR$H2p-P@sN}q=U+C+?%aWx;pa`J_JK|lHk z{F_NCFeykK;b88h%n4{~xTZWRgs8TvYMi?UumhYep%20#jW1;d{Tyzzw_zv+%%|2_ zMYM2ofxM3*lj~^TCEy$J%&7HOsVbVDM8Xr&akXnEpmeO$Z z#E~mgzLqOD;T6yC>oQt5L}T&jy>U}*M%NaFk@47t_%>hzF3uu^2nLlC3ADV~)tXG2 zspPbED$&MR9*9k+(O4|ii^3>qVZ}fb5NX7--slznSQG=hYwjinoP}!Cz7Umnl(Obtg9zv zJc_j1MPJ-hIr-m@QP3Xi?!9`5=c%IZgIk}$IFnAp~BhFJ~N)hu8`GZmE{qPK+F;PiM|; zK8BFndJWNWzze(%i~!u~tBBqJP5^HLZ}B!q5dx=aeZyDsD0kj@>z?Y&E*QPKj=xzR z zE!#V;ee4%?Beo%Cbfxg4iB~354*%k`RokvN6d&-~0_tNd%G}#|gurL?N6S`up0>_@ zY&yR`{FcnYFk6j1!?(eEhdRqJwmy!DgxI95vO2(u& OZNaSgNat-|TK)vo610Z^ diff --git a/py-client/sdk/__pycache__/trade.cpython-311.pyc b/py-client/sdk/__pycache__/trade.cpython-311.pyc index d8d8ad7f7f27d97317c42f0f15d971424d7cec2b..f261d4b4337bdaceca3b37f1aa37fabd5ab82dae 100644 GIT binary patch delta 1658 zcmZ`&O-vg{6n?Wm_S#^e*o$NHw=|-`E zEpk+GK)of~Dp8dKhl;A<+^W|edg;kVR#~lDDLqE*!BLY7Jy6X$v2jJ;N}p!mym|B9 z_q}nNA4M9z4FvoE0MVbmzWI-Er{UL}kNyInzuyCZdmuv!VMs9;VMb>2C;7%Og;Zt5L;m8h`5^xue?ro0{yfiZz4e9IqQ{=9c3jvawFG#|>D?7}s=z^h#6N&BaVzxkT3j z0Kn36AG+llsJdq|8Z!O+2;iXN-K(rl+dcpsR2VDNQ zV??|WM`H3&OcunUqBykcw;LlRK)%u6o7?j3ZxotGip?YW z`VqS^Xic+IWBmNW>|0kHLD5uKq2ti1q8rz01zF5g8Uq0D3q^jh{y?2M%owMLGXF#5 z-szJStJULdqHALmR7Se19CydEt(RHc|yY>2#tWlmUYd`2b}Mr;S|N z;pk|`Jfs%@Ts=wL6?I+HWa9W0UCHW3I-8Llj*=T=Ngs_Iq(K6abxqCcvQ9~qB#HF9 z?&sOlTfyp+>VM%Jpo6gD%Ks=A4Bwl$J7L#%*@56!^Y_o)S^RSGNbETjdkSK_D8`S( z#G#ldh=WCOFkhd1!m}a&Q(!~>60o7Bk_Q@^N?!22^h|)ZSN3jv&lOsSimgNW#We3b=C#%$2rOX8KE#vVUof{0Qe4E@?2F(6ZKMPpncE*zg_uC0<^ZXIe+kZjN#xoFFMS0NB{r; delta 707 zcmZ`%&1(}u9R1DiX1|gRQMdUdO-kuuW14E!DmL{aCIywIl2oLNbQNdCw%W841(7`z zjDLXq4i)jDf>Jzr7XJWG-X!GY#e=tqo(d&0o5o)13_O@OZ{B+|%=bj+aQMAp=l}q_ zKVGc7k)MaZEy;WdKzdq}cUS9jz>-Bmaz#C)(zmj;=Pu&{^0Ej%Ygdf$6Sy73^XQkO#vvj;Eo(MF#J-!r| zf+Yahgl-%nCZ3Tl;wSmKF%AHh#Et?0o4~XZBY8ZfOp+YlQ%2+h0Nj!BwPfO3<%)5f zi{*iMM;Rq~99A!oadgzvq<~xMdXiaOB=kkY0MO(`vssqMQ7uPiu&RwI+_!3(_(Z#< zX1QKv6Xc~MY(LB)$y6WxXQ1>COMqsFa-eHz<3nPbBGKe z%v(M6A7gWGc0-WLw_{`NSfZWEwbPk)JlP#e1oSQ(08_XTyh;lAIXGQD%U!2A@K4e) z=WFFk&8c55SDezKGe0|9uPpSVXSnEJogKCKpg|wqch9j)SPZ%6kC7Vb!om2<=l}d? H*EjtOE)1HQ diff --git a/py-client/sdk/models.py b/py-client/sdk/models.py index 5975405..ee2c2ae 100644 --- a/py-client/sdk/models.py +++ b/py-client/sdk/models.py @@ -68,6 +68,7 @@ class OrderItem: class PositionItem: stock_code: str = "" stock_name: str = "" + trade_id:str = "" direction: Any = None volume: int = 0 open_price: float = 0.0 @@ -87,6 +88,7 @@ class PositionItem: def from_dict(cls, data: dict[str, Any], code: str = "") -> "PositionItem": return cls( stock_code=str(data.get("StockCode") or code), stock_name=str(data.get("StockName") or ""), + trade_id=str(data.get("TradeID") or ""), direction=data.get("Direction"), volume=_number(data.get("Volume"), int), open_price=_number(data.get("OpenPrice")), float_profit=_number(data.get("FloatProfit")), market_value=_number(data.get("MarketValue")), stock_holder=str(data.get("StockHolder") or ""), @@ -102,6 +104,7 @@ class PositionItem: return cls( stock_code=str(data.get("StockCode") or ""), stock_name=str(data.get("StockName") or ""), + trade_id=str(data.get("TradeID") or ""), direction=data.get("Direction"), volume=_number(data.get("Volume"), int), open_price=_number(data.get("OpenPrice")), diff --git a/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc b/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc index 0e1d348a9074b41ca660757bc43435c73ccbf99e..6c6e81c817bf6fabf18932b99bb6c7e6d41f56e9 100644 GIT binary patch literal 3879 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 H7XbVXzS_z) diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc index cbf10da5b0acb03a3ebf4c91f348a4fb40a6e95d..fc08c1f8f4a745f9bb4676a131de66080412010e 100644 GIT binary patch delta 1631 zcmZuxU2IfE6h3qB?)L7zvp@aYpUducm)*9mlwDc{QcHnqQra!HKuxeTwpMNnUFdFd z_m&zmD~9^eNJFB7#i(e5S|2J2xQQAi;YpL0#B7^Q*fhbvq*mhtYr=yMKA5@fr9hbE ze)G+D&YUxI=giC;y4`F4!e$cz0K~!v$y)ynyG0OwGy_})4bgxY%f`Y{v4a8!*~<``d?CqQ_3sB)o;F9ow^=u-1*#&9rtcd zuYFJdxbn%LUtd^#`_|gEnYH&m@1{-m1Q(6v&e=WanKEr`@ML;iU9o6mgJYv9z0V9E zS9bxxiuw5IbY@s>Wn9Rd9M_ceM2ddw2n#mdL2aiCj;?y{w6-E0A6M1kl*T5n;EAD8 zouyMsW(5srYyc3XyPN@W1OVaykohGy;Jk^>dCWuTuC07}V9Dk$*!)vm5!itW&t$^F|xUGmC(}_FSw~T#DI8eZWJPt7J;2j)V z#-Sw~F5qw;hl{vks=es)TpFE{N*2KGyf?Ni#qv_DxMc_3S8+8KXN$;q0AMlXOf*n+#=y zJAtxg#FTU!3Na;FKE08Lqj>4h)wNq>!=?z2&!PdB7wx7_tCt>jRaN>7PhviU)Ho4m z{raW2;Bq0InStP>kACNh+cu+GwO0D8N2X4!*0C< z23aC8qYbIrurfHLsqNGkxavH{+!$ooXENn}3dktpzSL8wiWRHmVwL|NtGT@5KA6iZ zN@l@kFY#hjeDgp_&Y6P0aq)V&lY<4Wp|go!XhaNm)h2wf=nEwF@{6q~v6olRv($V> zQ`N~_Rk#8*_Oqm07^DCo%IO~JBwGP{8-hVK#*AkS_<IzEu;)5+E7x0h8a1~eG=xg-oh%($%T+nSD> zhn4wKWFR8?!}N&4bW&t=KU9(N7|166R#+wu2L7PJVEzSWqOc!CTl(PTo{*wQL4c3JM#^e=cc0RgXRQKDc(heVb(E8y zEkcgssAn{Qw$|)%)W!q8vBgV{($?&!Hlul1!}O_Pgb-A)56}y-Lj9R7=P-yuJ7VwJ zVyqG0Zm3S=6SyEX!e3}>62Q9Tdl2TK5ot$OB28s;UB+d2U7m+!eD9r7)V&i+B2Dzp z$g`zU_iV3$;gG#}taaizeXkdksF(l%v7heg2WTpu4MYKetaw&?5dg{n(0a@LApQ*| z62Us&PMRM!ZzYRA@Z3&U`>yOLFyAr&;QAAKO^1%I)h6YRnXc`TJ9~n)Nm)G484?Uf z=|cY)o^m_=3({o339ZQh>#~291?gpMhn;{m;4@CZN?)l% zDzSZjD8OpY3%YAeVBWK;=_j#DE9uF|kgr;O*z^39*0Dh6(gsQ9JfAWbwX41czzMA( zz&iSjm&>Hj%+%&_sACp!n!4ry4p1mlxzEmJiu9|g)7zPyc#yu%Z0DWjUi*;yTjsKa zqjWJlw08~wepVNknoIUl^VQ?`tIPlX5r(H3#u%m;_A*Q}9HGCmMeiaa&)?(pSZ*8- zQ9U;t;vN+T>D^o*<(2X%=g?YqUDziY$LhA&PaC<<_2*c|u*hBWr|d50O&m0T?0@KP zI1o?#4uN>08w|&Ky0Ti*-kjE*xc}b$LN-3=iL9B?K0Pr{L&imp3 diff --git a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc index 6bc032440de5431dfa54a9a1ee0084c54bdab520..9159fce2fb1139aadc9c23ae251a316641e4563a 100644 GIT binary patch delta 1553 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 PuZQf1+#*aiF diff --git a/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc b/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc index 0361f667657ca599783d0023a25770b798415d96..777bd75e4d6d9106adec3d8c70aad375efd32a4c 100644 GIT binary patch delta 229 zcmZ4Jy~mq(IWI340|NttruwX`&W*f9oV=+F3=Gi$`Nf$fnfZB>+c}eYGtfj_CYy0> z=JiDp{mA8tDze#*`!*wE-DD}=De5&J7#QjqotZu`Ft{@LGJOG25lkN$7Ub#(v7@DoE!-Z3=Gi$`NflaIFmV&VVpml-#I*>oJriS zJTSJxqqfPD`#7a1%X6!4p2j_$k+FQT9`6+8@(Y!<9~c-Mm^_%ifT&=mj|>dFAs`NK z2-61;Ir$~8o%sa`h%y(Dm;+M)i1L6?K}=B~*X_FKAzA#2j4&&Db MlS-SH@JomR01n1E*8l(j diff --git a/py-client/strategy/trend/__pycache__/state.cpython-311.pyc b/py-client/strategy/trend/__pycache__/state.cpython-311.pyc index 7f7ebdc976bef9b02a22c4cb6281ca214750881b..e2b519b1b50f0a71cf557588f2636a8590f7d7fa 100644 GIT binary patch delta 2061 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 JN@ObGKLEHOosR$j diff --git a/py-client/strategy/trend/__pycache__/watch.cpython-311.pyc b/py-client/strategy/trend/__pycache__/watch.cpython-311.pyc index c48040ae9aba72a6ce1675771c047462486dd45c..01ae40bc7c4c3f6f94ddce457500c74c8dcacf9f 100644 GIT binary patch delta 65 zcmV-H0KWgNC#EM2rwt7X000004 None: - self.lock_timeout_sec = max(0.0, float(lock_timeout_sec)) + def __init__(self, lock_timeout_sec: int = 180, cancel_timeout_sec: float = 10) -> None: + self.lock_timeout_sec = max(1, lock_timeout_sec) self.cancel_timeout_sec = timedelta(seconds=cancel_timeout_sec) self.data: list[OrderItem] = [] - self.lock: dict[str, float] = {} + self.busy_cache = SimpleCache(threshold=10_000, default_timeout=self.lock_timeout_sec) self.mutex = Lock() @staticmethod - def new_order_id(_leg: str) -> str: + def new_order_id() -> str: """生成 ``trend-xxxxxxxx`` 格式的本地订单号。""" - return f"trend-{secrets.token_hex(4)}" + return f"trend-{secrets.token_hex(12)}" def busy(self, code: str, side: str) -> bool: """判断证券是否存在仍在处理中的同方向委托。""" with self.mutex: - key = f"{side}-{code}" - return key in self.lock + return self.busy_cache.has(self._busy_key(side, code)) + + @staticmethod + def _busy_key(side: str, code: str) -> str: + return f"{side}-{code}" def refresh(self, client: Client, orders: list[OrderItem]) -> None: """用账户快照刷新委托,并撤销超时的活动委托。""" current = datetime.now() - now_timestamp = current.timestamp() data: list[OrderItem] = [] - lock: dict[str, float] = {} + busy_keys: set[str] = set() canceled = 0 for item in orders: @@ -78,21 +81,27 @@ class OrderBook: data.append(item) if item.status in BUSY_STATUSES: - key = f"{item.side}-{item.code}" - lock[key] = ( - item.created_at.timestamp() - if item.created_at is not None - else now_timestamp - ) + busy_keys.add(self._busy_key(item.side, item.code)) with self.mutex: self.data = data - self.lock = lock - log.info("[Order] 刷新完成,跟踪=%d,处理中=%d,撤销=%d", len(data), len(lock), canceled) + log.info("[Order] 刷新完成,跟踪=%d,处理中=%d,撤销=%d", len(data), len(busy_keys), canceled) def place(self, request: PlaceOrderRequest) -> bool: """按最新价提交委托,并立即写入本地方向锁。""" + side = ORDER_SIDE_BY_OFFSET.get(str(request.op), "") + if not side: + log.warning("[Order] 下单失败,代码=%s,原因=未知买卖方向(%s)", request.code, request.op) + return False + + key = self._busy_key(side, request.code) + with self.mutex: + if self.busy_cache.has(key): + log.info("[Order] 跳过重复下单,代码=%s,方向=%s", request.code, side) + return False + self.busy_cache.set(key, True, timeout=self.lock_timeout_sec) + try: result = request.client.passorder_latest_tagged( request.op, @@ -112,7 +121,6 @@ class OrderBook: log.warning("[Order] 下单被拒绝,代码=%s,本地订单=%s,状态=%s,柜台订单=%s", request.code, request.order_id, result.get("status"), order_ref) return False - side = ORDER_SIDE_BY_OFFSET.get(str(request.op), "") pending = OrderItem( id=order_ref, code=request.code, @@ -124,8 +132,6 @@ class OrderBook: local_order_id=request.order_id, ) with self.mutex: - key = f"{side}-{request.code}" self.data.append(pending) - self.lock[key] = pending.created_at.timestamp() log.info("[Order] 下单已受理,代码=%s,方向=%s,数量=%d,本地订单=%s,柜台订单=%s", request.code, side, request.volume, request.order_id, order_ref) return True diff --git a/py-client/strategy/trend/state.py b/py-client/strategy/trend/state.py index b7647d6..390d928 100644 --- a/py-client/strategy/trend/state.py +++ b/py-client/strategy/trend/state.py @@ -10,15 +10,13 @@ from threading import Lock from typing import Iterable from sdk import OrderItem, PositionItem +from .order import BUSY_STATUSES, COMPLETED_STATUSES # 委托状态:无操作、处理中、已完成。 STATUS_NONE = "" STATUS_ING = "ING" STATUS_OK = "OK" -STATUS_FAILED = "FAILED" -STATUS_CANCELED = "CANCELED" -STATUS_UNKNOWN = "UNKNOWN" @dataclass(slots=True) @@ -106,6 +104,7 @@ class State: self.set( StateItem( + base_order_id=position.trade_id, code=position.stock_code, base_qty=position.volume, base_cost=round(position.open_price, 2), @@ -143,22 +142,14 @@ class State: ): local_order_id = getattr(item, order_id_attr) current_status = getattr(item, status_attr) - if ( - current_status not in {STATUS_ING, STATUS_UNKNOWN} - or not local_order_id - ): + if current_status != STATUS_ING or not local_order_id: continue matching_orders = orders_by_local_id.get(local_order_id) - if matching_orders: - status = ( - STATUS_OK - if all(order.status == "56" for order in matching_orders) - else STATUS_ING - ) - if status != current_status: - log.info("[状态] %s 订单=%s,状态=%s->%s", code, local_order_id, current_status, status) - setattr(item, status_attr, status) + status = _order_status(matching_orders) + if status != current_status: + log.info("[状态] %s 订单=%s,状态=%s->%s", code, local_order_id, current_status, status) + setattr(item, status_attr, status) self.set(item) # Opening orders normally have no position until their first fill. Order @@ -209,3 +200,15 @@ class State: } except (TypeError, ValueError) as exc: raise ValueError(f"[状态] 状态字段无效: {exc}") from exc + + +def _order_status(orders: list[OrderItem] | None) -> str: + """将柜台订单简化为无状态、处理中或已成交。""" + if not orders: + return STATUS_NONE + statuses = {order.status for order in orders} + if statuses <= COMPLETED_STATUSES: + return STATUS_OK + if statuses <= BUSY_STATUSES | COMPLETED_STATUSES: + return STATUS_ING + return STATUS_NONE diff --git a/py-client/tests/test_trend.py b/py-client/tests/test_trend.py index f0b43c1..e6dd6e0 100644 --- a/py-client/tests/test_trend.py +++ b/py-client/tests/test_trend.py @@ -13,7 +13,7 @@ from strategy.trend.order import OrderBook, PlaceOrderRequest from strategy.trend.open import do_open from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.boot import RunOnce -from strategy.trend.state import STATUS_OK, STATUS_UNKNOWN, State, StateItem +from strategy.trend.state import STATUS_OK, State, StateItem class FakeClient: @@ -238,6 +238,13 @@ class TrendTests(unittest.TestCase): ) self.assertEqual(state.get("A").base_status, STATUS_OK) + canceled = OrderItem("2", "A", "BUY", "", "54", None, 50, "local-1") + item = state.get("A") + item.base_status = "ING" + state.set(item) + state.reconcile([position], [completed, canceled]) + self.assertEqual(state.get("A").base_status, "") + def test_low_cash_still_runs_position_management(self): client = SimpleNamespace( portfolio=lambda: Portfolio( @@ -276,7 +283,7 @@ class TrendTests(unittest.TestCase): "A", base_order_id="missing-order", base_qty=100, - base_status=STATUS_UNKNOWN, + base_status="ING", )) state.save() client = SimpleNamespace(