From 556e21d624acdd61db30bb7d79837ae577033f63 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Tue, 1 Sep 2026 14:28:49 +0800 Subject: [PATCH] feat dev6 --- api/qmt_rest_new.py | 50 +++++---- py-client/__pycache__/main.cpython-311.pyc | Bin 8368 -> 8858 bytes .../libs/__pycache__/market.cpython-311.pyc | Bin 3019 -> 2878 bytes .../libs/__pycache__/signal.cpython-311.pyc | Bin 3402 -> 3599 bytes py-client/libs/market.py | 4 +- py-client/libs/signal.py | 13 ++- py-client/main.py | 8 +- .../sdk/__pycache__/models.cpython-311.pyc | Bin 16581 -> 16197 bytes py-client/sdk/models.py | 39 +++---- .../trend/__pycache__/boot.cpython-311.pyc | Bin 9702 -> 10362 bytes .../trend/__pycache__/log.cpython-311.pyc | Bin 0 -> 6522 bytes .../trend/__pycache__/open.cpython-311.pyc | Bin 5550 -> 6937 bytes .../trend/__pycache__/order.cpython-311.pyc | Bin 6451 -> 8229 bytes .../__pycache__/positions.cpython-311.pyc | Bin 8967 -> 9421 bytes .../trend/__pycache__/state.cpython-311.pyc | Bin 11137 -> 11677 bytes .../trend/__pycache__/watch.cpython-311.pyc | Bin 3275 -> 5091 bytes py-client/strategy/trend/boot.py | 95 +++++++++-------- py-client/strategy/trend/open.py | 23 ++-- py-client/strategy/trend/order.py | 28 +++-- py-client/strategy/trend/positions.py | 20 ++-- py-client/strategy/trend/state.py | 10 +- py-client/strategy/trend/watch.py | 99 +++++++++++++++--- py-client/test.py | 4 +- .../__pycache__/test_signal.cpython-311.pyc | Bin 0 -> 2700 bytes .../__pycache__/test_trend.cpython-311.pyc | Bin 21184 -> 24897 bytes py-client/tests/test_signal.py | 33 ++++++ py-client/tests/test_trend.py | 52 ++++++++- 27 files changed, 334 insertions(+), 144 deletions(-) create mode 100644 py-client/strategy/trend/__pycache__/log.cpython-311.pyc create mode 100644 py-client/tests/__pycache__/test_signal.cpython-311.pyc create mode 100644 py-client/tests/test_signal.py diff --git a/api/qmt_rest_new.py b/api/qmt_rest_new.py index 015d1a4..bedf133 100644 --- a/api/qmt_rest_new.py +++ b/api/qmt_rest_new.py @@ -934,7 +934,8 @@ class TradeDetailDataHandler(BaseHandler): ret = safe_call(get_trade_detail_data, self.acc(), account, datatype) if ret is None: ret = [] - self.write(json.dumps({"data": ret}, separators=(',', ':'), ensure_ascii=False)) + result = [fixed_fields(obj) for obj in ret] + self.write(json.dumps({"data": result}, separators=(',', ':'), ensure_ascii=False)) # get_value_by_order_id() - Get order or trade details by order ID class ValueByOrderIdHandler(BaseHandler): @@ -1080,7 +1081,28 @@ class HoldingHandler(BaseHandler): data = json.loads(self.request.body) account = data.get('account', 'stock') positions = safe_call(get_trade_detail_data, self.acc(), account, 'position') or [] - self.write(json.dumps({"data": positions}, separators=(',', ':'), ensure_ascii=False)) + holding = {} + for position in positions: + stock = position.m_strInstrumentID + '.' + position.m_strExchangeID + holding[stock] = { + 'StockCode': stock, + 'StockName': position.m_strInstrumentName, + 'Direction': position.m_nDirection, + 'Volume': position.m_nVolume, + 'OpenPrice': position.m_dOpenPrice, + 'FloatProfit': position.m_dFloatProfit, + 'MarketValue': position.m_dMarketValue, + 'StockHolder': position.m_strStockHolder, + 'FrozenVolume': position.m_nFrozenVolume, + 'CanUseVolume': position.m_nCanUseVolume, + 'OnRoadVolume': position.m_nOnRoadVolume, + 'YesterdayVolume': position.m_nYesterdayVolume, + 'LastPrice': position.m_dLastPrice, + 'ProfitRate': position.m_dProfitRate, + 'FutureTradeType': position.m_eFutureTradeType, + 'ExpireDate': position.m_strExpireDate + } + self.write(json.dumps({"data": holding}, separators=(',', ':'), ensure_ascii=False)) # get_trade_detail_data('account') - Query account assets class AssetsHandler(BaseHandler): @@ -1094,28 +1116,6 @@ class AssetsHandler(BaseHandler): self.write(json.dumps({"total": round(info.m_dBalance, 2),"available": round(info.m_dAvailable, 2)}, separators=(',', ':'), ensure_ascii=False)) -# get_trade_detail_data('account') - Query total assets -class TotalMoneyHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - account = data.get('account', 'stock') - _data = safe_call(get_trade_detail_data, self.acc(), account, 'account') - info = _data[0] if _data else None - if not info: - raise HTTPError(500, "Failed to get account data") - self.write(json.dumps({"total_money": round(info.m_dBalance, 2)}, separators=(',', ':'), ensure_ascii=False)) - -# get_trade_detail_data('account') - Query available cash -class AvailableMoneyHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - account = data.get('account', 'stock') - _data = safe_call(get_trade_detail_data, self.acc(), account, 'account') - info = _data[0] if _data else None - if not info: - raise HTTPError(500, "Failed to get account data") - self.write(json.dumps({"available_money": round(info.m_dAvailable, 2)}, separators=(',', ':'), ensure_ascii=False)) - # passorder(23) - Simplified buy order wrapper class BuyHandler(BaseHandler): def post(self): @@ -1275,8 +1275,6 @@ def make_app(): # Legacy compatibility routes (r"/api/holding", HoldingHandler), - (r"/api/money/total", TotalMoneyHandler), - (r"/api/money/available", AvailableMoneyHandler), (r"/api/order/buy", BuyHandler), (r"/api/order/sell", SellHandler), (r"/api/order/status", OrderStatusHandler), diff --git a/py-client/__pycache__/main.cpython-311.pyc b/py-client/__pycache__/main.cpython-311.pyc index bb08f26499d997121261a72ec5aee3de54fa103d..8edc95ed2bf759df2e9431b28ba63065fdab2cf4 100644 GIT binary patch delta 1965 zcmaJ>O=ufO6n?W>$(Afxf0kv*jxGPnvST^Fl$fN{iGLE;#32ooZr!XU%~+Pa`tj^K zwu4)T)`yfrsTk-X6ynm<3B-XIx6nf`r4V{)kW)dzLZH2r6nZf95_&4LvXUx=&Mw2g z@BPf%@4Ywohi{BJZaW+{0D$<(&(psb906g%ROszLgA`E`F<_Ho&R8T10t^@_HBt=* zEGo4bt7P5BY?95;)k$>(rR_WxUmK3Xw$}+&P<>(BI|#(Ni2&}ReM6|0Bv+M-yRhwj z4TuYyv1QW)aMx6ElRU&lgqH_iLA3eb@`kst?F|D4;@T7d?g|x01OgC9H&kQ-X&<|S zT0;ihSb?#vIx2Ww64A`lJGVM~k!J$j( z$aG4bMqXCz#_laob;bgW1NN>1ZYbkSZfIPSx&$Cy*hku4H6~rSk+fH%*#Ctfb>k+| zGb`Z0x+sNlkn|d|h@00%DS|uj0n&$?Fq#!{>$+L$$DO!sU6c;uCNeN9;Ly4#Madv; zpXXHifCdF1Li$jJFab!EZ3YWo98qwzr2$%9Y9CJBSX_m zIXXYRuo%l~)A3YtU?HPT=T`<2N{VE)=}at@jpkRV4*-mwlq@W!D3O;^S)5x^X&(R#u)Nui+IFs+?V_chI~->>tmDH+xQAsfmc=en#2iV?%W5i{REV6)s#+|YAaoP}4()W<9*D@n{-`Ix6FlxI9?_goaw?&G z0B9Wmu(UIT#@Q#%K;H-esN()1<@m0pSW1&;b5vf)XmTu{qVE8}xt)j3f`Gzo#^bm2 z@<a;0v+=|0J4bSN+d|3TL7wlgch?MAgL@ zO{z4<|0B;r(PEB2+6C4tiTny7U84Yv< zI6IV6sMo|WmeaX7b#thm-3zp#Q|#wJ5c%1&z^S$OI6T8GEvkkIrMgw4u>^_767#w< zPg7Y zCk&Wp}kr_&TIAjVFz1#Y_1~gzwAQmj}taPo@+HFaDO(i3!xH(9z*02 zMDB#Q26tho1Vcp_dIIjUNq856B?vx-&PUL>F@9@m7mk(SSP_o>3ZWf;+c;{DBH#7I S9z>Dv)h*6eLPteD(*FSb9@xzQ delta 1519 zcmZ8hU1%It6h3!;c9YpjH$S_ZY_|K8ZIkS#-PjtmE%k>Qch`z0l0=YcWu5H3X*ZeO zN$*V3rVojfJQS(ea1o(OTZ%-a71Vw0gGGq=A}i?2V4r-^w}K!=e2_C~YSVc*-1B?S z`ObIeTJh@>9ba^GXaE4D@bUc6Mn_z_C2iym9G5Mnt{N%<3Y1s@BS3*FRTV(4gw2rN&vT|ojd{(FqrSz(HYD~#41S*P@tL>t;dX5z1QgV8xL81 z^|%rD&$=awQaUREYolQrzSFt8tORACUF$%*#dRsUmXUyEXb(#~9mO&;ZuH;eM#&%8{$|gO^)MKTkGi1G*B(W6GFtwK6#* z?rLe062EB)bsPW+&40Bmg@nZSVYYSzr&cZ7VH~;Tsb^hx@q(vMv2#nzaf_VW z+zXyJd85`i<0J;qBp}+;%ZM! ztpdO)al2=AKSG1r;}3^GlK$n%0G~iRxI}CO`4>&GmoO7HrFL~`w^(>=H7USDsBX&DbE66B*82}h!_B%n6Jdh4H ze@t8<1zC+gfE{sP?`n1DZjQCObFJ>&SI(V_-$hz_>1Vz4i(YE!C2=E}GJ>)iefaR9 zlLdfF*^&mEk&*^?i+V|eEiG3bk++^Lln3P>2G#P2?6p-Y*g&O{C;U~vTfPqfCh~=C z?EIbLOk5Vp)WM{WS%{jhS+z{ZVGdW(c?rdom`e?jW$|I^SWYzO->f=k z*{18$)JNTuk&y^1kNHXLLe1q*yv;rU_^0iKx{Z5ziC;&4;{rYs-=vR;zti^=9mqhF a43tTtK%$qc51>GzPmA|NH9MvtrT+te-(kW4 diff --git a/py-client/libs/__pycache__/market.cpython-311.pyc b/py-client/libs/__pycache__/market.cpython-311.pyc index b7f517046140db67a31927c3789735a53e496246..b544d7d960e6b3f852093a92edf54b819d8d1554 100644 GIT binary patch delta 532 zcmZ`#K}!N*5PkExsk>@XX(AZ}9m0bZlpex76-9?S6rBvQb_;8Dbz6z3C?X>0Qhb!2 zIu;Q`;i*f1Ao>G#@zUL>gLEl&!2+Rg;LW@@1M}v4_CIxbu00+J09q$6)~3%q0XjJA zFG#XeX`nK{gj2={L#M4V~&Uv2H*p*L44csO;vvC9G);2H=Z4U4Zz0~fFg{B4j=-8 z2${^dV!$nHGzl`}K`}tYqvGP;%zTLS5P%!#Jpl9|fJ%ENrLQYSGNr28W>U*3S$zmV zpXo*eu#Y#{!c|%(lpgFrw^L;e{cA5#xj~*P?fkI0YIkp7yc$4&AO@D!D{@(3AcFf>iM!l}Zl{f(MDwEH4+x4Chr-$wvy@R2Ks;>t7ff99yqRA)?}XgJ5#%HURLxD5)Ve6tlvtPXvQ=xwR1 z^kP4~tY7=7y+9x8bUJ;?zciY%y?i;U{!}qVM6l+7J*hsptj>n7yZG89^=SfT~5-f|k%i;~7|7yWp7p7(qIc%OHtYvr*0 zvCYN-09+V(c(b2+W`99+4vwA+XVcFI)+&4EXho8S8J`(6HE>a@|3-|-& zcBX_rkZHZ$cpea25y&iV;hY^&VjMz{g#~gPvgihR9~4uG!bO(H4Bd`sTx31&4FK5M zYlj>qv-_We@_D))8I7fVZ-?&u8{4HTs1v_8ParcMV1!{dxDbuqk`w8aC;)&;#$&Qd zCFIzwhhnsSsv;h(IZq7_o1@d>VPnBG7DTWIe{ z2ns!g355uO7hUnPs#!|JQ<0=3`gMi67(?(WT;PO&S+&KJ>8VI^GMY|JC*tB+{RFfa zlQKzJC1+xbDjA)Qt2SfGq>&JP`aR45;KpCME-nB7E70#;q9YDy*~Sq$KB zmTnZpzbxv!87(3WbX@ZryxC^((~QBd6odbPJc}G9dGpevMGaWwczSL7c=dF!>Him+ GTm1oq66$6E delta 664 zcmZ{hL1+^}6o%hqb~ou}6HThHXd9z8YLs3QDnbJlLJt-XmRO`>DX~e{ZP&ESZX%Jb z5y>G!!RTCaOhIfxgo;9Q5cDhvo3PMyp%>A6j-IN_F5dL<`2GiPhWUTy#mLuF;q6dJ z1OWJ5f4TCCdmsMjL-L0M-!~ts(`|l|{@~+*0%U+ zBP0=~-(5=nD+tqT{-5XsjRh8vK+QmG94ke`;Z>vfP$w|}ND3qY0OSJ0MF4PcnP?6s z`9~y0dx3M}1OP+;K;NTBflJ2^#b`6#@^{6!EykZ_`}D!kGoG9V0D3euiUxE<%tYqz z`2aTX24`XuRe;>0t73|7iD{lUvB}c|@%%~t4FY(J{`yC-d1mu2jRc2lDdvSe>sd>) z=`6D*RPtK6s1p|35%TNn#Zn1TT;}^n&IHpV%5P*%L*B1 z83X``yG2IWF-a|{#-gq&rM#w-DFBdx1JLVqFBnsN{B_>|7@r3kxRL7z!_7n^)5sjk zW9`XZPqjU@@!9@?aDb@&RJ5;l4?!PQdX?Fd3&y4X=Gok_OztV`&U00p*N(P63gpwnyMRQif&1xC`&&}PK85NGyuj{+S8q3Pj}0nelb1$2g5vy awvC*@e_6y3euxol5NyYqkHe&ru=6Td3} diff --git a/py-client/libs/market.py b/py-client/libs/market.py index 1ae2107..34565c0 100644 --- a/py-client/libs/market.py +++ b/py-client/libs/market.py @@ -29,11 +29,11 @@ def refresh_market(api_host: str = API_HOST) -> str: logging.error("获取大盘指数失败: %s %s", url, exc) with _market_lock: _market_status = result - logging.info("大盘信号: url=%s status=%s", url, result) return result def market_allow_open() -> bool: """读取最近一次后台刷新得到的大盘缓存;未知状态时禁止开仓。""" with _market_lock: - return _market_status == "UP" + # return _market_status == "UP" + return True diff --git a/py-client/libs/signal.py b/py-client/libs/signal.py index bfccea4..2ce60a9 100644 --- a/py-client/libs/signal.py +++ b/py-client/libs/signal.py @@ -16,7 +16,10 @@ class SignalResult: def fetch_signal(api_host: str, sub_url: str, timeout: float = 5.0) -> SignalResult: url = f"{api_host}{sub_url}?t={secrets.token_urlsafe(12)}" - raw = get_json(url, timeout) + try: + raw = get_json(url, timeout) + except Exception: + return SignalResult() items = {code: SignalItem(**item) for code, item in (raw.get("data") or {}).items()} return SignalResult(raw.get("code", ""), raw.get("total", 0), raw.get("updated", ""), items, raw.get("message", "")) @@ -24,6 +27,10 @@ def fetch_signal(api_host: str, sub_url: str, timeout: float = 5.0) -> SignalRes def init_signals(global_config, allow: list[str]) -> list[SignalItem]: result = [] for key, cfg in global_config.signals.items(): - if key in allow: - for item in fetch_signal(global_config.api_host, cfg.url).data.values(): item.signal_key = key; result.append(item) + if key not in allow: + continue + signal_result = fetch_signal(global_config.api_host, cfg.url) + for item in signal_result.data.values(): + item.signal_key = key + result.append(item) return result diff --git a/py-client/main.py b/py-client/main.py index cc40e97..8bd04ae 100644 --- a/py-client/main.py +++ b/py-client/main.py @@ -5,6 +5,7 @@ import logging import os import sys from datetime import datetime +import traceback from apscheduler.schedulers.background import BackgroundScheduler import config from dataclasses import dataclass @@ -21,6 +22,8 @@ logging.basicConfig( format='[%(levelname)s] %(asctime)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) +logging.getLogger("apscheduler").setLevel(logging.WARNING) +logging.getLogger("httpx").setLevel(logging.WARNING) from sdk import APIError, Client from libs.market import refresh_market @@ -140,8 +143,9 @@ def main() -> int: STRATEGIES[config.account_config.strategy].start_strategy() logging.info("%s 策略启动成功",config.account_config.strateg) return 0 - except (OSError, yaml.YAMLError, ValueError, RuntimeError, KeyError) as exc: - print(f"启动失败: {exc}", file=sys.stderr, flush=True) + except (OSError, yaml.YAMLError, ValueError, RuntimeError, KeyError) as e: + print(f"启动失败: {e}", file=sys.stderr, flush=True) + traceback.print_exception(type(e), e, e.__traceback__) wait_for_any_key() return 1 diff --git a/py-client/sdk/__pycache__/models.cpython-311.pyc b/py-client/sdk/__pycache__/models.cpython-311.pyc index 8c501732199eacf2104ffa9620d077efa1e7d246..805c3d113aa261a978281e9e041cfecd3e798063 100644 GIT binary patch delta 1648 zcmajfUrbw790%}oTIg6A(9+U>_uh_LD}jaqh0$IrZtf3ZE^4x&qMO1-nK1d&U6wgn zlr1q4H!=gqZ3UH9RA3?*vrOHBF(&fty~&M<8lx}n#Ro5mKA9QwJ%A%yqW9sGZ_fGs z&N=r_?(#Xh{955mL4lPJLhe*uKJF2(7Y0SP=xA~#%+r*RIf^M%7ZKz*gJH95TbMdKR5^ypapaS2^<4YffvEkpbI<$ zxg}E`}3oGwa!g zybGLSV#Pyri0!U;BX9V}VDo^SE?dcSfB z=?ZTjMFc4}U44R%^7fjVMLNQ6*X*$Ho=Yqoi9GnuzYD#K^d6W5t2IbzzT`L}c%My1 z=IA(<%y!h4Q_BcuCfHcrp2iOl`LRAk`UtqdG?)RnRV4#v!By}vnB&__A%ag>_4e=S z6hC>eKGm_h3uf>3@n4J+WBj+Xx(p%YiSqn=-^o4O@-ydxO2!sY5dzo2r~KpUPoC`V?`umc-5s4u7ULqg z0Y1ZdmCyO!H!%RdV%r-==uN)rj}6258(s_Ri~DGry%`ruHjJY2OR)N~mJq==?7R4@ zbcwY*d|?~^40mIOe_i|=;ct~cbAI3a{`e`DSV5yizh>b^y}FiK4RSv#82baPn2E`) QQYhewOP6 zt0ESPY#L02Q#-iCzG#Yjyl8Fz5O4-G{*HVEbI@OzR7Ay+B45wr`i z2~Z1wADJvg2-$2le8b)-drgK9mJbcl=Ex2SjXdbhMlt5N--L{;WO zf2K?UfcoU>xbvM z1!KEtZ0C*bk`{86$XuJL%hhM=`4xVlwpFZcy`B_I0nrrTO#w*EOiX@y)!`BUqY5OA~Ktom(Rq+C)PeZ)nRq>z2vvrc#xJinv>X6x!o^ zBw5jrkSQNq`VZA`vUry}4u+$H;k|<_Hv(5u#zCd1HJ~?uk7w)#JYBD+PU4&OBlc6^ zDY;IlZvk`xNb5FGBY3}yQJnw@IEF`E5$Z(#PuE`*bteDYiWezu8j1=SLmr9tr?@vT z)u>aAfeT3E^Q)er#&DNs#b{|c_PJ+G8K{xv&lh{SEU33g>p75s3A}Xmb8=Aly_I@A zv-&c17Vm3VuOMw%Jk_wKZt}miE`WLmFa;=NKuwcX-v$Yo#MZ_sIVe~)|5KxjQk;ci z4p*)Xwq62BhMNKPK0pu111*P+D;_z3VZxl@TDc& z3k<+5JnMZ~4vHMtp5{}F?>i2Br`beJ<1(MQ`aTzWKL->J={88f7r58=0(BdUzL(tO z_649qE)}^R~rh`U+3Ahz&kUC87W0E_lBK?v@}KEmC?(2C2jO Ji~iST{{j(VOLG7K diff --git a/py-client/sdk/models.py b/py-client/sdk/models.py index e002a2f..3de94e4 100644 --- a/py-client/sdk/models.py +++ b/py-client/sdk/models.py @@ -99,30 +99,23 @@ class PositionItem: @classmethod def from_trade_detail(cls, data: dict[str, Any]) -> "PositionItem": """从 TradeDetailData/Holding 的 QMT 原始字段创建持仓。""" - instrument_id = str(data.get("m_strInstrumentID") or "") - exchange_id = str(data.get("m_strExchangeID") or "") - stock_code = ( - f"{instrument_id}.{exchange_id}" - if instrument_id and exchange_id - else instrument_id - ) return cls( - stock_code=stock_code, - stock_name=str(data.get("m_strInstrumentName") or ""), - direction=data.get("m_nDirection"), - volume=_number(data.get("m_nVolume"), int), - open_price=_number(data.get("m_dOpenPrice")), - float_profit=_number(data.get("m_dFloatProfit")), - market_value=_number(data.get("m_dMarketValue")), - stock_holder=str(data.get("m_strStockHolder") or ""), - frozen_volume=_number(data.get("m_nFrozenVolume"), int), - can_use_volume=_number(data.get("m_nCanUseVolume"), int), - on_road_volume=_number(data.get("m_nOnRoadVolume"), int), - yesterday_volume=_number(data.get("m_nYesterdayVolume"), int), - last_price=_number(data.get("m_dLastPrice")), - profit_rate=_number(data.get("m_dProfitRate")), - future_trade_type=data.get("m_eFutureTradeType"), - expire_date=str(data.get("m_strExpireDate") or ""), + stock_code=str(data.get("StockCode") or ""), + stock_name=str(data.get("StockName") 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 ""), + frozen_volume=_number(data.get("FrozenVolume"), int), + can_use_volume=_number(data.get("CanUseVolume"), int), + on_road_volume=_number(data.get("OnRoadVolume"), int), + yesterday_volume=_number(data.get("YesterdayVolume"), int), + last_price=_number(data.get("LastPrice")), + profit_rate=_number(data.get("ProfitRate")), + future_trade_type=data.get("FutureTradeType"), + expire_date=str(data.get("ExpireDate") or ""), ) diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc index 643655cb7b8fe4c0563c38e4eb0c2fb54d4cb78a..be15688b95e1dc9a48eed646a4d46063503d8148 100644 GIT binary patch literal 10362 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

