From 027d7e06eb58ced7b6480e6ee8d50ad89e0e9276 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 31 Aug 2026 15:33:39 +0800 Subject: [PATCH] Optimize --- py-client/__pycache__/main.cpython-311.pyc | Bin 7845 -> 8368 bytes py-client/libs/__init__.py | 3 +- .../libs/__pycache__/__init__.cpython-311.pyc | Bin 646 -> 652 bytes .../libs/__pycache__/market.cpython-311.pyc | Bin 2130 -> 3019 bytes py-client/libs/market.py | 23 ++++- py-client/main.py | 12 +++ .../ipo/__pycache__/boot.cpython-311.pyc | Bin 2896 -> 2893 bytes .../trend/__pycache__/boot.cpython-311.pyc | Bin 8498 -> 9702 bytes .../trend/__pycache__/open.cpython-311.pyc | Bin 5590 -> 5550 bytes .../trend/__pycache__/runtime.cpython-311.pyc | Bin 2189 -> 2295 bytes py-client/strategy/trend/boot.py | 79 ++++++++++++------ py-client/strategy/trend/open.py | 2 +- py-client/strategy/trend/runtime.py | 2 + .../zt/__pycache__/boot.cpython-311.pyc | Bin 5624 -> 7553 bytes .../zt/__pycache__/open.cpython-311.pyc | Bin 2010 -> 2026 bytes py-client/strategy/zt/boot.py | 65 ++++++++++---- py-client/strategy/zt/open.py | 6 +- .../__pycache__/test_market.cpython-311.pyc | Bin 0 -> 2187 bytes .../__pycache__/test_trend.cpython-311.pyc | Bin 20725 -> 21184 bytes py-client/tests/test_market.py | 22 +++++ py-client/tests/test_trend.py | 78 +++++++++-------- 21 files changed, 208 insertions(+), 84 deletions(-) create mode 100644 py-client/tests/__pycache__/test_market.cpython-311.pyc create mode 100644 py-client/tests/test_market.py diff --git a/py-client/__pycache__/main.cpython-311.pyc b/py-client/__pycache__/main.cpython-311.pyc index b99eac335e95e67338ff115cd64cf5ac921ce414..bb08f26499d997121261a72ec5aee3de54fa103d 100644 GIT binary patch delta 2129 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 diff --git a/py-client/libs/__init__.py b/py-client/libs/__init__.py index c899b38..4ee0cac 100644 --- a/py-client/libs/__init__.py +++ b/py-client/libs/__init__.py @@ -1,6 +1,6 @@ from .calc import calc_buy_volume, trading_time from .lockfile import is_lock, write_lockfile -from .market import market_allow_open, status +from .market import market_allow_open, refresh_market, status from .signal import SignalItem, SignalResult, fetch_signal, init_signals __all__ = [ @@ -9,6 +9,7 @@ __all__ = [ "is_lock", "write_lockfile", "market_allow_open", + "refresh_market", "status", "SignalItem", "SignalResult", diff --git a/py-client/libs/__pycache__/__init__.cpython-311.pyc b/py-client/libs/__pycache__/__init__.cpython-311.pyc index d08283456a6a9e4002b5a8980e333549ecf09317..542a67aa630c412f1b0f8556921af5c305e86101 100644 GIT binary patch delta 287 zcmZo;?P29z&dbZiz`(%3s5>?5{6t6mf$H9uNUic#AzA zWNLi;N`}uMH~tdRi^<7MDvpVd&&jkQTm5&xj8gW`}V6&yqa!NIAcLpV566XMysB)z1RPIl_#67mgP zq#wntqvSI<6x=#V1~+++-}d9ZH*;>%4@xZn@IK3zMM;myV|jXcOb{Y$%dWC~OWD4y z9N$sYrvx!V^PTuxb2g;MVtgu zcLC^TOxq0{0Ck5qF6RsUG%wZ{_t$Ka>LDAAgQQ>s5xXN*ux#o+Nf9LllVXrF&P5^$ z&b@59-n>z1IF`|^-UmPcXcn9th|Fr5GCx^Cetni0fr-|ML@C%=* BPr?8I diff --git a/py-client/libs/__pycache__/market.cpython-311.pyc b/py-client/libs/__pycache__/market.cpython-311.pyc index 9f8ad486f0ae12ea57a805e87ffb3241c1080a3f..b7f517046140db67a31927c3789735a53e496246 100644 GIT binary patch delta 1677 zcmZuxT}%{L6#nkqnb~3gU=bFiRZs-8O|3Oj8c` zd(L;}S;3Db?mI4*9RSFd8z+C2uDeSkmjU#Ffsp|GSxNkafe8VjgZe1}qf88l9g<%n zV3J>E@-YhdZGOd{CqSjc0Pgdm*5$V|TeE{Hs3t%UB*eG!07&^Df!85_CE$u?-(rHbAU(9ASN!~~!d`kCz0I4rV%+Ify*WwHDPtCF6)Yy~M-OtR}<798#ynQD% z(4V^eC>g!5I5DvB@NshFqIr8{;rpRv-=oxoVLs@eA04a6wd#5F;u)p3OVv&U*{(CR zTHp1;$=k)7*If=^;p$`aTCY{r7?z4igE1~u3O<)%N?T;BvyxFNZ;3dYI-0e1Rc{Ie zTiR#cOIAp4*1JQq;)%}Ipik7+1DK`VT0ncv($v!}ArU}DMMZ`7I(NA>wN+C?ZMg#4 zTL6jyguDcZA}z!1P1q0JIFqm+O4tuQ*pZdS9l1D?7DQ&1lP%Qa`QKC;s_$d@kVHG0PqoQeT375pEXA%PmYnzfj}_a6zozvwYRMu2SQ1~?Y%uaM3*Z7=f#Wr2<(j=}bbq7Ce`rtt0VQm9X-^ihgL$(dOx$5O$ z?IquwS@Dx#Fkl_~TWITejBnZB%6lC_)*VoHYdiQ)o_FMDW6V}oID@jpe;;GoR#`BU zzb2+%I(6k#)H&lRi@J@!Y?W-&n`t;np#}YomWb08*D7s2Hg8QsB-IuSQcJ{ZR$Q;r Htk3-yF1pPp delta 800 zcmZuuUr1A76#squzTMsJZq`=LX$8}Xw#xq4#2(TysVIzDO$jfNnBQHgS0~rWL}vP6 zAkl+ziy#Pmv5;Pb97c`!0-Id`8elre&3gxuZOGN z`g|S$c=c^*6 z0#n@@OA{_@GM6+@nsN7nQ&l)gA?n!?Ym&>Ky5EOLjl*4b^(DKLJ$I94j6U=z01N>h zbCv!eXUrF@&)j8K=gvlutrDGk)(E41mS+9?IWXeh=bYt9p1z^LJGx&Z=m*CveN4GC z^_%5L4%4(BIr7gOTLxiV%9FffZ_cTML3syvjXFf0LR3!9$ngoy7|!sH8QPTX*zuaW z)J(3K&!vdV@BxS_=0~YTvM*!iuGG@QhR4&HC^Jp~DAOrJH|lJrO%F{n0O4>rY#b9M z|Ja~0s$~ZTbv=_C$V_PC#%TZn0F!F~=CS3gHlMgo&TR)muR|-5)qJrwR;rB^18t>1 z+hTArXl*M;D9M%mf=&n y)@hsvK!g@cLcj_oglwT|-u6Gl^eYkbiW0fhe69kC)Y-5??XVs@DE*JwTYmwhu)7fe diff --git a/py-client/libs/market.py b/py-client/libs/market.py index 21192e9..1ae2107 100644 --- a/py-client/libs/market.py +++ b/py-client/libs/market.py @@ -1,10 +1,13 @@ import logging import secrets +from threading import Lock from .http import get_json API_HOST = "http://139.224.247.176:13499" MARKET_URL, PERIOD, HTTP_TIMEOUT = "/a/market", "60m", 5.0 +_market_lock = Lock() +_market_status = "UNKNOWN" def status(payload) -> str: @@ -15,10 +18,22 @@ def status(payload) -> str: return result if result in {"UP", "DOWN", "NEUTRAL"} else "UNKNOWN" -def market_allow_open(api_host: str = API_HOST) -> bool: +def refresh_market(api_host: str = API_HOST) -> str: + """由后台调度线程刷新大盘状态;请求失败时缓存为 UNKNOWN。""" + global _market_status url = f"{api_host}{MARKET_URL}?period={PERIOD}&t={secrets.token_urlsafe(12)}" - try: result = status(get_json(url, HTTP_TIMEOUT)) + try: + result = status(get_json(url, HTTP_TIMEOUT)) except Exception as exc: - logging.error("获取大盘指数失败: %s %s", url, exc); return False + result = "UNKNOWN" + logging.error("获取大盘指数失败: %s %s", url, exc) + with _market_lock: + _market_status = result logging.info("大盘信号: url=%s status=%s", url, result) - return result == "UP" + return result + + +def market_allow_open() -> bool: + """读取最近一次后台刷新得到的大盘缓存;未知状态时禁止开仓。""" + with _market_lock: + return _market_status == "UP" diff --git a/py-client/main.py b/py-client/main.py index 580eb84..cc40e97 100644 --- a/py-client/main.py +++ b/py-client/main.py @@ -4,6 +4,7 @@ import logging import os import sys +from datetime import datetime from apscheduler.schedulers.background import BackgroundScheduler import config from dataclasses import dataclass @@ -22,6 +23,7 @@ logging.basicConfig( ) from sdk import APIError, Client +from libs.market import refresh_market from strategy.trend.boot import StartTrend from strategy.zt.boot import StartZT from strategy.ipo import AutoBuyIpo @@ -122,8 +124,18 @@ def main() -> int: id="auto_buy_ipo", replace_existing=True, ) + scheduler.add_job( + refresh_market, + trigger="interval", + minutes=1, + args=[config.global_config.api_host], + id="market_refresh", + replace_existing=True, + next_run_time=datetime.now(), + ) scheduler.start() logging.info("IPO 自动打新定时任务已启动:每日 10:00、14:00") + logging.info("大盘信号后台刷新已启动:每分钟一次") STRATEGIES[config.account_config.strategy].start_strategy() logging.info("%s 策略启动成功",config.account_config.strateg) diff --git a/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc b/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc index 141828ad7be657a7fc6939e0e575d29721f5800f..0e1d348a9074b41ca660757bc43435c73ccbf99e 100644 GIT binary patch delta 241 zcmca0c2O>|vk0j@h1( zZSp&2)5-QMJc2AWj5SP>AZ=if!ZF!_MV*(khN*@%g{y`og?kOprc0r7HIWI340|NsC^Z7|x${TrYnHbM)_GU_8WL;FtR3bZ>hgos*ZDtup*2zDZ zO&QrH>#;~r)@9+D9LgfY%TdEr!uo|igq%MUwm_d_ovIgtK&HGqq zGAfw3SjCj*7iGs3mL}$v#3W^=>lWsg#1vHOCg)_P=9Ns2WY=b_o!rVkg-eiC`2&L> wt8xeHWPOeo0u0hF+Du2Z-S+-;sVU;zGXF{Be?d$P(#D`Bg%~pRLh?6!lN5dmV?@ zc6{~u+Irx{XvZ#BMymoI?V~h(57a zbR0JXYQqYNzCeR@2_kzDcWzm^xoo0M9sU)$am3H=EDJ1TR z$K@S|rEpS-<4CmLGGpXeZ|Sj zwxR?ennYG*g(X5gq)feaMa5XXikuFgXb=q(+{>SXU;qy5_rn14o7EqtJL2J>p6{;3PDis2tW1d zG-eB_bp`aMSn1oVei%MKWx*V3m@mM$_OBzEAF^Q7KCnASpAvqY5<@f+D zA$th~cmQx|)3Err7cZVmd!R|BMB9 z<(XL4wIt_Sa@mu0tsdGr%?rc3MiQf|$C$B!^R}#eWzM}a%QxltrVQVdFXOo8`Me7_ zewcs2R^Ml<$JShDGHi90-JD}LXK0(BpHDOaY=*lYN4pR1t`v3x+-*bcKKKS|cGl{@ zsnHSHhlpI;*6xFQzMA${Jq{8#(>T4SrW|z^5=EM#-G<3899XALy35MSiibO8_$9(C zQ>HP8ikwdMJGTQ-Nze6`sg>oPa^)vQQvhYzb6G{@ZmS{1egFU$5x_W72VHKpwOmxs zxV-`gDd?>znvZ$t>Ra44RH?q@-l2Y0?iEBf1-(^^Ce){O;f*NuaQy*5XWn6r?OE(?tegoGFfW>k2Pi>?26=+u?xo z7dn7J>!3~9TGRlftSQ^8IX~ZXbb z)#q`kvlZ*pBuyR-|N7xgb?)l<*{f%hWMJmj(ZvCOEjE#ek*~$o1lkDfB0w|d9U{F; zU>^aR@`RgkErC@egKW`AEEoj8=6m_UgdT8b z5e^exSfQUhQh5-m&DC$$JTpRxbENa^f!`e%YrVERTi=#-Zpk^fWQDCcVXHb^-KqYw zYF}CaYvmMgAO6_`-g}?-P7A&%p=?TUP6-vXxpIQxAb7f z9m=v@xpiz;hV9B5p~6RI)>Ama4>3dC)2!ok=V(iot;?}>8QSvtqKW$BnhpE!HhA7_ zgS%}s;|sao*w}7?dzPB^<@$TeQ2TN{K0@5DPbe6ZWW?m0hZk>L03fVP86X7!XMuwh z8Y9W3jydgV1fp&))PwTEj0V6J2oy4J;px>EB@w0I)r8nj*I2v?^~oIo@bJzbXGi`p z^V^ZR+gE3AUYfmmW$w=Dx%0!b=Z??)`O3quPyGAV@XS#9!!#>tVc z#ENi84pY|AEVLgD;XX;Bgt0JExb$fSfJs@b=@Viftvoa*okH&YbWjQPky0k!9Z_gO zP@oE0eHh|clyIRgF${#1a1X5;G!st7lt@&fSCq!Y`=uD=k3tzk>4em9v=S)fK8e*A zfLq8YVTAv%o1$?sDNB4SK1C{a6327uP45QR`V9O)Q@{519)1A!pjD_f4=+P6 U>OiPI53gaD&OogH572k~ADt^YLI3~& delta 2080 zcmaJ>ZERCj7=BNCZ+mZh`@OE~y0slJNVBo2Xc6T@oovGojBcXdAGW3UZtJ*hm(x2? zQ!}CvM;tJ6hR*L{9HIlJPDK&)kAjJstfs#j7k{{RDxlp4>e5Iq!S! z+va_q^Oo-Y;4bUi7K;f0U}bq?u*q}GT8Dl(wyt%A6XKg3OL0p!C)FHa#DLTwc@HpRP}286 zsF4KG#N7AjgdY7wRCddVDHsd}muIJo&s{7Xf3GHb8#?SZ{A-kXs3MWa9Me`fKM^NXK2M;e@JbU%RE2UT8t;9>KUi^6R;ulL3r-IRFJe}%G z^tZ+0@$_&?ja0VL=-e5$TZfpr-;JA*V*>!x13;!j2e2Q?!dwUcpw1)!fF1z->Ysbv z{GG1mG&;!v%y3jTx}G+wH6pUEz!*XT-3uMv}e8fv* zTt>w)RqlThH_$DQ<#;+3PbhMMkz#5L8);O?B>Gb^B@$DV^ge8(1+5gQ(+ieO>}4KQDa!`#d1o*j{?AUs?axRuH+QZ zNZG-g4Zi?yHvC3mf5)|2U@fD&#vhwDyT8k>zidNYc0GQWwnJ-OFIn$iWu}eQT=)btC%UoD%0m_gT~+Bx z+2bKQyjJ8Uk9xNg)*B$-dR(sBYCg;EMYug{obr%6g&Jb0b0dv0S$?pVw0UZI1Y@^n zxh(q>d97}MyygvzWR2LJWw0x&$Byg0orSD9Ydon3n9*13BT#Rta;TgzWSOjPP@8Ua zon!%K*yI6`zh?CpqV>J8NE692I?(q zGLeeJW0`>nj;V=sfk~wL(%3-r8)=@44J9Hw2GSX|axLf`#?*kglct$?T9ONf*wB!i zk_vp^u%blNM0^jn(<2T52niJ-dugDJ6dE?5c4BP|uglP>LdRouVQO}KZx!}Zm(S|v z4mL&+ITd(>EczcZl?~u%m`vsDt)w^5dk=8VG9b2fJfXej*MznA0!KaVo-*aQmOR%o z&$V1L0P7y_nQWf&p4#zVUq0NKW4iK8*L?LM9|doF_{&Yr+XL`bz~71VmjHDl{bh4~ z*IKx|*5A$Pzc!+7P9H)$Lpn?a!PI&o!}UI;(Z?wKoo^z6)xI80gdH@aiB(5?$1925 znYMULiEAr`OULvpI3kT&Y5OqdQep0uPZ6s{uoix*I6(yo}|qx#hZ)AV+T?=ZT2s noUG@XgdkvGY;R|2V=r%TWo|BKZ(v}t)!!2YNzedEleiT2FXJMx diff --git a/py-client/strategy/trend/__pycache__/runtime.cpython-311.pyc b/py-client/strategy/trend/__pycache__/runtime.cpython-311.pyc index e81d92c6a4d5a3d13a67749596fd3f2195b8ae9b..5659c4df63e0fe1c88af6520049d8c0d31c4a09c 100644 GIT binary patch delta 693 zcmZWny^9k;6o0eH?&foo%_XN0Kkm|KkPBE?9GrF{9QXleA=@l_Gn3q!*za z9j~KtU2H^MuS>uHZn4H`^8*3+OjJ9y40r&{c?GOB`*PLpvGxHB-PzBFJ+kaPJwhI^ zCrKjizvMKN3FbG+I}+#L2zPjyb@l*&Z5+U`d$wcTDeYGQU};ei@>pU;1+|Gt4g=xS z@dRx})B{SBES8#SD#lD@AUQVl-2@rO6rKFk><`qNES(paj6>;54Cs_2mK9my#X$Me z+&B88d}Q@oE&!l%YNxOBXV&q_qP#WN_I(X-d!6=ZxQW+Pv6}CXGnpaxeJm-WLz9?h zh&+~~;v_BF)>$9}DuOiSDVnN~rSdJB#N)6KSjaA=Y>HJl0a*4#c$khR7&2c5Q|=dX zP-`=_TrC}Ys$B6P$AP3r=&T3DErs;l0|5A2*^3gE34RZ6sXwFCngn1~B7`hpcMiM1 tU@%vff3h3y?KxbqMSf<#zk7vjui$@ouDIXIotMD4^n+}!j0;(Mj(^Wzu?+wK delta 625 zcmZXRziSjh6vw}_x4-T#yZ0l93KxvQ;&Md_3Bg8fEG&c|q{=qSxHA{e?A{);y9Cq3 zLIe#d&LtLh7M{gL5CmKQg%nn2ogZM6pdeVu%$hpP#>l-wK8`x`?a1&90!Ek~nvvyMk?q+kH^LEZ?O4nXoyhfE0%oytSc~djUA-$| zBWik00u5Ye)q|Na0VYJQkVnHkFy|SVJJx`S_7`IeU~-Ybn`N~%bnDqNfYZ;rM=!se ze0Y7b|L$mS|M=kT=_|65-zRSf%Z~}y*U)W#TGv)eJ2rr+8H9a4NQEVn%0?JJq@f@5 zHiRibl?Fka4N^sBBr%edrd(9jY%CZTiEz|xJmdp^i>AS&X_+!sJVja7+@Xkj{nSqd z4K}%0?HW_t&*#j$mFvSNOND}*n(bO0fRI7nWINwAx7H8!&Y|8B^B1MnZcEImjlSQ@ z(oAsQ7bOX*jHWaQX_D|n=(69C*bL%9uU|MSe3lZnDeU~I)qbG#8On$u&T@` zcS@7EDoaYoH2}ZuTT#riklzuD0OY47KahX77V-sqHosxNxZNg;XZSyyOZHQS^Afbn NU&*C2?Vm2S None: """打印策略启动时的账户、资金和持仓概览。 - - 该函数对应 Go 客户端 ``logic.Overview``。为便于单独测试,可以 - 显式传入账户配置;未传入时使用 ``config.account_config``。 """ account_cfg = account_cfg or config.account_config @@ -99,20 +97,24 @@ def StartTrend() -> None: open_watch=DipWatch(), add_watch=DipWatch(), profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct), + executor=ThreadPoolExecutor(max_workers=2, thread_name_prefix="trend"), ) Overview(assets, positions, config.account_config) - while True: - started_at = time.monotonic() - try: - RunOnce(run, signals) - except Exception: - # 单轮错误只记录日志,下一轮仍继续运行。 - logging.exception("趋势策略本轮执行失败") + try: + while True: + started_at = time.monotonic() + try: + RunOnce(run, signals) + except Exception: + # 单轮错误只记录日志,下一轮仍继续运行。 + logging.exception("趋势策略本轮执行失败") - elapsed = time.monotonic() - started_at - time.sleep(max(0.0, 30.0 - elapsed)) + elapsed = time.monotonic() - started_at + time.sleep(max(0.0, 30.0 - elapsed)) + finally: + run.executor.shutdown(wait=True, cancel_futures=True) def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: @@ -148,14 +150,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: logging.exception("获取持仓失败") return - # 5. 更新状态机 - try: - run.state.reconcile(positions, run.orders.data) - except Exception: - logging.exception("订单状态对账失败,本轮禁止自动交易") - return - - # 6. 验证有效开仓信号:排除已有持仓和未决订单。 + # 5. 验证有效开仓信号:排除已有持仓和未决订单。 allow_open: list[SignalItem] = [] allow_codes: list[str] = [] for signal in signals: @@ -164,16 +159,48 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: allow_codes.append(signal.code) # 6. 获取持仓和待开仓证券的实时行情 tick。 - all_codes = allow_codes + position_codes + all_codes = list(dict.fromkeys(position_codes + allow_codes)) try: ticks = run.client.full_tick(all_codes) except Exception: logging.exception("获取行情失败") return - # 7. 执行开仓:必须同时存在有效信号且大盘允许开仓。 - if allow_open and market_ok and allow_open_by_cash: - open_signal(run, ticks, allow_open) + # 7. 更新状态机 + try: + run.state.reconcile(positions, run.orders.data) + except Exception: + logging.exception("订单状态对账失败,本轮禁止自动交易") + return - # 8. 持仓计算。当前 Go 版本的 managePositions 为空,保留扩展入口。 - manage_positions(run, ticks, positions, market_ok,assets.available) + # 启动线程,开始计算 + # 9. 持仓计算。 + futures: list[tuple[str, Future]] = [ + ( + "持仓计算", + run.executor.submit( + manage_positions, + run, + ticks, + positions, + market_ok, + assets.available, + ), + ) + ] + + # 10. 开仓计算:必须同时存在有效信号且大盘允许开仓。 + if allow_open and market_ok and allow_open_by_cash: + futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open))) + + # 11. 开始执行 + for name, future in futures: + _wait_worker(name, future) + + +def _wait_worker(name: str, future: Future) -> None: + """保留单轮继续运行的语义,分别记录工作线程异常。""" + try: + future.result() + except Exception: + logging.exception("趋势策略%s线程失败", name) diff --git a/py-client/strategy/trend/open.py b/py-client/strategy/trend/open.py index bc19391..e8040be 100644 --- a/py-client/strategy/trend/open.py +++ b/py-client/strategy/trend/open.py @@ -1,4 +1,4 @@ -"""趋势策略开仓逻辑,对应 Go 版本的 ``logic/open.go``。""" +"""趋势策略开仓逻辑。""" from __future__ import annotations diff --git a/py-client/strategy/trend/runtime.py b/py-client/strategy/trend/runtime.py index 40b0a18..fcf3ce7 100644 --- a/py-client/strategy/trend/runtime.py +++ b/py-client/strategy/trend/runtime.py @@ -2,6 +2,7 @@ from __future__ import annotations +from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from config import AccountConfig, GlobalConfig @@ -42,3 +43,4 @@ class Runtime: open_watch: DipWatch add_watch: DipWatch profit_tracker: GridTrailingTracker + executor: ThreadPoolExecutor diff --git a/py-client/strategy/zt/__pycache__/boot.cpython-311.pyc b/py-client/strategy/zt/__pycache__/boot.cpython-311.pyc index 0fb171f577391fd4f654585d670cfd50231ba547..4810a0e5ab6790102dcc3141844b08abece20b9d 100644 GIT binary patch delta 3502 zcmaJ^Ym5`u6+YK9w#WAPvBuu_EBgSv%R6Pa3lVI1wFrVJ(1bN)Ew9H5yKAq}ow0ay zwq7b!mNY6$CJo!FQkN=hsWxq?LYpmA4HQxlMOAI&Hr7-tqwu!r$N!khGkeT;V`%98%DpYhJbl4Kwp zV7x04OqPes880R(l9k~~0+L(uB&w3t;pziy;!V^fYs0mS_a*9*_2GKPmn9mKjp0TD z(o!jKh7Ug_Et4wF@ZlzDxm0_G4>wC8sp$+KZjsug)-!y#RpRzTsQtqKHrCj7q?I&{ z>;3~rX*??JO{EgsPsC$sB}KIka!z!mj>U%~^1zYdXyVOx$um3=HJfcqwHp9*JB|0# zxO`N1981Xq%0OyZrc1Ow+Y7{|UAI*?_yK@=kT}rXeS{835m|{JiyVt7)UJJOJ4ouZ z-S+L)P5`KS^ELZb()u;D$CWXH=TI6h0N?<~$3M3Mob_r$j(-!c*5mxsCZ)!-0Z?HO zKgWg&gI<##^qJh01z_4@&PI6ckaMLr?rN}%dA0wzYCO%R3P5QA?Om?Ha?Y!DiBD_) z<$~I2F_LPZw^;bO!E#eRMF6Jfd>;{1AyuoA!3tBalEKP3exZ|9ru38*V91KI5r=lc z-AbypwazAs(y9H^-PAVXRJu${0Ld;1Z#WijYdws(RM%vkcFR%WsyA&18??2qiiSpW zu5wd6z%*~pN5s;aK%26~6auKC3aVY@RmYSKVA^JCM%<=(M%iWx=gcjOcFd{HDFI+w zFzrS>$~R0NKys?$;vEoJq<5+Auh8!TNmMCkeXvzdQ!7}J(h zO30$SlF<{9<0(26r?T!>SSm$^qse&W7>)N2oJflR@bIPIe>i&j!EgTX=-N~oVSMn% z(fca)Aw&^4}X2>;rRDMEfjI0XhK_&tVM!O(KYx@TU3_gO^QtGP=xVFS0Gu5 zWGRwmNL<=2?{4DMJiavWlNDMpuWlhb|40fjz|<{0_31;xGyO%F#F zB)%SGl4Nu^dL$m1%XM9h9FGntrHzMtl%+17EAvdZDk&*?Qn$+kn&MlX(S=ylc&+Z~ zPtjOB(w9CN>5ImObRk}#3ReM%GG+=ojNx(NVeOzp z&qa0#b{fUNz`93!G2Z#UR4Nf;^|mySXWix?09*tWCbe6RfcAmYn<1d`bcuq=Um7Y= zEh;y~0Zh+HjdFq|<(Fhn$fDb5TuvtxioqJP8V;~*IgDQPn6Ak9VHV3q1b-V9NYFyY zaLuugdaO1-rKh#2T6vmW*$m2u|8_!t%VZJZnBhj#xv?f_CE@ z_u{Ut{fSgmp}0exlNCxM*mJQ^m;e2W#2qAerR@2InF}B5J8AY3H4lnUG~qdWqR?TIGk=-+p-I2M=EV?W1d#L%adk;sPJC z&>;jN!D<@A$AIPqlp zxunjr)W+=Qh!wL`#y0l7KqX8xY~-$!jao+xh|CcE^_ON9m5#txh5G4H4Z zkbwoJ;uo5-kT{C=7^-vt2_8N!U%1+4;KK9>fsx?zjx5#fk%*LvMIyR0QtBX)OoZae z82ytqNq;Og97|IgA68Zwks%vz!qYT*@_yv-I;T4l1AX$USTqr1En5$=IlJa6(IM>q zsr~`Qxc$1AE*YgmafRYv43q@>OA&i zRzHkP!6ee{G(F7PIRlloaSSGk*J-2uu?oVnhQ-GQRInO=9r82bnPf^zC*qsvDFDDb zt9+RN6gfgj0a~)q@&PnyzY8>%cV*!V9_=52OTM}+EPAxF!M%MeNM{iilU~A+P!Sdr U(Mk4@B0NE>d3?c_5VnK=1AoCf_y7O^ delta 1618 zcmZ`(eP|nH7=PZ}C3lzPlBP{^X)o)?KDwo=bDvt)Ev|8H6UGpxb)4ue&7~vhUDLgH zRUC2IU^u_PmAA?SWff)}+dyF|6-1cRA^XR+1Qr4l{KF3vp`w3?Ajos|wmI-Pe*E6& zecq4fd4BJ6a`4afk@sae2mtWT-_zx1gHw?dS)5wiU-N5zi`N1K3}SGmpa}*s{HAD0 zn&jF7X3z?0A(sng*pfBb<)Rs}qFU7DlG$R#w3y3-W~5&t#-NL~3>_>ipGjQ+T?Kro5_ez*$< zKz_f-1DuxF73o(Zv3G-?N2*YkJ+GMv`%X?|d{roOo~<19Se2XQ05090A;4Y_-^Xr* zx^_7|o)bV7$}w+Lg>tLM%WWQ;^#NS+d7~Hjmn8N}NNvw}767O34xdS~qjKE$vc$H9 zw-5zy$c~4fY2S3m~}(^W%udoI%fu z0ab-6e{by0&U-w3cc12~a_p+K+Beh1j>@aZ5PL&jnYt{@1^_MvyoCe|I0zY$JrP}- zPypa5?eV9(e|+VWA5NV7{)10{`e>HD8Lg46EZK6plp)Q?XoVJv`pD$-`bfSomg%9m z6-5QnRS5SZV8nC-3MP=Z?V@8-jGJP1DW;XKM_7%p1_AR(v3_(Vg2b}1{X}H1#x|~t zpaJVoF)htt!K@Uu=rJM|S{#jW4&``*zW-IKR#Vyn}tn$9``8 zx~CzAUXyN$>Y}KgeRRe?f9!m1&c7gL>tc4^{n?E+l~v;DR8&2?h+ zn?YS4opdH?QP(Lu*Ws_YRmBP|OW^>r1Qtv(OC$D%Lf$O6N@*tCvD-$R{8&++pq0^* zv)o-m?wpmUV?~FGxO&-QmyV3*O)8;{aDXR5?F%&0e9nn0QQ(=;s5{u diff --git a/py-client/strategy/zt/__pycache__/open.cpython-311.pyc b/py-client/strategy/zt/__pycache__/open.cpython-311.pyc index 0dfea264355879b5109e441f5ba5c469c593204a..01a2f163f41e48a36a34dc337774351f94db1803 100644 GIT binary patch delta 106 zcmcb`|B9b?IWI340|NuYD*35dt0(eGGWtza_hd=sisIf_@STN+JHH?`FTOZ4Jufk5 zb0KRQBO}k`8*Gu>w>ZHPNr}a&lMUI$xeOT?7#JBC7>esB2eDW1t1<9OG;o4oktPEJ F0|1Ga9Mk{+ delta 107 zcmaFGe~X`YIWI340|Ns??B6L_5fgbO8NDW|du}ZG&cel+Uyzy?pOjdfy1AS+jggUi z@;$al#gz<2nhXpKMd}O;48J&Ra`RJ4b5iY!3>X*~7#SECiW?>;vsVZ(@H6@`egKnT F None: return today = datetime.now().date().isoformat() try: - run.state.reconcile(positions, run.orders.data, today) + signals = init_signals(run.global_cfg, run.account_cfg.signal_allow) except Exception: - logging.exception("[ZT] 状态对账失败") + logging.exception("[ZT] 获取 dcm 信号失败") return - signals = init_signals(run.global_cfg, run.account_cfg.signal_allow) candidate_codes = [item.code for item in signals if item.code not in position_codes] codes = list(dict.fromkeys(position_codes + candidate_codes)) try: @@ -63,13 +63,50 @@ def RunOnce(run: Runtime) -> None: logging.exception("[ZT] 获取行情失败") return market_ok = market_allow_open(run.global_cfg.api_host) - if market_ok and assets.available >= assets.total * run.account_cfg.min_cash_ratio: - open_base(run, ticks, signals) - manage_positions( - run, - ticks, - positions, - assets.available, - today, - force_buy_back=datetime.now().time() >= clock_time(14, 50), - ) + can_open = market_ok and assets.available >= assets.total * run.account_cfg.min_cash_ratio + force_buy_back = datetime.now().time() >= clock_time(14, 50) + + # 状态对账与开仓判断并行。持仓线程在自己的线程中等待对账完成, + # 以保证它读取到最新的底仓和做 T 轮次状态,避免并发写 State。 + with ThreadPoolExecutor(max_workers=3, thread_name_prefix="zt") as executor: + state_future = executor.submit(run.state.reconcile, positions, run.orders.data, today) + open_future = executor.submit(_run_open_signal, state_future, run, ticks, signals, can_open) + positions_future = executor.submit( + _run_manage_positions, + state_future, + run, + ticks, + positions, + assets.available, + today, + force_buy_back, + ) + _wait_worker("状态对账", state_future) + _wait_worker("开仓", open_future) + _wait_worker("持仓管理", positions_future) + + +def _run_open_signal(state_future: Future, run: Runtime, ticks, signals, can_open: bool) -> None: + state_future.result() + if can_open: + open_signal(run, ticks, signals) + + +def _run_manage_positions( + state_future: Future, + run: Runtime, + ticks, + positions, + available: float, + today: str, + force_buy_back: bool, +) -> None: + state_future.result() + manage_positions(run, ticks, positions, available, today, force_buy_back) + + +def _wait_worker(name: str, future: Future) -> None: + try: + future.result() + except Exception: + logging.exception("[ZT] %s线程失败", name) diff --git a/py-client/strategy/zt/open.py b/py-client/strategy/zt/open.py index 5749f7c..6cdfed8 100644 --- a/py-client/strategy/zt/open.py +++ b/py-client/strategy/zt/open.py @@ -9,7 +9,7 @@ from sdk import OP_BUY from strategy.trend.order import PlaceOrderRequest -def open_base(run, ticks, signals) -> None: +def open_signal(run, ticks, signals) -> None: """仅处理 dcm 信号,使用趋势策略同款反弹确认建立底仓。""" for signal in signals: if signal.signal_key != "dcm" or run.orders.busy(signal.code, "BUY"): @@ -25,3 +25,7 @@ def open_base(run, ticks, signals) -> None: if run.orders.place(request): run.buy_watch.forget(signal.code) logging.info("[ZT 建仓] %s 买入 %d 股", signal.code, volume) + + +# 与 trend 策略的开仓函数命名保持一致。 +open_base = open_signal diff --git a/py-client/tests/__pycache__/test_market.cpython-311.pyc b/py-client/tests/__pycache__/test_market.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f6a22ec029e75153bfaf30b04ab6ec1323f8eec GIT binary patch literal 2187 zcmbW2O>7%Q6oB9CuI-Jpu9K#uc9p0Bdg#)`rAVlNS|qCpkf1t%TcKKr)z&j6PS!un zj>(TW1Stp93tWIx1*agCUXgMn5(kd!$W^12kRow`o2&E?<%Hg>?bwM0h_|z&nfGSi z%+CAf?T@io6ae7-)HjQJ3cw%OX-g<*kb?&R@EuT~#DNmY5rS>SQF00esyb>(%V|=l zIiXTG7cNC|5dsRl4K%a?G|ZGmz3p^U1=v#Cz1#qc(#Vp6eLk!JQx*r?woL%oYIlGD zRkHFm^}kSS5`WMxmn*^+#Y)-Dn8XiNZINFv6@R#7^Cc!M+i@z(R;9|y{u#~+oVg2D zV9IwK1NxRi4lc+KWsoBjatb9IkW;C$0XdDTSuiz!=(cox%g!&bJIobszBN}$7ywiN za`310Bf4yqP=%^gE0Q946xvh(w&X{nyb1(9hab8|AB$)O0fmM)3BXoo9MyXL$PfU9 zliKp&i*OBGZCP1S@4+%5a1ThZW+xEcA_k6oy+P<*AJ@x*8Oa=-bBg!fRB*DXPnfV4 z-AdWls4Z+iZ09jRzH;|HllTT_qQ=YCXSP#gM`spDD?QlF)KN@K6@EYTIB0mvdbD2|c@h~QiMpGXNjE1aR5l&TC{fnrz+L5nSsV$f* zd9d;r!nK!A#xwOVGw`;!zXHHI{2n{&jhNe`zg+%p+DpIhz4n0@%hqGrMl9O|GB87e zde0boVvOw?V-G*}CU4ZoZ#KqnZi~7x-7uy-W4fsx4-F9vbH3;^F=N{7{*u$GQSc48zQjr2$Qs1MSj3m>ZV z!tqG$#jpi=2l{Rik>eiKdi~H7m*syzj!c~r(aAILUmy=>IELPjR0;*BSZ0`g+#+Qb zD->8>@VGSI&AgkL|1e{Q1CAr}*%{6&oDX4olOVUye#>^;fMk9aiIN!ebLeEEhrohe zbZVSg_nk_9DL{Aa+CM|vXA17FhuWW)w^!@eXB*dNz1UnmHrI&FAu)53o4NkXuGWp& zhB50Iv&d|FXfO?#os-P=>bsqGw_D8i8@#U@PxQpDo_L&msh*r{Bq!_oR70Qg+C@_P z{iSqV-HB`Ik;u*{NsmM_X2{nqt8AB;W%*IdDphFB!M0&pm`DN(e-1-2hA9LN5DIzd z9Dh%;!qn%!BJcNl>uD-#XsQXFDyoK3*gaYC=i5$jBPF|7wk*@YtrThkV`y=_FTP$Y z7X=2{kF{H=QYF9C%6(m_RHnLs8y){Pw``XMTOCJ78m_@7a8e7Ux( eCOtK|rwy;avvK>2`HlH?`7~9~E_iBEZvSuM(f=3# literal 0 HcmV?d00001 diff --git a/py-client/tests/__pycache__/test_trend.cpython-311.pyc b/py-client/tests/__pycache__/test_trend.cpython-311.pyc index ea67ad1f04f2057bcca1a4d59d2a4e4bc3b854be..87fb86f6fc014e3b6a35102b90b0a016f8baa12f 100644 GIT binary patch delta 6778 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