U zCrL?+VS^?U8#qIMCHCP38cpt2iiU3QWrAt?Hu+3`5&&*WwQ_y7UbpQrRye?53IGhv z-o<#4fl1ZWFg{P~sXUgcof^RB=-1SUw8RGW3DU0gWicwJIuNCs>7*F)4|gC$zo$zm zlG(dcxUE1WP4D(Tl;h;S4sdec%IKyRT6?su8Ewtj)Wa9lSJk!AFZAqJdbX`+-NF0b z;BbaM&pd$}6wLk+WS&kc6ZP48c}H#bMEQ!_+t=X5mzcfN)Eam{^dd9Ewkeaz*m;&e zNk<3!@nbqacnp{6&fq$_?s~3=6HQh9dzTdFVF5O#_jT)^+Aseju?+zD8!&tdGynhq diff --git a/py-client/strategy/trend/__pycache__/watch.cpython-311.pyc b/py-client/strategy/trend/__pycache__/watch.cpython-311.pyc index 44c031e7a6c9b92fa8b22296cc6ae13697eee67e..0db6c9af0ab0cdc8063487758969df70c59d556e 100644 GIT binary patch literal 5091 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 UHQGy#G~r410CkVHc_4E2UvPTN5dZ)H diff --git a/py-client/strategy/trend/boot.py b/py-client/strategy/trend/boot.py index eaaf29d..aa5b2d2 100644 --- a/py-client/strategy/trend/boot.py +++ b/py-client/strategy/trend/boot.py @@ -5,8 +5,8 @@ from __future__ import annotations -import logging import time +import logging as log from concurrent.futures import Future, ThreadPoolExecutor from datetime import datetime @@ -25,42 +25,22 @@ from .positions import manage_positions def Overview(assets, positions, account_cfg=None) -> None: - """打印策略启动时的账户、资金和持仓概览。 - """ + """记录策略启动时的账户、资金和持仓概览。""" account_cfg = account_cfg or config.account_config - print("\n" + "=" * 80) - print(f"【时间】{datetime.now():%Y-%m-%d %H:%M:%S}") if account_cfg is not None: - print( - "【配置】" - f"account_id: {account_cfg.account_id} " - f"host_key: {account_cfg.host_key} " - f"buy_value: {account_cfg.buy_value:.0f}" - ) + log.info("[启动] 账户=%s,主机=%s,单笔金额=%.2f", account_cfg.account_id, account_cfg.host_key, account_cfg.buy_value) if assets is not None: - print( - f"【资金】总资产:{assets.total:.2f}元," - f"可用资金:{assets.available:.2f}元" - ) + log.info("[启动] 总资产=%.2f,可用资金=%.2f", assets.total, assets.available) else: - print("【资金】查询失败") + log.warning("[启动] 获取资金概览失败") - print(f"【持仓】{len(positions)}只") - print("=" * 80) + log.info("[启动] 持仓数量=%d", len(positions)) for position in positions: if position.volume <= 0: continue - print( - f"【持仓】{position.stock_code} {position.stock_name} " - f"持仓={position.volume} 可用={position.can_use_volume} " - f"冻结={position.frozen_volume} 在途={position.on_road_volume} " - f"昨仓={position.yesterday_volume} 成本={position.open_price:.3f} " - f"现价={position.last_price:.3f} 市值={position.market_value:.2f} " - f"浮盈={position.float_profit:.2f} " - f"盈亏比例={position.profit_rate * 100:.2f}%" - ) + log.info("[启动] %s %s,持仓=%d,可用=%d,成本=%.2f,现价=%.2f,盈亏=%.2f%%", position.stock_code, position.stock_name, position.volume, position.can_use_volume, position.open_price, position.last_price, position.profit_rate * 100) @@ -88,6 +68,7 @@ def StartTrend() -> None: config.global_config, config.account_config.signal_allow, ) + log.info("[启动] 趋势策略已启动,账户=%s,信号=%d,持仓=%d", config.account_config.account_id, len(signals), len(positions)) run = Runtime( client=client, global_cfg=config.global_config, @@ -102,31 +83,43 @@ def StartTrend() -> None: Overview(assets, positions, config.account_config) - 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)) - finally: - run.executor.shutdown(wait=True, cancel_futures=True) + DEFAULT_TICK_INTERVAL = 30 + while True: + current_sec = time.localtime().tm_sec + + # 计算距离下一个目标时间点(0秒或30秒)的等待时间 + if current_sec < DEFAULT_TICK_INTERVAL: + wait_seconds = DEFAULT_TICK_INTERVAL - current_sec + elif current_sec < 60: + wait_seconds = 60 - current_sec + else: + wait_seconds = DEFAULT_TICK_INTERVAL + + # 等待到目标时间点 + time.sleep(wait_seconds) + + # 单轮失败不能杀死唯一的交易定时线程。 + try: + RunOnce(run, signals) + except Exception as e: + log.error(f"[Trend] 本 tick 执行失败,下一 tick 继续: {e}", exc_info=True) def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: """按固定步骤执行一轮趋势策略, ``RunOnce``。""" if not trading_time(datetime.now()): + log.info("[运行] 非交易时间,跳过本轮") return + print("=" * 40 + f" RunOnce {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} " +"=" * 40) + + started_at = time.monotonic() + # 1. 刷新订单数据,清理过期订单。 try: run.orders.refresh(run.client) except Exception: - logging.exception("取消过期订单失败") + log.exception("[订单] 刷新订单失败") return @@ -134,20 +127,20 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: try: assets = run.client.assets() except Exception: - logging.exception("获取资产失败") + log.exception("[资金] 获取资产失败") return allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio if not allow_open_by_cash: - logging.info("资金总闸:可用金额太少,禁止开新仓") + log.info("[开仓] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total) # 3. 获取大盘状态,只有大盘信号允许时才执行开仓。 - market_ok = market_allow_open(run.global_cfg.api_host) + market_ok = market_allow_open() # 4. 获取当前持仓及持仓证券代码。 try: position_codes, positions = run.client.positions() except Exception: - logging.exception("获取持仓失败") + log.exception("[持仓] 获取持仓失败") return # 5. 验证有效开仓信号:排除已有持仓和未决订单。 @@ -158,21 +151,26 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: allow_open.append(signal) allow_codes.append(signal.code) + if allow_open and not market_ok: + log.info("[开仓] 禁止开仓:大盘信号不允许,候选=%d", len(allow_open)) + # 6. 获取持仓和待开仓证券的实时行情 tick。 all_codes = list(dict.fromkeys(position_codes + allow_codes)) try: ticks = run.client.full_tick(all_codes) except Exception: - logging.exception("获取行情失败") + log.exception("[行情] 获取行情失败,代码数量=%d", len(all_codes)) return # 7. 更新状态机 try: run.state.reconcile(positions, run.orders.data) except Exception: - logging.exception("订单状态对账失败,本轮禁止自动交易") + log.exception("[状态] 订单状态对账失败") return + log.info("[RunOnce] 本轮就绪,持仓=%d,候选=%d,大盘允许=%s,资金允许=%s", len(positions), len(allow_open), market_ok, allow_open_by_cash) + # 启动线程,开始计算 # 9. 持仓计算。 futures: list[tuple[str, Future]] = [ @@ -196,6 +194,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: # 11. 开始执行 for name, future in futures: _wait_worker(name, future) + log.info("[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000)) def _wait_worker(name: str, future: Future) -> None: @@ -203,4 +202,4 @@ def _wait_worker(name: str, future: Future) -> None: try: future.result() except Exception: - logging.exception("趋势策略%s线程失败", name) + log.exception("[运行] %s线程失败", name) diff --git a/py-client/strategy/trend/open.py b/py-client/strategy/trend/open.py index e8040be..9592fb9 100644 --- a/py-client/strategy/trend/open.py +++ b/py-client/strategy/trend/open.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging from datetime import datetime from libs import calc_buy_volume @@ -10,38 +9,46 @@ from sdk import OP_BUY from .runtime import Runtime from .order import PlaceOrderRequest from .state import STATUS_ING, StateItem +import logging as log def open_signal(run:Runtime, ticks, open_signals) -> None: """逐个验证开仓信号并提交买入委托。""" + log.info("[开仓] 信号总数:%d", len(open_signals)) for item in open_signals: # 1. 验证信号配置允许开仓的时间区间。 signal_config = run.global_cfg.signals.get(item.signal_key) if signal_config is None or not check_timezone(signal_config.timezone): + log.info("[开仓] %s 信号=%s,跳过:不在信号时间段(%s)", item.code, item.signal_key,signal_config.timezone) continue # 2. 检查该证券是否已有买入委托锁,防止重复下单。 if run.orders.busy(item.code,"BUY"): + log.info("[开仓] %s 信号=%s,跳过:买入委托处理中", item.code, item.signal_key) continue # 3. 验证行情和最新价格是否有效。 tick = ticks.get(item.code) price = tick.last_price if tick is not None else 0 if price <= 0: + log.info("[开仓] %s 信号=%s,跳过:价格无效", item.code, item.signal_key) continue # 5. 根据单笔买入金额计算整手开仓数量。 volume = calc_buy_volume(price, run.account_cfg.buy_value) if volume <= 0: + log.info("[开仓] %s 信号=%s,跳过:数量无效", item.code, item.signal_key) continue # 当前价高于昨收价可开仓 if signal_config.gt_last_price_is_open and item.last_close>0 and price>item.last_close: try: do_open(run,item.code,volume,item.signal_key) - logging.info("[开仓] %s 买入 %d 股", item.code, volume) - except Exception as e: - logging.exception("[开仓] %s 失败: %s", item.code,e) + log.info("[开仓] %s 信号=%s,买入=%d股,原因=现价高于昨收", item.code, item.signal_key, volume) + except RuntimeError as exc: + log.info("[开仓] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc) + except Exception: + log.exception("[开仓] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume) continue # 4. 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。 @@ -50,9 +57,11 @@ def open_signal(run:Runtime, ticks, open_signals) -> None: try: do_open(run,item.code,volume,item.signal_key) - logging.info("[开仓] %s 买入 %d 股", item.code, volume) - except Exception as e: - logging.exception("[开仓] %s 失败: %s", item.code,e) + log.info("[开仓] %s 信号=%s,买入=%d股,原因=反弹已确认", item.code, item.signal_key, volume) + except RuntimeError as exc: + log.warning("[开仓] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc) + except Exception: + log.exception("[开仓] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume) def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None: diff --git a/py-client/strategy/trend/order.py b/py-client/strategy/trend/order.py index 203713b..5aadf06 100644 --- a/py-client/strategy/trend/order.py +++ b/py-client/strategy/trend/order.py @@ -3,12 +3,13 @@ from __future__ import annotations import secrets +import logging as log from dataclasses import dataclass from datetime import datetime, timedelta from threading import Lock from typing import Any -from sdk import ORDER_SIDE_BY_OFFSET, Client, OrderItem +from sdk import APIError, ORDER_SIDE_BY_OFFSET, Client, OrderItem # 表示委托仍在处理、可能继续成交的 QMT 状态。 BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"} @@ -57,6 +58,7 @@ class OrderBook: now_timestamp = current.timestamp() data: list[OrderItem] = [] lock: dict[str, float] = {} + canceled = 0 for item in orders: # 不处理状态不对的 @@ -69,6 +71,8 @@ class OrderBook: and current - item.created_at > self.cancel_timeout_sec ): client.cancel_by_id(item.id) + canceled += 1 + log.info("[订单] 超时撤单,代码=%s,方向=%s,柜台订单=%s", item.code, item.side, item.id) continue # 缓存本次有效订单 @@ -86,20 +90,27 @@ class OrderBook: with self.mutex: self.data = data self.lock = lock + log.info("[订单] 刷新完成,跟踪=%d,处理中=%d,撤销=%d", len(data), len(lock), canceled) def place(self, request: PlaceOrderRequest) -> bool: """按最新价提交委托,并立即写入本地方向锁。""" - result = request.client.passorder_latest_tagged( - request.op, - request.code, - request.volume, - request.strategy_name, - request.order_id, - ) + try: + result = request.client.passorder_latest_tagged( + request.op, + request.code, + request.volume, + request.strategy_name, + request.order_id, + ) + except APIError as exc: + log.exception("[订单] 下单失败,代码=%s,本地订单=%s,HTTP状态=%d,错误=%s", request.code, request.order_id, exc.status_code, exc.message or str(exc)) + return False if not isinstance(result, dict): + log.warning("[订单] 下单失败,代码=%s,本地订单=%s,原因=响应格式无效", request.code, request.order_id) return False order_ref = str(result.get("order_ref") or "").strip().lower() if result.get("status") != "success" or order_ref in {"", "unknown", "none"}: + log.warning("[订单] 下单被拒绝,代码=%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), "") @@ -117,4 +128,5 @@ class OrderBook: key = f"{side}-{request.code}" self.data.append(pending) self.lock[key] = pending.created_at.timestamp() + log.info("[订单] 下单已受理,代码=%s,方向=%s,数量=%d,本地订单=%s,柜台订单=%s", request.code, side, request.volume, request.order_id, order_ref) return True diff --git a/py-client/strategy/trend/positions.py b/py-client/strategy/trend/positions.py index 1955577..bc2fe84 100644 --- a/py-client/strategy/trend/positions.py +++ b/py-client/strategy/trend/positions.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging from dataclasses import dataclass from libs.calc import calc_buy_volume, calculate_min_profit_rate @@ -12,6 +11,7 @@ from sdk import OP_BUY, OP_SELL, PositionItem, Tick from .order import PlaceOrderRequest from .runtime import Runtime from .state import STATUS_ING +import logging as log LEG_BASE = "base" LEG_ADDED = "add" @@ -43,11 +43,11 @@ def manage_positions( runtime.profit_tracker.retain(active_keys) remaining_cash = max(0.0, available) - logging.info("[持仓] 共 %d 只,开始处理", len(positions)) for position in positions: code = position.stock_code tick = ticks.get(code) if code in runtime.account_cfg.excluded_codes: + log.info("[持仓] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=已配置为排除股票", code, position.stock_name) continue if ( not code @@ -56,6 +56,7 @@ def manage_positions( or tick is None or tick.last_price <= 0 ): + log.warning("[持仓] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=持仓或行情数据无效", code or "未知", position.stock_name) continue pnl_rate = round( @@ -70,9 +71,8 @@ def manage_positions( pnl_rate=pnl_rate, minimum_profit=minimum_profit, ) - if profit_decision.message: - logging.info("[止盈] %s %s", code, profit_decision.message) - + profit_action = profit_decision.message or "未触发" + loss_add_action = "未启用" if runtime.account_cfg.enable_loss_add_position and market_ok: loss_decision = handle_loss( runtime=runtime, @@ -82,8 +82,14 @@ def manage_positions( available=remaining_cash, ) remaining_cash -= loss_decision.reserved_cash - if loss_decision.message: - logging.info("[补仓] %s %s", code, loss_decision.message) + loss_add_action = loss_decision.message or "未触发" + elif runtime.account_cfg.enable_loss_add_position: + loss_add_action = "大盘信号不允许" + + log.info( + "[持仓] 代码=%s,名称=%s,盈亏=%.2f%%,止盈=%s,补仓=%s", + code, position.stock_name, pnl_rate, profit_action, loss_add_action, + ) def handle_profit( diff --git a/py-client/strategy/trend/state.py b/py-client/strategy/trend/state.py index f8d3804..7475e14 100644 --- a/py-client/strategy/trend/state.py +++ b/py-client/strategy/trend/state.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import logging as log from dataclasses import asdict, dataclass from pathlib import Path from threading import Lock @@ -93,6 +94,7 @@ class State: 立即保存,确保首次接管的持仓在程序重启后仍可恢复。 """ known_codes = set(self.codes) + imported = 0 for position in positions: if ( not position.stock_code @@ -111,8 +113,11 @@ class State: ) ) known_codes.add(position.stock_code) + imported += 1 self.save() + if imported: + log.info("[状态] 导入持仓=%d,状态总数=%d", imported, len(known_codes)) def reconcile( self, @@ -151,6 +156,8 @@ class State: 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) self.set(item) @@ -158,7 +165,8 @@ class State: # reconciliation must therefore happen before stale state is removed. for code in list(self.codes): if code not in position_codes: - self.delete(code) + if self.delete(code): + log.info("[状态] 已移除持仓状态,代码=%s", code) self.save() def save(self) -> None: diff --git a/py-client/strategy/trend/watch.py b/py-client/strategy/trend/watch.py index bab726b..315fb19 100644 --- a/py-client/strategy/trend/watch.py +++ b/py-client/strategy/trend/watch.py @@ -1,7 +1,7 @@ +import logging as log from dataclasses import dataclass from datetime import datetime, timedelta from threading import Lock -import logging @dataclass(slots=True) @@ -11,24 +11,95 @@ class _Entry: class DipWatch: - def __init__(self, expire_seconds: float = 300, rebound_threshold: float = 0.61): - self.expire_seconds, self.rebound_threshold = expire_seconds, rebound_threshold - self.data: dict[str, _Entry] = {}; self.lock = Lock() + """观察价格低点,并在价格达到指定反弹幅度时触发。""" - def triggered(self, tag: str, code: str, price: float, now: datetime | None = None) -> bool: - if price <= 0: return False - now = now or datetime.now() + def __init__( + self, + expire_seconds: float = 300, + rebound_threshold: float = 0.61, + ) -> None: + self.expire_seconds = expire_seconds + self.rebound_threshold = rebound_threshold + self.data: dict[str, _Entry] = {} + self.lock = Lock() + + def triggered( + self, + tag: str, + code: str, + price: float, + now: datetime | None = None, + ) -> bool: + """更新观察价格;达到反弹阈值时返回 ``True``。""" + if price <= 0: + log.warning("[%s观察] %s 价格无效:%.2f", tag, code, price) + return False + + current = now or datetime.now() with self.lock: watch = self.data.get(code) - if watch is None or now >= watch.expires_at: - self.data[code] = _Entry(price, now + timedelta(seconds=self.expire_seconds)); return False + + if watch is None: + self._start(code, price, current) + log.info( + "[%s观察] %s 开始观察,收盘价=%.2f,反弹阈值=%.2f%%", + tag, + code, + price, + self.rebound_threshold, + ) + return False + + if current >= watch.expires_at: + self._start(code, price, current) + log.info("[%s观察] %s 观察已过期,重新观察,收盘价=%.2f", tag, code, price) + return False + if price < watch.last_close: - self.data[code] = _Entry(price, now + timedelta(seconds=self.expire_seconds)); return False + old_price = watch.last_close + self._start(code, price, current) + log.info( + "[%s观察] %s 刷新低点,原收盘价=%.2f,新收盘价=%.2f", + tag, + code, + old_price, + price, + ) + return False + rebound = (price - watch.last_close) / watch.last_close * 100 - if rebound <= 0 or rebound < self.rebound_threshold: return False + if rebound < self.rebound_threshold: + log.debug( + "[%s观察] %s 等待反弹,收盘价=%.2f,现价=%.2f,反弹=%.2f%%,阈值=%.2f%%", + tag, + code, + watch.last_close, + price, + rebound, + self.rebound_threshold, + ) + return False + del self.data[code] - logging.info("[%s-触发] %s 反弹=%.2f%%", tag, code, rebound) + log.info( + "[%s观察] %s 反弹触发,收盘价=%.2f,现价=%.2f,反弹=%.2f%%", + tag, + code, + watch.last_close, + price, + rebound, + ) return True - def forget(self, code): - with self.lock: self.data.pop(code, None) + def forget(self, code: str) -> None: + """清除指定股票的价格观察状态。""" + with self.lock: + removed = self.data.pop(code, None) + if removed is not None: + log.info("[价格观察] %s 已清除观察状态", code) + + def _start(self, code: str, price: float, now: datetime) -> None: + self.data[code] = _Entry( + last_close=price, + expires_at=now + timedelta(seconds=self.expire_seconds), + ) diff --git a/py-client/test.py b/py-client/test.py index b878c21..82c3ed7 100644 --- a/py-client/test.py +++ b/py-client/test.py @@ -14,12 +14,12 @@ def main(): _, positions = client.positions() print(f"总资产:{assets.total:.2f}元,可用资金:{assets.available:.2f}元") for p in sorted(positions, key=lambda item: item.stock_code): - if p.volume > 0: print(f"{p.stock_code} {p.stock_name} 持仓={p.volume} 可用={p.can_use_volume} 成本={p.open_price:.3f} 现价={p.last_price:.3f}") + if p.volume > 0: print(f"{p.stock_code} {p.stock_name} 持仓={p.volume} 可用={p.can_use_volume} 成本={p.open_price:.2f} 现价={p.last_price:.2f}") data_dir = os.environ.get("QMT_DATA_DIR", "").strip() if not data_dir: raise SystemExit("环境变量 QMT_DATA_DIR 为空") codes = json.loads((Path(data_dir) / "pass_codes.json").read_text(encoding="utf-8")) for code, tick in sorted(client.full_tick(codes).items()): - print(f"{code} last={tick.last_price:.3f} close={tick.last_close:.3f}") + print(f"{code} last={tick.last_price:.2f} close={tick.last_close:.2f}") if __name__ == "__main__": main() diff --git a/py-client/tests/__pycache__/test_signal.cpython-311.pyc b/py-client/tests/__pycache__/test_signal.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e12b0b07a1ee6cb1f68966bc12cdde4bb972555 GIT binary patch literal 2700 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 delta 3705 zcma)9du&@*89(R7cI-Hhx_Lhu+i}w5s!5hUvaVSuG-=XitV!3nbTUai$M?o{>PL3& zO(D~?qe6|W9d$7=#L@!CMG_*NLJi?0i;6YZi{ecT;z$2|d0+#B%58v+fK zNMsjLiXS9O$%_&p-;=NyegPjZrqv~lVtnm`L@8CJ$tJ#+R+g?;o0YOdVkUD*M3p96 z_#G7${3vzOa(B13OIB~1W47T4Dj_|j&rGQ+HvFskd#H~uRUAAhvlW(wj z`R@xFEyF~1rku{Uyb2L(zRp77Ln_NgI#@hLTliven)*_UCHtvU@U;eRUA)NUrXJqr zYP5k$qlrJ_YU200HqnjzwCn2@gpq2lsFVu#Vwf1xmThbgzpbi{Zsn1xLv92sQ$va> zvSdi@9fl>UtMTmOm#ZdeA0MgqVF#cxngPkXk~2w(KL%c?+g z;Y5-JHJycYH8R7B`TL&g)~qI+uqgwSU~e-U;ZUMX4eLoZ!+Lp>*K5jU3;##`7Cz_Q zL3gLld7q`J?{9cZ>Yj$NpnrvVb{O1R*w--B3YY=h19%ui5_=5o$GOW^#-Hshsg@k+P^%D{k%fxmJBTmlzjuvtY+fJ;PEH`$6Y9B8RB7_5E6YOoleVZh47e+ zuWJEdnt>&D98e5+5~SrtRNZid6h&2niRt*Tk*BFTvtwKwB|%^*x}eQ)hBQrOdVeUU zsfI(F9*;+LT~*|Kv;I7ViDGx=(TW33aN1Ery}YhNp`)oI9Uoa5KaWZF3=BXV`&Ndr zHMRGnD~iYUjGgb?J}aC(gh`^fJcrh|aVkX4f|C!@>GU*IdTH^I56?a%M74~5^kO=l zW}`6q4!UoHyfvT8RxV!ln#9;mVg?dMeq7bGP()?V!|e-z7Xc^*vLh!?CqS)rBFZ$K z#rbb;Zl_*Ox86n*soS^CTf8~wTFARvbKKg!c@~w-Ol2I|qXZjtlOc6>$b~sG5>Zvd z3f0SSFg2V^grhOl6psl1H4IlEa$C;GgRm@^V#zT7QTJNv<>$MP(fd=QU-++u9^`?Z zn({(;5Zjln{rB-??;t(SFZR??AOCmH(1umV>QzyGO_}DjeUhK;l|>onNBZj;B4n7b zm6EeS*|zHrSF(4|1$kyl+z%uiE+MkSey96d2RYYtLDCuP`37O#)VHe@kuQtl0;w%f zA%_KO*vxW!ZRJDSC-|j4cP+?Ruo^bVM%N6-R8ouT(PTnnUEJGWK~M5q`X{|BDaq8x z4~An=HKAYUvEfuUurqq5X2=qo0`qqRMly$~(0vV0ISy2^cW?(zLMG3b{VVPaEAADI zoi)WE>M$yiQ4onG$3wATcp_pJq+U*UFVS*-Zg<-n@hTB>|AIhH@bvCxv&Oda+1^%u z=fF0~Q^yA0%I{cF`8`GPsHQ~|k+vjLR3@seoX5^!G5dH(Q-gQwGwe0~=pB8HKZjRQ zFy27xO~7@6Iqe^+{|S;p9h)Q19Dcd)jQ@hW=N)&?yY3#blXvZFsaYXAr-!Pa!UJBU zbL7;fQ|werh8hUavr0ebG54A~@3eo&1?sNI&s-L_!i~re)~{!+=f_3RO?|tX#KFO( zA>_y2M(fwPR;Nom?RS4s9Kv@nYX}=Du?=DmwP+*}iUkj;GwdV=e+Re-_ygdNfcI|X zZa?iGsutO29tAryEV0=ew4VJX)`Fx(!I14)sj(06Ra|=Fj`=3K;vy8{`YR#C&pyP* zOMs67%gC~1b^XhhzDlHT(L#q+v$$JOJk6`mfmS)-ZvfP9_Gh@WuP%iUxM4Fn%m^Km z|BijYRf<;~e&X3>wDH&dZT#s2UG$aIZx1|b87W--avp&70e}X08t`4f4*^SnPXPY` ztdje7d;>IN_2>C7Mmo4<)XO_Z50s0|$CzOc2IHYr723?c>R$&dT}XSj`CT20^0vx)9ficr)NJY}oy(ruT+4)eH;& zAQ%=byejI??A`ICG96R5GbbekCr5ja@|NI9U*`q#fBr7=PlIKg-u24OQxa|el;p;v XRqwhQE{nl`T|H&O%Bw<6;wt$c7

7fHrNX`s=TWH|NK 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 Z3`;?X*iI02GOU0Pk{T@iKY+5k{{nET>lpw5 diff --git a/py-client/strategy/trend/__pycache__/log.cpython-311.pyc b/py-client/strategy/trend/__pycache__/log.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89c847fdcb6a85e6e80c778ce4f0c75eff04ea89 GIT binary patch literal 6522 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 SLuBcT3*%oA+0QPt;r|b2>YdC0 literal 0 HcmV?d00001 diff --git a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc index 4218111d5dc82d879f938de3949dcf8ab73b7e41..711991abbcb0c1f05012f32d257b8daf08ab711a 100644 GIT binary patch delta 3258 zcmbtWeQZBzFAO=Vyhe zFEgvMc2!*ndN49tsX>B}E-Ru5f8BQet6p?KJcMk|=mCHLK;Qg} z0=NV`!lCAdcZp|_*j`j4-q93RArLezFrqpB=OID>FR`Rdidw@)g7DJf*Ql&)h!n_Q zmGUN43?PtRo%l5>f8m)md=LWR#nC83AiVSvk1cl*73LT#fLEy{9%BV!k-_3xX2g=? z8h}ZSYRJ{D;fh+z)i?n#xs(%v*fjx(CPO(vc=b3hQPAk4jEWyXEImXEG)wm@rCQUU zcHRCS^0-U+ABK3=<`$^1m z9}D#I^{x5kaXs)hRT)6QeS6$PoHi})A}XUxI|-KWD*X!Wl z^4QbUz&$slTO)bHYTT%5C};)UI0Z0CsVfMM0;?I){Zbcb%>51^{#jKD*wNEy<*~4+ zQnp~VBUQ30fm+2)fE<9+#8DVPBPjN*0NyfHeXLx(qPI{Ch7&iD)izD6@&{GcxX!=; z!H6by^n10fTUFZ%HPY3!jV?Q;PpIvPCic!6)wTVg>gG-hZsLe07FX-bYC6GQC~lx$ zh(@Xa;8-etW`0IIzG?pRE2(qS=`%Cwi#HeJmu>8U$cxM6ws2m>VGA$DV++$$3!lHW z7{5G!``y(06M5@Pw;o*jG=1Zb&)UNFB=NIXk4AQ`i4k26^4ZPQudk)9Ur4__nz}lC zf8@>i+aGcGwu;oP39#ILW;#6gvY}pI%#x#xL8#k*(`BiocP% zeLI4WQLbYV$2u(^$DBI%ZYus+YWkD=S8goaD7k8{OcK6eZ<6!`P6d;M&m9o2QoBpY zlR?&4o#jtg>P7hR`MY6o0gz!T^d-=_C95bo0_ z!}yBZq>k@)w!GLi5ajy1UW>Q_d{>XBx9&AR-xV6HJL&Vd1HAZO+V+#@0RUg%;SK`e z45UcyaI0jrf7CD`B(^ol+nN%jLna*(>9}XE$v{2jLk}?(ZQ6$IbBrn0Dp?Lnok!%( zF6l&%e8MjUUVj7-L@#R|0vPvd9w{`VQ1R#5nhhGgC2#l2EPr=Ua-NW#o&@8S8Lz~6 z#R2V(>b$(=FYFf{>ENsK!Cxd8m&~{%#wAW@t@bC#|HG7KXkaSkg8B^T#CzJBC~XX( zIdl0aG1~He+t{gzLx1Q_m>sg&A(=8_9Ksq&a~+$A4BCH48_z5!`4`VgRKHx9jM zHx7M?X6H&aW&jx@XgSS;?4ND9+a?`3CLcK^^#!C*IAP{xGk-`j^Q&eyo6XLZ)L@K| zf}~Q6WRy0nL`s;0vN#(< zGlLQ{m|zC8bEfJHAU%(rEm2&>cB!Imw(;&y6=S#5>z9IDf(gq^SXwy~OjwE9RDmIU z3Q(R#6{J_Bs=c%1UA?jwdL-7HVESdIUs7g*>CX_@ESt?{jv>5@a(gM<1OR{A>Db$- zxzlK9Eu-!^`*|be>?T%_WE`!_0L+=Zl$;? zC;>jg9grkE0iG+L;`MtGsupJqTPwKD0AQy%{66CFwW}y{_F(j+KU;c-0~nl~#*8N?Z0MwZ33)uP4yU;RhLquaH~`mHco>9|DIPs|5O; wuE`%{BR+RC=T_cK0O9|Mzc00l|1J$}xd#?Wof&!u!W-jzv`_4|w38U||3q+i#sB~S delta 1948 zcmbtUUu;uV82?W1pZ2!5e`vdQTQ}%7idJAaH$VqsFdb`UAsOhB)i45_I(zA#d2Smp z-m#%GJkW=cqZoa{L@)+pMn(+AX-1q#j4>@~BB?I%fh9Z`Qlk$G`e4p2m#q+A^z`KG z_y2s~FW>#%-}c!?$H#WN6#yUv(1oe1z+?407`F1!vmO!4tGIEdSD^OO*G z<96IS#m7CEI{@L1d;kFMe12x>?6r+(-`U?r>5C}TWd;Ba0R3@}19%TKQv{~b!G&>7 z1BF$s9|alQc@w>h{D`9^x4(hc_*34TWY_>SUJ+=??Q^TBuuB6)G)(8sEUe99$k=qz z1aQe@$OIJ*X{eT*cA5NifRmnikT`=dH!f--=8oxw5L7smYcOH~G*JV^X2^<6f8+wA zE54sv&JbLJ!xv3$hECF zDQgTK^Y!i*Ie<%?VMUO6ssnQC4H;lPkkGiLE|K&DaQ>xUu1=m3PxbjZAQa9@Rc{-t5UCNbXx0531iP2T>J9t?)m;O#GFmfpS) z4B_CNH$Tk#9NNpO!mqBDDr^w? z(C>`W5nDICW^3V))Ik5Tb z+k3A@=3?`QORi|y6)iq;mzS59s~ob&=qdXqj+grAj6E>nDGU^wdS>vIcADa(;!{VmCn`%g)Ih`qTNF??z$ zsZRpHH}>rj!L%S)BRxBW1!o|#MOfG(M0$j8Tet7=nXdb+{i1L^80bgBcSz_L1;VDF z!l^kzHtAe4lOx{QUmSZ7^3x926KxfFe?F5-rW1XHWJy>gGTR_5OphR5nsBu{JDJRu zhfaFwIoBJ=H|t)rfs3-bDuS9L$uYv#581&K{HxhKsfg)hCZ9`G1a&Nx%vHElB2#f3 z8aqtX#G#r=4O>*e9)%*z8ZtY;RCN&*ftc2~;FxnRuYlZ2~ ztS7RQ5#8lk=E9S~`oR56>!o8&0TiUCo1$9@8~GY+dQYs3Pk{6?Ei3MxWoJ5z^Qpux pa#a7!0o1>#ySd%snACr_Xm|5#%~kPn#KzzeIz+#3-pjLe{{T!&sE7an diff --git a/py-client/strategy/trend/__pycache__/order.cpython-311.pyc b/py-client/strategy/trend/__pycache__/order.cpython-311.pyc index 18fed447002e38578e0df04874f3a8f3dfb5fbde..fde9b452367c844e43aaa6577501c71ad99d576e 100644 GIT binary patch delta 4144 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 Yh);bg%rG?t`0wxr9lv>pS*q=S0AJ!K7XSbN delta 2316 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 diff --git a/py-client/strategy/trend/__pycache__/state.cpython-311.pyc b/py-client/strategy/trend/__pycache__/state.cpython-311.pyc index a9499a428e3441d93bd73736a83def3914a9c019..cd5903f751d3da058e003b60971b0eca3e615bc0 100644 GIT binary patch delta 1629 zcmaJ=Yitx%6h3EX_Wgc!U(jyjZpti_0;v`QMZ_YlNlR-*aG+gkXIs1MgUoJeBvW2O zEMyIq3tD4|jg=ylSYZ=2wlSFagP1_3rY6lM6=R^azgVLtihs=AQmhiZx%1sK_ndRj zIrn_`=+5)IO&5(u9RR?W9q&XZeElXj=K9P0hnX15D+LtTm@2O3)o~54K@d<-#pGsaE4sUT}&=D3Bo#I3v)LD2FxLC4z#J?{_Mt&-Tgm!&~x>VG6vmQKik1zW%f<=A;hP%D(yDJnrwsa zZNwL{xAnb<+p{gkYltrqi+LKak$H1@t$@9Zgf-A|te2J8sNz8k5KAOB$}Ii~xUUdC z<&fAY`^c_VVhL}l@+|RR>MgP4vSmBNMiu0CX^{;Agiqw zzr={aw0-e>fN0^`=@3|nNxR7TVy}0E0T_Ll8iJr~E_|FSTVwTN)jUnLI|Kl`=T3b# zd*o<~|L)kt-4hpPGe_@?lj@*wZz8jxnLRQ-d+yWfpwPe>NSkfzb2%oK?98!UiH>BB ziA55eQhX8s#0CJ!>Gvg)J&AC8Qi!B-ny&c%q?nEfInDlLsw>@lSBp^a&tpr2X2i&kKRn5Su$^D`+RWt3#i)74IhsVgDu1nZXhTU6nkj%TE zYVQs%ZG150|M`;-@4j`eI+%Lc_3B`%d{Z#>ABuc_ZSLau{CgiQuw;QB_a-uT#)s#I zs7nK96SvUFt%URJsoX)uT@+rS+)$??>9%xQ6zk|#J%ybBz?uKyj~FIno+^Be%y~i> zB{pvmJIQ+Qdi6m{7F3WP?*}Tk)|Ur2S?2SqoW`m=ILSs|7&T6$*2>H4Zd6V>8mAnMvZFD}EgM!~JGoi>JYFS^ zlE199q=UNVbXz*II@NX{awY5c_cE&EwA>S9_=zvOcH?r-XJ89 zzzk6YFW`9gs&aakjv4ynX~X z;~J$S5B~;%RB~I&ydot+P8SY$bf>$;NH{Fgw8TuITd`zkXIG+A+(k}vyedHzJIFmQ wfJx#AmFX@6(8mj@&17q6rOqkCgGmyhZ|n9dupaa9_~n-c-EaS6B60xmHyH`MvH$=8 delta 1092 zcmaJ)T**63^?>270Z zHwDXVZ9zyZ3vE0?1GRL)rWz^;9jyAGh`xwVtN0L$qHp@<7Omh9R_(xmb~_ zim7zDR8gxw)7RzRioY5#169q`5G>i$EyWC4s@Y@t%#h_b!&bnIa8MRk-`?7xmU zL2+#pr|6cpeTRdiaHP@T9e&~8hW7mqzrf$^4i+9o#%p1Rr-OO~d79IQ6I=9K)ecWL z^zrPL>Qw}|*6qLop?S)ImX}cKW5eMm!0@wi7Jy7=1Sinz|K!}C!U9S+8j=Gok&Z_qq(`H;Ou6Vq9HCF6MXsKp@mN+^ zaiHbz3eLuQ@IAT|GqhAgawNxd0_}(ct-#&qng^korsJ_VJr)lNo(V7nIuw6Ld