From e320de3241436b36762d29660ad4e396a9b6b4d4 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Mon, 7 Sep 2026 14:04:26 +0800 Subject: [PATCH] feat libs,sdk,trend --- api/qmt_rest_new.py | 121 +++++----- docs/zt.md | 45 ++-- .../__pycache__/collector.cpython-311.pyc | Bin 2802 -> 3203 bytes .../libs/__pycache__/order.cpython-311.pyc | Bin 0 -> 8514 bytes .../__pycache__/orderbook.cpython-311.pyc | Bin 0 -> 12964 bytes .../libs/__pycache__/overview.cpython-311.pyc | Bin 0 -> 1722 bytes .../libs/__pycache__/runtime.cpython-311.pyc | Bin 0 -> 1515 bytes .../libs/__pycache__/watch.cpython-311.pyc | Bin 0 -> 5020 bytes py-client/libs/collector.py | 11 +- py-client/libs/order.py | 16 +- py-client/libs/orderbook.py | 173 +++++++-------- py-client/libs/overview.py | 3 +- py-client/main.py | 15 +- .../sdk/__pycache__/__init__.cpython-311.pyc | Bin 793 -> 823 bytes .../sdk/__pycache__/models.cpython-311.pyc | Bin 17993 -> 21776 bytes .../sdk/__pycache__/portfolio.cpython-311.pyc | Bin 5672 -> 5895 bytes py-client/sdk/models.py | 207 +++++------------- py-client/sdk/portfolio.py | 18 +- .../trend/__pycache__/boot.cpython-311.pyc | Bin 10747 -> 10435 bytes .../trend/__pycache__/open.cpython-311.pyc | Bin 7224 -> 7303 bytes .../__pycache__/positions.cpython-311.pyc | Bin 9015 -> 9000 bytes py-client/strategy/trend/boot.py | 38 ++-- .../zt/__pycache__/boot.cpython-311.pyc | Bin 7553 -> 9106 bytes .../zt/__pycache__/open.cpython-311.pyc | Bin 2026 -> 3319 bytes .../zt/__pycache__/positions.cpython-311.pyc | Bin 5383 -> 6431 bytes .../zt/__pycache__/state.cpython-311.pyc | Bin 8955 -> 11095 bytes py-client/strategy/zt/state.py | 65 +++--- py-client/tests/test_deal_model.py | 128 ++++++----- py-client/tests/test_orderbook.py | 94 ++++---- py-client/tests/test_trend_collector.py | 88 ++++++++ 30 files changed, 515 insertions(+), 507 deletions(-) create mode 100644 py-client/libs/__pycache__/order.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/orderbook.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/overview.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/runtime.cpython-311.pyc create mode 100644 py-client/libs/__pycache__/watch.cpython-311.pyc create mode 100644 py-client/tests/test_trend_collector.py diff --git a/api/qmt_rest_new.py b/api/qmt_rest_new.py index c952772..cd2888d 100644 --- a/api/qmt_rest_new.py +++ b/api/qmt_rest_new.py @@ -174,7 +174,7 @@ class PortfolioHandler(BaseHandler): result = { "assets": format_assets(account_data), "positions": format_holding(positions), - "orders": [fixed_fields(order) for order in orders], + "orders": format_orders(orders), } self.write_json(result) @@ -183,8 +183,7 @@ class PortfolioHandler(BaseHandler): class HoldingHandler(BaseHandler): def get(self): positions = safe_call(get_trade_detail_data, self.acc(), 'stock', 'position') or [] - holding = format_holding(positions) - self.write_json({"data": holding}) + self.write_json(format_holding(positions)) class OrgHandler(BaseHandler): def get(self, handler_type): @@ -207,22 +206,18 @@ class OrgHandler(BaseHandler): # get_trade_detail_data('account') - Query account assets class AssetsHandler(BaseHandler): def get(self): - _data = safe_call(get_trade_detail_data, self.acc(), 'stock', 'account') + _data = safe_call(get_trade_detail_data, self.acc(), 'stock', 'account') or [] self.write_json(format_assets(_data)) class OrderHandler(BaseHandler): def get(self): - ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') - if ret is None: - ret = [] - result = [fixed_fields(obj) for obj in ret] - self.write_json(result) + ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') or [] + self.write_json(format_orders(ret)) class DealHandler(BaseHandler): def get(self): deals = safe_call(get_trade_detail_data, self.acc(), 'stock', 'deal') or [] - rets = [fixed_fields(deal) for deal in deals] - self.write_json({"deals": rets}) + self.write_json(format_deals(deals)) # ContextInfo.get_full_tick() - Get full tick data class FullTickHandler(BaseHandler): @@ -330,56 +325,70 @@ def format_holding(positions): for position in 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, - 'OpenPrice': position.m_dOpenPrice, - 'OpenCost':position.m_dOpenCost, - '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 + 'stock_code': stock, + 'stock_name': position.m_strInstrumentName, + 'direction': position.m_nDirection, + 'volume': position.m_nVolume, + 'open_price': position.m_dOpenPrice, + 'open_cost':position.m_dOpenCost, + 'float_profit': position.m_dFloatProfit, + 'market_value': position.m_dMarketValue, + 'stock_holder': position.m_strStockHolder, + 'frozen_volume': position.m_nFrozenVolume, + 'can_use_volume': position.m_nCanUseVolume, + 'on_road_volume': position.m_nOnRoadVolume, + 'yesterday_volume': position.m_nYesterdayVolume, + 'last_price': position.m_dLastPrice, + 'profit_rate': position.m_dProfitRate, + 'future_trade_type': position.m_eFutureTradeType, + 'expire_date': position.m_strExpireDate } return holding -TRADE_DETAIL_FIELDS = ( - 'm_strOrderSysID', 'm_strInstrumentID', 'm_strExchangeID', - 'm_strInstrumentName', 'm_nOffsetFlag', 'm_nOrderStatus', - 'm_nVolumeTotal', 'm_nVolumeTraded', 'm_nOrderTime', - 'm_strInsertDate', 'm_strInsertTime', 'm_strRemark', - 'm_dPrice', 'm_dTradePrice', 'm_dTradeAmount', -) -MISSING = object() - - -def fixed_fields(obj, fields=TRADE_DETAIL_FIELDS): - result = {} - for field in fields: - try: - value = getattr(obj, field, MISSING) - except TypeError: - continue - if value is MISSING: - continue - if not callable(value): - result[field] = str(value) - if not result: - attrs = getattr(obj, '__dict__', {}) - result = { - key: str(value) for key, value in attrs.items() - if not key.startswith('_') and not callable(value) - } +def format_orders(orders): + """显式转换委托/成交字段,保留原始数值类型。""" + result = [] + for order in orders: + result.append({ + 'stock_code': order.m_strInstrumentID + '.' + order.m_strExchangeID, + 'order_sys_id': order.m_strOrderSysID, + 'ref': order.m_nRef, + 'order_ref': order.m_strOrderRef, + 'direction': order.m_nDirection, + 'offset_flag': order.m_nOffsetFlag, + 'limit_price': order.m_dLimitPrice, + 'volume_total_original': order.m_nVolumeTotalOriginal, + 'volume_traded': order.m_nVolumeTraded, + 'volume_total': order.m_nVolumeTotal, + 'traded_price': order.m_dTradedPrice, + 'trade_amount': order.m_dTradeAmount, + 'insert_date': order.m_strInsertDate, + 'insert_time': order.m_strInsertTime, + 'remark': order.m_strRemark, + 'order_status': order.m_nOrderStatus, + }) return result +def format_deals(deals): + """显式转换委托/成交字段,保留原始数值类型。""" + result = [] + for d in deals: + result.append({ + 'stock_code': d.m_strInstrumentID + '.' + d.m_strExchangeID, + 'order_sys_id': d.m_strOrderSysID, + 'ref': d.m_nRef, + 'order_ref': d.m_strOrderRef, + 'direction': d.m_nDirection, + 'offset_flag': d.m_nOffsetFlag, + 'price': d.m_dPrice, + 'volume': d.m_nVolume, + 'trade_amount': d.m_dTradeAmount, + 'trade_date': d.m_strTradeDate, + 'trade_time': d.m_strTradeTime, + 'remark': d.m_strRemark, + 'close_profit': d.m_dCloseProfit, + }) + return result # ============= Route registration ============= def make_app(): diff --git a/docs/zt.md b/docs/zt.md index d331b01..5bdc136 100644 --- a/docs/zt.md +++ b/docs/zt.md @@ -19,39 +19,22 @@ zt_max_price: 200 ## SQLite 状态存储 `libs/orderbook.py` 使用标准库 `sqlite3`,数据库路径为 -`{qmt_data_dir}/zt_{account_id}_state.db`,每个账户/策略独立存储,由一个策略实例串行更新。 -启动时仅创建当前表结构和索引,不执行数据迁移或旧 JSON 导入。 +`{qmt_data_dir}/zt_{account_id}_state.db`,每个账户/策略由单个实例串行更新。 +启动时仅创建当前表结构和索引,不执行迁移或旧 JSON 导入。 -`OrderBook` 初始化后调用 `load()`,填充 `positions`(按代码)、`deals`(按系统订单号) -和 `deals_sys_ids`(系统订单号集合)。`sync_deals(list[DealItem])` 根据该集合过滤已保存及 -同批重复成交,再以单个事务批量插入,提交成功后刷新缓存;不修改持仓。 -`sys_order_id` 对应 API 的 `m_strOrderSysID`,`local_order_id` 从 `m_strRemark` 的首段提取。 +两表使用 SDK 同名字段;另有自增主键 `id`,成交表增加从 `remark` 提取的 `order_local_id`: -| 表 | 字段与用途 | 索引 | +| 表 | 数据模型 | 索引 | | --- | --- | --- | -| `positions` | 自增 `id`、股票代码 `code`、底仓订单/数量/成本 `base_order_id/base_qty/base_cost`、补仓订单/次数/数量/成本 `added_order_id/added_num/added_qty/added_cost`、状态 `status` | `code` 唯一索引;`base_order_id`;`added_order_id` | -| `deals` | `id`、系统/本地订单号、证券信息、方向、API 状态、剩余/成交数量、委托日期时间、备注、委托价格及成交均价/金额,字段映射见下表 | `id` 主键;`sys_order_id` 唯一;`local_order_id`;`(code, insert_date)`;`(insert_date, insert_time)` | +| `positions` | `PositionItem`:`stock_code`、`stock_name`、`direction`、`volume`、`open_price`、`open_cost`、`float_profit`、`market_value`、`stock_holder`、`frozen_volume`、`can_use_volume`、`on_road_volume`、`yesterday_volume`、`last_price`、`profit_rate`、`future_trade_type`、`expire_date` | `stock_code` 唯一索引 | +| `deals` | `DealItem`:`stock_code`、`order_sys_id`、`ref`、`order_ref`、`direction`、`offset_flag`、`price`、`volume`、`trade_amount`、`trade_date`、`trade_time`、`remark`、`close_profit`,以及 `order_local_id` | `order_sys_id` 唯一索引;`order_local_id`;`stock_code`;`trade_date` | -SDK 的 `DealItem` 与成交表业务字段一致: +`load()` 只更新 `positions`、`deals`、`deals_sys_ids` 缓存,无返回值。 +`sync_positions(list[PositionItem])` 保存完整持仓快照,同一证券更新时保留自增 ID。 +`sync_deals(list[DealItem])` 按系统订单号去重后批量写入;同批重复记录仅写一次。 +`order_local_id` 取 `DealItem.local_order_id`(`remark` 首段),为空时拒绝写入。 +成交日期规范为 `YYYY-MM-DD`,金额缺失时用成交价格乘数量补足。 -| API 字段 | SDK / SQLite 字段 | -| --- | --- | -| `m_strOrderSysID` | `sys_order_id` | -| `m_strInstrumentID`、`m_strExchangeID` | `instrument_id`、`exchange_id`,组合生成 `code` | -| `m_strInstrumentName` | `name` | -| `m_nOffsetFlag` | `offset_flag`,解析生成买卖方向 `side` | -| `m_nOrderStatus` | `status` | -| `m_nVolumeTotal`、`m_nVolumeTraded` | `remaining_volume`、`traded_volume` | -| `m_nOrderTime` | `order_time` | -| `m_strInsertDate`、`m_strInsertTime` | `insert_date`、`insert_time` | -| `m_strRemark` | `remark`,提取 `local_order_id` | -| `m_dPrice`、`m_dTradePrice`、`m_dTradeAmount` | `price`、`trade_price`、`trade_amount` | - -API 返回已成交数据,每个系统订单号仅入库一次;`status` 保存 API 原值,不增加确认流程。 -成交金额缺失时仅使用成交均价乘成交数量补足,不使用委托价格。 -买卖方向使用 `side`,ZT 通过本地委托号前缀区分底仓买入与做 T 买回。 -ZT 根据实际成交数量及金额更新持仓,日期取 API 提供的 `insert_date`,入库时规范为 `YYYY-MM-DD`。 -首次接管持仓仅写持仓表。活动委托和重复下单检查由 `libs/order.py` 的委托簿负责。 -持仓快照与新增成交在同一事务内保存,历史成交只追加;写入失败回滚数据库并恢复内存状态。 -持仓更新按 `code` 保留原有自增 ID。ZT 将轮次状态写入 `status`,卖出、买回数量及均价、 -轮次日期在重启时从 `deals` 重建;补仓字段预留,ZT 买回不计为补仓。 +ZT 使用 `volume/open_price` 保存底仓数量与成本,做 T 轮次从成交历史恢复, +不再使用持仓表的旧状态、底仓订单或补仓字段。买卖方向由 `offset_flag` 计算, +本地订单号从 `remark` 提取。持仓与新增成交在同一事务提交,失败时回滚并恢复内存。 diff --git a/py-client/libs/__pycache__/collector.cpython-311.pyc b/py-client/libs/__pycache__/collector.cpython-311.pyc index bb236122f48a096a1916fcd89f6be5e590936a5e..ea480ca0afea05ec70b631105a2d28d5d7e8868c 100644 GIT binary patch delta 937 zcmY*WT}TvB6h3!%W_NaVcl`f{*1%*6ZLOq`i0Vm%gs>+iWZXOc#JVGQW(!4ewN;}m z)2ptTSPzxZW(vXvCivF#20hq7Amq-jPfEV^6n90<%e~))bInka z0F+)^d@x~~bQht%$;yUW5es3iIRiTs0oxU!ox{#B*PMwn6c^4^1ngpQ%(n~J9p;+d zm~R3pYjUx3&_)zJNCG~_d92q)s03v`9w)mkN4FCcQ^uq@Lh#uv_0P06W89IL) zfPVA>5MXOl`bb=`9stahiJkT7#FNq8sW;}t%boRiso1l`)4|Q;@@#VEeR4i*E`-e4 z)#L_EEYIz2j3-v-&B)utW;8jnWJY4CFKemIvEQL#VgUdN83i4R-s)5Re#NJ&q*V*Z zJzA%#H%e9`SJR2CD;>SnI#B|+x=mGek^umQ`=p>JsC7z~yu-L~RIV|0`iQF?G_ti| zTX&b0LTOY?G^xpV4JIppWfSIMO>`-E>`{!D?{~Lb}?=*{UsF432~wP zc<)<5D2v-m4_M0#{-Nv-Dn4}5Kd=M9S_Qgi<%!7TeB!E52{pJY7LrY~_y$jYx&@?= zT^3RV0HlP;V&Xj>BGNwIi^^z}m(FCfoLs`Oubf?4S3uL{fKM?vy|+gph4g^0LOHbD zR(!dDrC7THy1}>mRarNlm0dh`D@*_9E7rI$o}n4unquP%=zKO z(87I>IkRd$|7=djc3+Pkj+>42-N#fYVn$+n8{?_iNNRKJlGkglM3YNX)smG+%*S~K z7w9XY92L^9LXG#oC=+P!k$e5Bj5X%S<6&-Dh3g2;(Mrdd!$KSY&?t#?#ZlLAkv-Nh zU;{kV&)gnXv1St1rZm%E;d-}tb delta 504 zcmYk2J#W-N5Qb+Rf1U5_gB&LX5Mm)gkp+(UQVJ5KfG%yiq(Ii~m^eCiTloU{-v~(!^i2D%~&_RPH5{PDY_0Fqj=AAd!e~hzFX_^24?0kOz`ZSnlD}v_h zyG4YtiZQNy)jRX|`zpbK>SL@1tgE1kReI*{rz+TkJez-#w$`QEk_{dq$(D3mzA7zBB+_K0)zVH;PRhDQ z+Q^q^mFa6-?zwBp9Wiv9$)m0RRNgUa%VWkG4kjgsW;y#e_GR0dDQy72E%z>YD>Ap< vf3}@7_Ws}XDdQh|h%ZWft4q$Wc>7SRuErgD3;-P6T(}SY#;pwwe75}oiqv@9 diff --git a/py-client/libs/__pycache__/order.cpython-311.pyc b/py-client/libs/__pycache__/order.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7b390536dcf32a1e8ac9bc1357a5750f1789551 GIT binary patch literal 8514 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 ofg3K4OfT228kZh?t}10W)qJBbKOjz>3?Zh%IU# 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 literal 0 HcmV?d00001 diff --git a/py-client/libs/__pycache__/overview.cpython-311.pyc b/py-client/libs/__pycache__/overview.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ae6cf72e36daf580279396ea752e22d1c818f35 GIT binary patch literal 1722 zcmaJ>U2NM_6h5}&#BrRAwJ8HlT^mYZ5!!_IP`Zf@B=&~ZA@!}xu-w=Uc3nI3Ppb}B zN~=nXP*^uuR|EqgMAM-THl&SCZ4o^1z+;N?5LryB)aIv8ZPoFzCv=_I%O2qP>i9e7 zo}cet-Fuh9VHyCSaQYkRbrN6;8LkjbaPTSsEC3Hfz+*fvV?(&}Cx!@~qa z{EeY*7M=+iO#(o52Kmc1wpESQe{ zM?Jy5y5un5W7dn7zL>O^Q* zK4Khka{(+uVqD82B(z74T%n!0XLbmH_g^6a_&@;%SjOGRDc^IvJjOg_5mLmBvIyx= z3x1&mdmJr7I^5#-dRv6l*5ZH0*H@qcmc4uo@)nN`LLz9MMyKSb`;}kkYd_wq%`Z03 z%EB+TFJ~L4XDd5he>l5y^+x5hd^JD4z4m$IByDA92Fz0caIukdsk;&1AJ=!+^V^Ts zs<&4gxbo?I?e5K;t2cJO`?^1V^3;g=Mr&F%yHHsWKg z@%3vfmAPAu=?0a`_dnMkENW<{cX*@_GB36!#r4M6U2iU;KhArt8n~LfUcG(SX?Ps5 zOzf`bYuD~?udTSmYG38HSLYln9#6E}{**2kQ?VjnH!b`J21g%im0S*|F|lfV}ea;s;Tfc zEpxiTW;8J+*tD7vlqRMOjpGH>fYuE)HOi(`UU0F(jR|%n#VM?*3v9Ds(8*4zx{)Al zKd0-0q1(ZXs*8rGD!LtVNvB5AHs$(lDx=7(#u5Q;g)W zm_BiN%t&S?Po!i~P>iH34(myET+qfvVd7+F(xxsn4Xqmh&@I-FVgOm#Z0jtx^)9|x ziuGpS-wd=B1IHdyp*&S)-YqjnO3W*c?sjx+KS&1YCqM@2Jx4`=iWCDcI|;?ZXMgwq z!TdQ=8u)O7VoMZTbVh{=kpuzJEL_0z*Fk&F_kxE;j9)10Q9=)?*YIbfnit~Vnv8; XLPyaZn-I=A-=4n*qxazdl+)y2)Cc^M literal 0 HcmV?d00001 diff --git a/py-client/libs/__pycache__/runtime.cpython-311.pyc b/py-client/libs/__pycache__/runtime.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5664a47d73cb5a6d35e26246d17d27c3ddb250b4 GIT binary patch literal 1515 zcmZux?`s@I7=CAOZtqtv*Q6$CCC2ENa6+$$_@Pn+Z6$(micJufgJH8XxvbeA$?Pt* z#Shn#)L$(Wp{-~{A{db*Erp`dBK;Tkzz@rU5J)bUd=OLOCqK!|_Ck-i`!GE3`_2xt z&-2dwSS;oN0F=L%KKxSw*b+hKNhbsytO39spr8RoRI!k)XbKWMWusnbJj7tPwPXV08?pxt+0XsR#9g& z3XB8oSpZsGQ2&C5}wdLJMH+Pq>?|jzS zTWYkwUT7_@wHEJhZ+yPJ@x{*VPg?hW-MzoO)wsGRRw|>opgWH1>%QqaUKz#x6EmFY z^gY+L-uj3cf$#D%igQ%=b;Hs4Bc2gKzAgnM#&n&lB2my2 zfn`QCZLGRNGLIMJe0PpH?bqfmol5?>W|>o7jVJ0><}UGG0FXlo0KCfp(0JfEHn*Jl|?j7wb1%bN5&@jppO%ViHFq7949${mjg<+EoK<6h@u z03fat?=Jw@Pa%XNI1|E|O?WZv#wNTRb|Zop!ftFrCHy}`Fc@|tf>*bb23-4v$6dA2B>mfiiS-s+8kDIvLjz literal 0 HcmV?d00001 diff --git a/py-client/libs/__pycache__/watch.cpython-311.pyc b/py-client/libs/__pycache__/watch.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7a4396033a17c59273027098276b8d4e18600f8 GIT binary patch literal 5020 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 None: + """每五分钟提交趋势策略的最新缓存,尚无快照时跳过。""" + from strategy.trend.boot import get_collector_snapshot + + snapshot = get_collector_snapshot() + if snapshot is not None: + collector_push(*snapshot) + + def _json_value(value: Any) -> Any: """Convert the QMT model values into values accepted by a JSON encoder.""" if is_dataclass(value) and not isinstance(value, type): @@ -27,7 +36,7 @@ def _json_value(value: Any) -> Any: def collector_push(account_id: str, assets: Any, positions: Any) -> None: - """Best-effort collector upload; never propagate errors to the caller.""" + """[暂停] 数据收集提交,太耗时,超过200毫秒.""" try: payload = _json_value( { diff --git a/py-client/libs/order.py b/py-client/libs/order.py index 8a66d41..9e725bf 100644 --- a/py-client/libs/order.py +++ b/py-client/libs/order.py @@ -70,29 +70,29 @@ class OrderBook: for item in orders: # 不处理状态不对的 - if item.status not in TRACKED_STATUSES: + if str(item.order_status) not in TRACKED_STATUSES: continue - if item.status in BUSY_STATUSES: - busy_keys.add(self._busy_key(item.side, item.code)) + if str(item.order_status) in BUSY_STATUSES: + busy_keys.add(self._busy_key(item.side, item.stock_code)) # 清理过期的 if ( item.created_at is not None and item.local_order_id.startswith(f"{self.order_prefix}-") - and item.status in CANCELABLE_STATUSES + and str(item.order_status) in CANCELABLE_STATUSES and current - item.created_at > self.cancel_timeout_sec ): try: - client.cancel_by_id(item.id) + client.cancel_by_id(item.order_sys_id) canceled += 1 logging.info( "[Order] 超时撤单,代码=%s,方向=%s,柜台订单=%s", - item.code, + item.stock_code, item.side, - item.id, + item.order_sys_id, ) except Exception: logging.exception( - "[Order] 撤单失败,保留在途状态,订单=%s", item.id + "[Order] 撤单失败,保留在途状态,订单=%s", item.order_sys_id ) # 缓存本次有效订单 diff --git a/py-client/libs/orderbook.py b/py-client/libs/orderbook.py index 8a4219c..5adef1b 100644 --- a/py-client/libs/orderbook.py +++ b/py-client/libs/orderbook.py @@ -1,62 +1,79 @@ -"""SQLite 状态簿:每个账户/策略使用独立数据库,单个策略串行读写。""" +"""SQLite positions and deals, aligned with SDK models; one writer per database.""" from __future__ import annotations import math import sqlite3 from contextlib import closing -from dataclasses import asdict +from dataclasses import asdict, fields from datetime import datetime from pathlib import Path -from sdk import DealItem +from sdk import DealItem, PositionItem SCHEMA = """ CREATE TABLE IF NOT EXISTS positions ( id INTEGER PRIMARY KEY AUTOINCREMENT, - code TEXT NOT NULL, - base_order_id TEXT NOT NULL DEFAULT '', - base_qty INTEGER NOT NULL DEFAULT 0 CHECK (base_qty >= 0), - base_cost REAL NOT NULL DEFAULT 0.0 CHECK (base_cost >= 0), - added_order_id TEXT NOT NULL DEFAULT '', - added_num INTEGER NOT NULL DEFAULT 0 CHECK (added_num >= 0), - added_qty INTEGER NOT NULL DEFAULT 0 CHECK (added_qty >= 0), - added_cost REAL NOT NULL DEFAULT 0.0 CHECK (added_cost >= 0), - status TEXT NOT NULL + stock_code TEXT NOT NULL, + stock_name TEXT NOT NULL DEFAULT '', + direction INTEGER, + volume INTEGER NOT NULL DEFAULT 0 CHECK (volume >= 0), + open_price REAL NOT NULL DEFAULT 0, + open_cost REAL NOT NULL DEFAULT 0, + float_profit REAL NOT NULL DEFAULT 0, + market_value REAL NOT NULL DEFAULT 0, + stock_holder TEXT NOT NULL DEFAULT '', + frozen_volume INTEGER NOT NULL DEFAULT 0, + can_use_volume INTEGER NOT NULL DEFAULT 0, + on_road_volume INTEGER NOT NULL DEFAULT 0, + yesterday_volume INTEGER NOT NULL DEFAULT 0, + last_price REAL NOT NULL DEFAULT 0, + profit_rate REAL NOT NULL DEFAULT 0, + future_trade_type INTEGER, + expire_date TEXT NOT NULL DEFAULT '' ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_positions_code ON positions (code); -CREATE INDEX IF NOT EXISTS idx_positions_base_order_id ON positions (base_order_id); -CREATE INDEX IF NOT EXISTS idx_positions_added_order_id ON positions (added_order_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_positions_stock_code ON positions (stock_code); CREATE TABLE IF NOT EXISTS deals ( - id INTEGER PRIMARY KEY, - sys_order_id TEXT NOT NULL UNIQUE CHECK (sys_order_id <> ''), - local_order_id TEXT NOT NULL, - code TEXT NOT NULL, - instrument_id TEXT NOT NULL, - exchange_id TEXT NOT NULL, - name TEXT NOT NULL, - offset_flag TEXT NOT NULL, - side TEXT NOT NULL CHECK (side IN ('BUY', 'SELL')), - status TEXT NOT NULL, - remaining_volume INTEGER NOT NULL CHECK (remaining_volume >= 0), - traded_volume INTEGER NOT NULL CHECK (traded_volume > 0), - order_time INTEGER NOT NULL, - insert_date TEXT NOT NULL, - insert_time TEXT NOT NULL, - remark TEXT NOT NULL, - price REAL NOT NULL, - trade_price REAL NOT NULL CHECK (trade_price >= 0), - trade_amount REAL NOT NULL CHECK (trade_amount > 0) + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT NOT NULL, + order_sys_id TEXT NOT NULL CHECK (order_sys_id <> ''), + order_local_id TEXT NOT NULL CHECK (order_local_id <> ''), + ref INTEGER NOT NULL DEFAULT 0, + order_ref TEXT NOT NULL DEFAULT '', + direction INTEGER NOT NULL DEFAULT 0, + offset_flag INTEGER NOT NULL CHECK (offset_flag IN (23, 24, 48, 49)), + price REAL NOT NULL CHECK (price >= 0), + volume INTEGER NOT NULL CHECK (volume > 0), + trade_amount REAL NOT NULL CHECK (trade_amount > 0), + trade_date TEXT NOT NULL, + trade_time TEXT NOT NULL, + remark TEXT NOT NULL DEFAULT '', + close_profit REAL NOT NULL DEFAULT 0 ); -CREATE INDEX IF NOT EXISTS idx_deals_local_order_id ON deals (local_order_id); -CREATE INDEX IF NOT EXISTS idx_deals_code_date ON deals (code, insert_date); -CREATE INDEX IF NOT EXISTS idx_deals_date_time ON deals (insert_date, insert_time); +CREATE UNIQUE INDEX IF NOT EXISTS idx_deals_order_sys_id ON deals (order_sys_id); +CREATE INDEX IF NOT EXISTS idx_deals_order_ref ON deals (order_local_id); +CREATE INDEX IF NOT EXISTS idx_deals_stock_code_date ON deals (stock_code); +CREATE INDEX IF NOT EXISTS idx_deals_date_time ON deals (trade_date); """ +# SQL is assembled once; order_local_id is derived from the SDK remark property. +POSITION_FIELDS = tuple(field.name for field in fields(PositionItem)) +DEAL_FIELDS = tuple(field.name for field in fields(DealItem)) + ('order_local_id',) +POSITION_UPSERT = ( + f"INSERT INTO positions ({', '.join(POSITION_FIELDS)}) " + f"VALUES ({', '.join(':' + key for key in POSITION_FIELDS)}) " + "ON CONFLICT(stock_code) DO UPDATE SET " + + ', '.join(f'{key} = excluded.{key}' for key in POSITION_FIELDS if key != 'stock_code') +) +DEAL_INSERT = ( + f"INSERT INTO deals ({', '.join(DEAL_FIELDS)}) " + f"VALUES ({', '.join(':' + key for key in DEAL_FIELDS)})" +) + class OrderBook: - """Persist position snapshots and append-only executions; one writer per database.""" + """Position snapshots and append-only deals. No schema migration.""" def __init__(self, path: str | Path) -> None: self.path = Path(path) @@ -76,8 +93,8 @@ class OrderBook: def load(self) -> None: with closing(self._connect()) as db, db: db.execute('BEGIN') - positions = {row['code']: dict(row) for row in db.execute('SELECT * FROM positions')} - deals = {row['sys_order_id']: dict(row) for row in db.execute('SELECT * FROM deals ORDER BY id')} + positions = {row['stock_code']: dict(row) for row in db.execute('SELECT * FROM positions')} + deals = {row['order_sys_id']: dict(row) for row in db.execute('SELECT * FROM deals ORDER BY id')} self.positions = positions self.deals = deals self.deals_sys_ids = set(deals) @@ -85,86 +102,58 @@ class OrderBook: @staticmethod def _insert_deals(db: sqlite3.Connection, deals: list[dict]) -> None: - db.executemany( - """INSERT INTO deals - (sys_order_id, local_order_id, code, instrument_id, exchange_id, name, - offset_flag, side, status, remaining_volume, traded_volume, order_time, - insert_date, insert_time, remark, price, trade_price, trade_amount) - VALUES (:sys_order_id, :local_order_id, :code, :instrument_id, :exchange_id, :name, - :offset_flag, :side, :status, :remaining_volume, :traded_volume, :order_time, - :insert_date, :insert_time, :remark, :price, :trade_price, :trade_amount)""", - deals, - ) + db.executemany(DEAL_INSERT, deals) @staticmethod def deal_record(deal: DealItem) -> dict: - """API 字段转入库记录;委托价格不用于推算成交金额。""" - if not deal.sys_order_id or deal.traded_volume <= 0: - raise ValueError('System order ID and positive traded volume are required') + if not deal.order_sys_id or deal.volume <= 0: + raise ValueError('System order ID and positive volume are required') + if not deal.local_order_id: + raise ValueError('Local order ID is required') row = asdict(deal) + row['order_local_id'] = deal.local_order_id if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()): - raise ValueError('Prices and amounts must be finite') - amount = deal.trade_amount if deal.trade_amount > 0 else deal.trade_price * deal.traded_volume + raise ValueError('Numeric values must be finite') + amount = deal.trade_amount if deal.trade_amount > 0 else deal.price * deal.volume if not math.isfinite(amount) or amount <= 0: - raise ValueError('Execution amount must be positive and finite') + raise ValueError('Trade amount must be positive and finite') row['trade_amount'] = amount - date = deal.insert_date or datetime.now().date().isoformat() + date = deal.trade_date or datetime.now().date().isoformat() if len(date) == 8 and date.isdigit(): date = f'{date[:4]}-{date[4:6]}-{date[6:]}' - row['insert_date'] = date + row['trade_date'] = date return row def sync_deals(self, deals: list[DealItem]) -> None: - """按系统订单号去重,批量写入已成交数据;失败时不更新缓存。""" new_deals = [] seen = self.deals_sys_ids.copy() for deal in deals: - if deal.sys_order_id in seen: + if deal.order_sys_id in seen: continue new_deals.append(self.deal_record(deal)) - seen.add(deal.sys_order_id) + seen.add(deal.order_sys_id) if not new_deals: return with closing(self._connect()) as db, db: self._insert_deals(db, new_deals) self.load() + def sync_positions(self, positions: list[PositionItem]) -> None: + """Replace the complete position snapshot, retaining IDs for existing stocks.""" + self.save({item.stock_code: asdict(item) for item in positions}, list(self.deals.values())) + def save(self, items: dict, deals: list[dict]) -> None: if len(deals) < self._deal_count: raise ValueError('Execution history is append-only') new_deals = deals[self._deal_count:] - positions = [ - { - 'base_order_id': '', 'base_qty': 0, 'base_cost': 0.0, - 'added_order_id': '', 'added_num': 0, 'added_qty': 0, 'added_cost': 0.0, - **item, - } - for item in items.values() - ] - for row in [*items.values(), *new_deals]: + positions = [{**asdict(PositionItem()), **item} for item in items.values()] + for row in [*positions, *new_deals]: if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()): - raise ValueError('Quantities, prices and amounts must be finite') + raise ValueError('Numeric values must be finite') with closing(self._connect()) as db, db: - # 更新已有证券时保留其自增 ID;仅删除快照中已移除的证券。 - for row in db.execute('SELECT code FROM positions').fetchall(): - if row['code'] not in items: - db.execute('DELETE FROM positions WHERE code = ?', (row['code'],)) - db.executemany( - """INSERT INTO positions - (code, base_order_id, base_qty, base_cost, - added_order_id, added_num, added_qty, added_cost, status) - VALUES (:code, :base_order_id, :base_qty, :base_cost, - :added_order_id, :added_num, :added_qty, :added_cost, :status) - ON CONFLICT(code) DO UPDATE SET - base_order_id = excluded.base_order_id, - base_qty = excluded.base_qty, - base_cost = excluded.base_cost, - added_order_id = excluded.added_order_id, - added_num = excluded.added_num, - added_qty = excluded.added_qty, - added_cost = excluded.added_cost, - status = excluded.status""", - positions, - ) + for row in db.execute('SELECT stock_code FROM positions').fetchall(): + if row['stock_code'] not in items: + db.execute('DELETE FROM positions WHERE stock_code = ?', (row['stock_code'],)) + db.executemany(POSITION_UPSERT, positions) self._insert_deals(db, new_deals) self.load() diff --git a/py-client/libs/overview.py b/py-client/libs/overview.py index e56b7b2..4036197 100644 --- a/py-client/libs/overview.py +++ b/py-client/libs/overview.py @@ -26,8 +26,7 @@ def Overview(assets, positions, account_cfg=None) -> None: if position.volume <= 0: continue log.info( - "[启动] %s %s %s,持仓=%d,可用=%d,成本=%.2f(%.2f),现价=%.2f,盈亏=%.2f%%", - position.trade_id, + "[启动] %s %s,持仓=%d,可用=%d,成本=%.2f(%.2f),现价=%.2f,盈亏=%.2f%%", position.stock_code, position.stock_name, position.volume, diff --git a/py-client/main.py b/py-client/main.py index 0ca4dab..7c85ad5 100644 --- a/py-client/main.py +++ b/py-client/main.py @@ -34,6 +34,7 @@ logging.getLogger("httpx").setLevel(logging.WARNING) from sdk import APIError, Client from libs.market import refresh_market +from libs.collector import submit_trend_data from strategy.trend.boot import StartTrend from strategy.zt.boot import StartZT from strategy.ipo import AutoBuyIpo @@ -109,6 +110,7 @@ def wait_for_any_key() -> None: def main() -> int: + scheduler = None try: if not require_windows(): logging.error("本程序仅支持 Windows 环境运行") @@ -143,18 +145,29 @@ def main() -> int: replace_existing=True, next_run_time=datetime.now(), ) + scheduler.add_job( + submit_trend_data, + trigger="interval", + minutes=5, + id="trend_collector", + replace_existing=True, + ) + logging.info("趋势策略数据提交任务已注册:每5分钟读取缓存提交") 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) + logging.info("%s 策略已结束", config.account_config.strategy) return 0 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 + finally: + if scheduler is not None and scheduler.running: + scheduler.shutdown(wait=True) if __name__ == "__main__": diff --git a/py-client/sdk/__pycache__/__init__.cpython-311.pyc b/py-client/sdk/__pycache__/__init__.cpython-311.pyc index a33517f56b1b924de8ee5080e02717074075feb5..0d113c25f86344cb4fc01a16830b61977f9b9779 100644 GIT binary patch delta 284 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 delta 4699 zcmb7HeN0=|6~FgkzKj7U4K@%y5;7WWk}9N-1)4y?e3*}z7#tP|DHy*81`H>eiBGYrE2|q*K>4TQ`<;>b=p65+SAAKhw59rrH{vrfPpo=RSyo zL6gcm`f=~M=bn4cx#yhseDN*(#~=_}i{xx-lyY&pm~Wp&at_StFK|wL?ktk?#OzC)L;r*e z=w2y@rbq>JSjwS|QUSdp<2wD9a;7)%*TC^ge?o_3_gr^F07S`qf|^ymFxP7awYh@|af@yB)DX#uU;g z#Oh9u4HBgm#9ASUIl>H8^?A|FBG&#CZ|YH-dRUXfJPNPxSL^#(eSbJkZ`A)aBa`-6a{J<8 zTv>Nr{)6|1cj+vz?^5f#c;yMT@&v0q5r*Y(0y1xZ(fgct?z`MnteT3MsW_adH|oDi zKxKy(Yd^MsXup1(A9ARN9Cwbk^HP^u>SCoXO(u!{*|Oc54B|x_912;3JXaC^*x@uFBLW! zP#f;Vtn=jEPWSCjH|rbYou|~!Q+EOr+&QT_Cz*3HEF{zDg~xOErNgE)bPRW6*4=xz zTfW^bGi8EzPpaLMcg{?4*R<-IX0GY5kTilfp)Ih^ZprSkyGy#Or!ujIS_z)BpQ2;~i4ZvjLnsVF0H7F1007E>1OXub$v6PyJedH1 zWG9o5@sh}GVu8E{?{@4U>9E*9ZP4t@)fM4|1C~z%K=af5 z5j{^B?*txzjdqrO+xSSvjGgH+G~&99h0A3U-jNco52JMui9-onW>17A;AYbs2P|42 zbuL5>e1(rsL(4FX^vKl{?m?fs&!>>5AtVxkm9?>G8=`@njg2YTd)8PT2q?k8WBtTG z(jyOgg0r-;d<@&^JLM*O2=pbz22-dP!G!Z3@?BhG)a@x)1qjj9iXP4ODH=JDMSoE7 zB%Y$5RkZAlxhJ7DM$Ig!0r(NEd*YC$W|p3NB0o1CAryxY{#PkR=tUI7tL1K=k~#orX3 zk|3;d!g?+3Xcr0^=!b{%n-d+&=_5UXAZZ*8C?qI)JCx@-u4`~a`5IUJBLQy2P}o^G zQYaVN3y&1GTN0zM0Q#T9g`qf+@XI>c9aNU%JwCr@I1ncaWwlal5H5h{;l5u%@NT%D zl6F3L;q>=UvrG$5x2ow@mcDPkkEfKVDJ3(t_YLV6buT78m&CHH+*qs{ixU`cST2w;|^;TA!wjzW+DKXb1qZ`Tk8gtwY z6Rt7G(O#R0s+H5YXQ8d??|A=%Zm^u}a|ePppFg0GA44p}`%=s;aaw@^o6J#Pb?(0Z zle}rJLCc{R%)B=|pc1A>>%=O^M)&=uIy{Ii-zL9YQ1;BER$txv1d*Mw$6-TU|j`bk|?NT?ASpyI^m%yPWP zKRz0CSW;sH1II1ve1m}?xeC%EMn8km&jH>Bh$?&srZFa60Up2y5TjQS4b@-P47&n` zEmSsGaKZ~9^cEelmyO1_zW^)Z)c0ZZO8^tVuK+-&7Ls>ivYktez zfw?MpUaF$gjd>2{>;M`VW=`c$WHF`Je9G~(sHd#22pCZ@~Oi0d-v3+9Sg_8yJ+ z+CWQ-_r&Glrn!(7BCtN{rTE4WpQqyG`oL+&#tavE6AhAxsEE}1ow4Ei2o400ZX%l9 zT=LX6_pWBn%CaBRh-W``U3xc(?W@s+x#Dd%SGuj7EHIpjq?rI%^z9 zTwT}UBSZrbs5hDZRB zNe`7YHMVBALpeJv$+0~_Yzj*t<;6%!naR4ijbRhVCc!@lDmFp{cq;%=6(BY7unOANcBUf% zZwsUKuyx}xsc_6jhyZT|AbJc)?Vx4tFNAb>Zy0Ta2=G<_qMJY}ZR4>Lw&5_^93z6i zA~32*$m)fr(7Hfjd=`h%<`@zD6@gJzLN;lmQCQT=<_MZ>5@@nXgAc2+Nt>&h5Jqi= RplU)GHL=d9^|B&!{|7A_6CD5m diff --git a/py-client/sdk/__pycache__/portfolio.cpython-311.pyc b/py-client/sdk/__pycache__/portfolio.cpython-311.pyc index 665186bcc6ec70c982b8190c7ddcec0d42361d30..a04fd184291c1bba5ce51716b2c9c5ab67b85758 100644 GIT binary patch delta 1307 zcma)6OHUI~6u!5enNFunr<9hbwfI079wj`Kghwetf%*iHEQC*#Hg$!VdIZ53IR981ov)mWx~pwTTvdGnB06hIrp6JobTK-^S1RtLVcsE z3PK3AKYDRJSx{^6)k0&JknDQ+!hBsEiOQmQPbVXSy^^ER$GnI+I=U z&{J^ZrS>hygAl5(r!j=kj?$maI4mx;ux!xJZur`aIK)XdB^GsbFKs~x(Pn^7gpe-L zTFx_LxkWMKS~-XM;Zg!P2|aje9oUrskmq=Lxq2F7KYW`wzOpW7a17d|Cjc7u#ek=S z^#(fFLuFWkUT7s-S9;qZLB~bKaqX#DbIK-`{!5d<+t?$D6HFu^S@-gOX%`T61Drlo z4w}4E%?1P2>~mlq$JvFlr!kPiQyFhomame;X2wNxE+Z1cQG2A zY=AD7aEZ$EXEggFI~i6ZY%@5ZLG(^An{nJpSR!Vqv+Ri$Veh2NeS88Q>Vsm5zgGk$ zO_yxyPTSdxeKB)8GnXL1b1fJhha3k+zJOM>ROu`aKbAK_&2PKbLhT!&_TPTWCw~^I z3?I-{6oJuU>soIFSl)N9YN<6XwV|a}puI}1Bdu&+WUH{-7dj#x5i~@L-qHLOT>6~SnMa498HfxPUhmJzUGXQ)7 z2ihNp3%)XZzI@xuzy|=IuCvS*BliY}@HX;@2T(2(2e#2aphO5X<9dK4qkV1gzbcXM lFSsN3=WKE_YbEIfLI|G6xzAok6HVt<(SIHGJ=!IJfS)j95)uFa delta 1160 zcmah{O=}ZT6uoa|GMRi#(tM|F8rqstr_^AKA5d)}bfcohDqT2=L+BezYh&O|)KY}1 z1OzvdzD^Nf^MQvi+fRADK6x`7);uQ!*ICVch9-!-uLcjUySM-y6y!4 zc-mNBNZ0gka;`RGE>t}3QNqtsJXOc#w& zUsW+x8mD?yG2Jvl169S;Xp(kS71KjQ7r^MQkCAOcblxN9h@YR4KM}2NC~pbz@Sm;` zlHwWl9!c{JbuK>u0GkqX13)8mm6c0Yg%zo7QM*!{U)+?~5T3}ae9MslpueA`c)`73 zrZKIS8MT>X$ZP}vn1PT307GTnf;MMY8m{?@U2>R)hbqE8Y$>xIjAIDMb7Y~8@-FQo zN!NF@dD4gO*dRitqXshv`AJ`nuX!g`^ul6%$2)ca2?l9+ol1H3nl)Rdw()laMc8P~ z&=Q!*BH56IUv>!7d4$7z+M&tiBtPp*@-M!dB+X6#S|5%gsGCI0;}D#J3HcEo)K8H# zzo>ssi8)5GIE%uaE@lFkhJ*{!l#>3frlT^)KLn7tFj5X125p zOV_`5Jt84-#p6gs3wKzGuZN2X;a*`8959LyMhNj}#5jgYLt{E-;s^-@@m}z$h^EcO zC$ZAi9Ny0>5rd@oFF198XbwR!zMj1bV9^NOF3-3QY;pO4@@%@6{Dfvq~Q7Y3Li}nPY1^~POXN51vvx5^`@SmS=#q$a_ F`2*Bz Any: @dataclass(slots=True) class OrderItem: - """由 QMT 委托明细解析得到的标准订单记录。""" + """Fields match format_orders() exactly.""" - id: str - code: str - side: str - remark: str - status: str - created_at: datetime | None - volume: int - local_order_id: str = "" - traded_volume: int = 0 - remaining_volume: int = 0 - exchange_id: str = "" - name: str = "" - price: float = 0.0 - trade_price: float = 0.0 + stock_code: str = "" + order_sys_id: str = "" + ref: int = 0 + order_ref: str = "" + direction: int = 0 + offset_flag: int = 0 + limit_price: float = 0.0 + volume_total_original: int = 0 + volume_traded: int = 0 + volume_total: int = 0 + traded_price: float = 0.0 trade_amount: float = 0.0 + insert_date: str = "" + insert_time: str = "" + remark: str = "" + order_status: int = 0 - @classmethod - def from_trade_detail(cls, data: dict[str, Any]) -> "OrderItem": - """从 TradeDetailData 的 QMT 原始字段创建订单。""" - instrument_id = str(data.get("m_strInstrumentID") or "") - exchange_id = str(data.get("m_strExchangeID") or "") - code = ( - f"{instrument_id}.{exchange_id}" - if instrument_id and exchange_id - else instrument_id - ) - remaining_volume = _number(data.get("m_nVolumeTotal"), int) - traded_volume = _number(data.get("m_nVolumeTraded"), int) - remark = str(data.get("m_strRemark") or "") - return cls( - id=str(data.get("m_strOrderSysID") or ""), - code=code, - side={"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}.get( - str(data.get("m_nOffsetFlag")), "" - ), - remark=remark, - status=str(data.get("m_nOrderStatus") or ""), - created_at=_trade_datetime(data), - volume=remaining_volume + traded_volume, - local_order_id=remark.split("|", 1)[0] if remark else "", - traded_volume=traded_volume, - remaining_volume=remaining_volume, - exchange_id=exchange_id, - name=str(data.get("m_strInstrumentName") or ""), - price=_number(data.get("m_dPrice")), - trade_price=_number(data.get("m_dTradePrice")), - trade_amount=_number(data.get("m_dTradeAmount")), - ) + @property + def side(self) -> str: + return _side(self.offset_flag) + + @property + def local_order_id(self) -> str: + return self.remark.split("|", 1)[0] + + @property + def created_at(self) -> datetime | None: + return _parse_datetime(self.insert_date, self.insert_time) @dataclass(slots=True) class DealItem: - """Execution data from the API's fixed order-detail fields.""" + """Fields match format_deals() exactly.""" - sys_order_id: str = "" - local_order_id: str = "" - code: str = "" - instrument_id: str = "" - exchange_id: str = "" - name: str = "" - offset_flag: str = "" - side: str = "" - status: str = "" - remaining_volume: int = 0 - traded_volume: int = 0 - order_time: int = 0 - insert_date: str = "" - insert_time: str = "" - remark: str = "" + stock_code: str = "" + order_sys_id: str = "" + ref: int = 0 + order_ref: str = "" + direction: int = 0 + offset_flag: int = 0 price: float = 0.0 - trade_price: float = 0.0 + volume: int = 0 trade_amount: float = 0.0 + trade_date: str = "" + trade_time: str = "" + remark: str = "" + close_profit: float = 0.0 - @classmethod - def from_trade_detail(cls, data: dict[str, Any]) -> "DealItem": - instrument_id = str(data.get("m_strInstrumentID") or "") - exchange_id = str(data.get("m_strExchangeID") or "") - remark = str(data.get("m_strRemark") or "") - offset_flag = str(data.get("m_nOffsetFlag") or "") - return cls( - sys_order_id=str(data.get("m_strOrderSysID") or ""), - local_order_id=remark.split("|", 1)[0] if remark else "", - code=f"{instrument_id}.{exchange_id}" if instrument_id and exchange_id else instrument_id, - instrument_id=instrument_id, - exchange_id=exchange_id, - name=str(data.get("m_strInstrumentName") or ""), - offset_flag=offset_flag, - side={"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}.get(offset_flag, ""), - status=str(data.get("m_nOrderStatus") or ""), - remaining_volume=_number(data.get("m_nVolumeTotal"), int), - traded_volume=_number(data.get("m_nVolumeTraded"), int), - order_time=_number(data.get("m_nOrderTime"), int), - insert_date=str(data.get("m_strInsertDate") or ""), - insert_time=str(data.get("m_strInsertTime") or ""), - remark=remark, - price=_number(data.get("m_dPrice")), - trade_price=_number(data.get("m_dTradePrice")), - trade_amount=_number(data.get("m_dTradeAmount")), - ) + @property + def side(self) -> str: + return _side(self.offset_flag) + + @property + def local_order_id(self) -> str: + return self.remark.split("|", 1)[0] @dataclass(slots=True) class PositionItem: + """Fields match format_holding() exactly.""" + stock_code: str = "" stock_name: str = "" - trade_id: str = "" direction: Any = None volume: int = 0 - open_cost: float = 0.0 open_price: float = 0.0 + open_cost: float = 0.0 float_profit: float = 0.0 market_value: float = 0.0 stock_holder: str = "" @@ -137,67 +94,14 @@ class PositionItem: future_trade_type: Any = None expire_date: str = "" - @classmethod - 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 ""), - open_cost=_number(data.get("OpenCost")), - 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 ""), - ) - - @classmethod - def from_trade_detail(cls, data: dict[str, Any]) -> "PositionItem": - """从 TradeDetailData/Holding 的 QMT 原始字段创建持仓。""" - 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_cost=_number(data.get("OpenCost")), - 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 ""), - ) - @dataclass(slots=True) class Assets: + """Fields match format_assets() exactly.""" + total: float = 0.0 available: float = 0.0 - @classmethod - def from_dict(cls, data: dict[str, Any]) -> "Assets": - """兼容新版 QMT 原始资金字段及旧版简化字段。""" - return cls( - total=_number(data.get("m_dBalance", data.get("total"))), - available=_number(data.get("m_dAvailable", data.get("available"))), - ) - @dataclass(slots=True) class Portfolio: @@ -206,17 +110,14 @@ class Portfolio: orders: list[OrderItem] -def _trade_datetime(data: dict[str, Any]) -> datetime | None: - return _parse_datetime( - str(data.get("m_strInsertDate") or ""), - str(data.get("m_strInsertTime") or ""), - ) +def _side(offset_flag: int) -> str: + return {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}.get(str(offset_flag), "") def _parse_datetime(date: str, clock: str) -> datetime | None: clock = clock.replace(":", "").zfill(6) try: - return datetime.strptime(date + clock, "%Y%m%d%H%M%S") + return datetime.strptime(date.replace("-", "") + clock, "%Y%m%d%H%M%S") except ValueError: return None diff --git a/py-client/sdk/portfolio.py b/py-client/sdk/portfolio.py index 53e117d..fef2b69 100644 --- a/py-client/sdk/portfolio.py +++ b/py-client/sdk/portfolio.py @@ -17,33 +17,33 @@ class PortfolioMixin: def portfolio(self) -> Portfolio: data = self._get_json("/api/portfolio") or {} positions = { - code: PositionItem.from_dict(value, code) + code: PositionItem(**value) for code, value in data.get("positions", {}).items() } return Portfolio( - assets=Assets.from_dict(data.get("assets", {})), + assets=Assets(**data.get("assets", {})), positions=positions, - orders=[OrderItem.from_trade_detail(row) for row in data.get("orders", [])], + orders=[OrderItem(**row) for row in data.get("orders", [])], ) def positions(self) -> tuple[list[str], list[PositionItem]]: data = self._get_json("/api/portfolio/positions") or {} positions = [ - PositionItem.from_dict(value, code) - for code, value in data.get("data", {}).items() + PositionItem(**value) + for value in data.values() ] - return [item.stock_code for item in positions], positions + return list(data), positions def assets(self) -> Assets: - return Assets.from_dict(self._get_json("/api/portfolio/assets") or {}) + return Assets(**(self._get_json("/api/portfolio/assets") or {})) def orders(self) -> list[OrderItem]: data = self._get_json("/api/portfolio/order") or [] - return [OrderItem.from_trade_detail(row) for row in data] + return [OrderItem(**row) for row in data] def deals(self) -> list[DealItem]: data = self._get_json("/api/portfolio/deal") or [] - return [DealItem.from_trade_detail(row) for row in data] + return [DealItem(**row) for row in data] def trade_detail_data(self, datatype: str) -> Any: datatype = str(datatype).strip().lower() diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc index aea9cea6bac81b77bd1f8cd26703cd0dac5e183e..5b4167fb78fd61f397c9316f32f6b1c25adb2555 100644 GIT binary patch delta 4989 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 KGF!+=0Qg`2H`T}h delta 5032 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 diff --git a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc index 2cd8ab37006b20c7f99f732e23da0cf0a7440e08..3d6b1a0fddbedb2e0a08a61fa1da8b20f8ea0ce4 100644 GIT binary patch delta 1360 zcmaJ>O=ufO6n^9VkyiURl6Dg?92$g&wL z^w5KPFs%c=gmuACA4;Jlz4W3>4yE)^sG&J*jG&7l5a_9;&|OGCkV|=^I7V$+7(Qmc zH}88t^Oy&3eLij6)Aa-Zz{FpBAN|&Q*C<^gj$DneDrJR$L!4MOv8t9;zK>V6vhJL6 z^i8pBI0KHoDV9xV&>3=an^KuNuQ?|-#d6Y#mmoiU_x^P+K}JZdIYs_dh~f>XUoK4m zfB^s-KMMe#!V}sb!ZMMJh~t(RsOI+cuCQmeGG}}h*JI7xo)754{+kb{{cGjcH8&D^ zbmJ-X=tjsv2Ho~F?Tab*kvi5=$3hSl`-R)wzW7k_OYgTz%dd3%Mf+?t4tlPoO@uD?6icPmUJURz{>E|Vv2DL z*zGF;o$2xpXgX456p&HiKOjOwr?L{zYzP7h9{~z42K4NMi{CH&SpTWvFWva(JK_vP zd=LadL8Y+KgCo@$EN{+yJBJf@C~VXj8{@l5tVKE@Q}FcgpSB7k=%Iecmy z$-yJIjq|?+IR9ZBXxiIPPG?e}ncHW7Rr?OqzTMfq%lBiC)Iv)wc-hn$l5M`5>KFQd zCjd~K%PXwFQ`)Gr$~qHK_q?}d7vyYb`-44i@{k!kBiw1)z2k(OYhFC@4N1)cKwe_= zXuX0k={?UBJ1&cOM!4rKWm^qiQw%x$B$$exTBlOZ@^UeoPR*0JF*r%Qov{r+|UfM;*+KUB?)0nT&UNkZW0Y;c!pcy~w zVS!C|Ccuufu1$BwWGC237T@$_g6tGKw&~7vF#Q%Jdmnvw$MYuv0Lw$(BEH%=@|RB9 zt(fuk&GP^d0D$!k7r=e^EtqJ+h$m0}R{WKF-!Qj^w_Q7??7k=rM1z`JVkEI+hKRQEGO?JK zi+4r!Gdp-s2JZ<4r_tHd1ma&O|K~=q^RZ7v$98+`Ns*qFX(kp6qO@iQxeRjg8XU)Q z8l4FYQBr{pxe@H|eC!v;jz+SBMHwuLLo{6|a~$V)8n()T({4@A6Kgxrvmqik#q^Av zz9Z%r)10I^paJUXC9D$5blI%LG=v<5Xe*jVBIi3g`9#sJS&m zmOnJy-38R#dgoUozGuX@-`lzNP1QCAq%mO4g)WjtzRPNwODKE>KZdu!YJ;MSS{1lPsm8^;Tz9LC!zU!}=#Sn)33yrAn@tRap-_>Uq@L9C!<>OG~-Z5)YvU{|c>pR=r~*-vb{( n>wtAXULH>fID&oX6-TiT5pjs7HcD`gUgK1oHJrHS#=!pq%r|%M diff --git a/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc b/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc index 6b08e8c32000dc7a1f763b1653e28c53c046f1ab..8225d9332dc4e93c1a0648a1c5845f54f2b76ad3 100644 GIT binary patch delta 1448 zcma)+Uuau(6vxl+<~BEfZfxnE9wGNpYS3IzZFa=*QQ>7C@;+R`$iN;zE7ZJhum)K`vFqIOi+{&LJV zXy7Gc$0aDqG-y#1K=aUNuZ$yP;FHLSd;#B!zuu*?S6pWOGjyL$(L#BpqqUc7ponpm>yNhj0ciCvJ5Wf5JN)7{J|Nbky*hr@8(d*Ue*XJ=g8=gtSfYgfw>#U151-zqY--?Vgda zLpyzHz9ydgL6iL)Pm@&Z z@Az4njI_=UoFhbMpC+b>%6?B||NCUwc=GWX5%p<|iueGddd2x|=@1_1Xpvim+mm-r zY($4Qv_e}ee14_<cExQthYcC^OMXz%a9hVSve=H~y+O>X{Z{%I1mwb80wJFE{w+DX;1N+;bYyT;Mz2~LS-p4QmX z2c@KtZDnknJqX)_51lP@h0?(O!JuOs>@nH$5CmZl8)I)*sNe>J@jIzv+QP!&bI$pl z-|u@)e&=`Yz2UEh)w?d20|0=0|LL4R@{yWbu~J*sJ@3&y1gMqTv)a5@_Y$ClIJc%>FQ`G7;T|F&&7bBAUz2MH1(E zXRNm<0jz@Ifk3MP1LkG2BUrQ<(rNoL*$zh5&aS1`?Ui6xHQ2>}aQ5{Z0O7Ne0Wc(u zVE2kNE3%of2?Tl-6-DJ_V_-w)%?>ne{k(sHQaOW+Se^VyLZec&HETkn%6kM zt^+jx|NH*Ew`&7W$N*wrCWpDLxNnb@! zUOj~+i>k|{o2R|Eh{pf)en4D2?@N*dU-zB&$MFgV_wg10T-kc)|CX3{Eue(>cYzp* zm9_(K$fUb;Dtw6$jlUlmB`#i#v^~EVxHEd{Wg&Y8Wf8Yg_FsZ`yXygW_%qY`=$#7> zhAJJWEAfG9eBjaB)!~VIC>q>ttLM5YDgQqlw3v z*ysW}Aim_kw;mxe?ucDpIgUp3Vy%d&r-NqEiueXsjsTvnX7zn|?iXvQYz@`iT9MVP zN^$Y#_0{V&x2L#RbGnKr86GLA)Js`K4N=eSI1)vTO6(|f05E(}tP|0UIEF|b4(G0T zOG2!jX!OGOd9Vq3rG(d0TEfe6=qU9}9qfgpq=ikRVGcnXcR^b+%A)+o_+pni$7qHz z7rKZvnKA*t3kBf*!XzcrRg(-V8K4G8 None: + """整体替换最新快照,策略线程不执行序列化和网络上报。""" + global _collector_snapshot + with _collector_lock: + _collector_snapshot = (account_id, assets, positions) + + +def get_collector_snapshot() -> tuple[str, Assets, list[PositionItem]] | None: + """供 scheduler 读取;复制在锁外执行,不阻塞下一轮缓存更新。""" + with _collector_lock: + snapshot = _collector_snapshot + return deepcopy(snapshot) + def StartTrend() -> None: """初始化趋势策略,并以 30 秒间隔持续执行。""" @@ -37,6 +55,7 @@ def StartTrend() -> None: portfolio = client.portfolio() assets = portfolio.assets positions = list(portfolio.positions.values()) + _cache_portfolio(config.account_config.account_id, assets, positions) order_book = OrderBook("trend") order_book.refresh(client, portfolio.orders) @@ -51,7 +70,7 @@ def StartTrend() -> None: len(signals), len(positions), ) - executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="trend") + executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="trend") run = Runtime( client=client, global_cfg=config.global_config, @@ -116,22 +135,13 @@ def RunOnce(run: Runtime, signals: list[SignalItem]) -> None: assets = portfolio.assets position_codes = list(portfolio.positions) positions = list(portfolio.positions.values()) + _cache_portfolio(run.account_cfg.account_id, assets, positions) run.orders.refresh(run.client, portfolio.orders) except Exception: log.exception("[Portfolio] 刷新账户快照失败") return - futures: list[tuple[str, Future]] = [ - ( - "数据提交", - run.executor.submit( - collector_push, - run.account_cfg.account_id, - assets, - positions, - ), - ) - ] + futures: list[tuple[str, Future]] = [] # 2. 验证可用资金;低于资金安全线时禁止开新仓。 allow_open_by_cash = ( diff --git a/py-client/strategy/zt/__pycache__/boot.cpython-311.pyc b/py-client/strategy/zt/__pycache__/boot.cpython-311.pyc index 4810a0e5ab6790102dcc3141844b08abece20b9d..1518a6b5b086dd39567599fb22f9a50c8643995c 100644 GIT binary patch literal 9106 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 U9z2ll#0hLu9?X-#HvjH-v(`_?$&$O?) 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 diff --git a/py-client/strategy/zt/__pycache__/positions.cpython-311.pyc b/py-client/strategy/zt/__pycache__/positions.cpython-311.pyc index 941d0dfc4d52306c1a38dbbb2c11df5e38caad48..409aaf0068b02162ddfd2b654f098df7ce993e14 100644 GIT binary patch literal 6431 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 literal 5383 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 diff --git a/py-client/strategy/zt/__pycache__/state.cpython-311.pyc b/py-client/strategy/zt/__pycache__/state.cpython-311.pyc index 2636d006bbf648bdc394eeead4b570002529682d..996f1a3ee6237c228effe3dbe5de221c7afc3547 100644 GIT binary patch literal 11095 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%0A-j}vpCGy7~m4HiHl{0 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 E2VVvB`~Uy| diff --git a/py-client/strategy/zt/state.py b/py-client/strategy/zt/state.py index fbf6cf8..b21f0bc 100644 --- a/py-client/strategy/zt/state.py +++ b/py-client/strategy/zt/state.py @@ -60,9 +60,9 @@ class TState: @classmethod def _apply_t_deal(cls, item: TStateItem, deal: dict) -> None: """实时入账与重启恢复共用同一套做 T 轮次计算。""" - cls._reset(item, deal['insert_date']) - qty, amount = deal['traded_volume'], deal['trade_amount'] - if deal['side'] == 'SELL': + cls._reset(item, deal['trade_date']) + qty, amount = deal['volume'], deal['trade_amount'] + if str(deal['offset_flag']) in ('24', '49'): total = item.sell_qty + qty item.sell_price = (item.sell_qty * item.sell_price + amount) / total item.sell_qty = total @@ -72,32 +72,32 @@ class TState: item.buy_cost = (item.buy_qty * item.buy_cost + amount) / total item.buy_qty = total item.phase = DONE if total >= item.sell_qty else SOLD - item.trade_date = deal['insert_date'] + item.trade_date = deal['trade_date'] def reconcile( self, positions: list[PositionItem], deals: list[DealItem] ) -> None: """Deduplicate each fill; partial fills do not wait for order completion.""" today = datetime.now().date().isoformat() - seen = {row['sys_order_id'] for row in self.deals} + seen = {row['order_sys_id'] for row in self.deals} rows = [] for deal in deals: - if not self._is_zt_deal(deal) or deal.sys_order_id in seen: + if not self._is_zt_deal(deal) or deal.order_sys_id in seen: continue try: row = self._store.deal_record(deal) except ValueError: continue rows.append(row) - seen.add(deal.sys_order_id) - rows.sort(key=lambda r: (r['insert_date'], r['insert_time'])) + seen.add(deal.order_sys_id) + rows.sort(key=lambda r: (r['trade_date'], r['trade_time'])) modified = False try: # Snapshot includes these fills: subtract their net quantity before replay. net = {} for row in rows: - net[row['code']] = net.get(row['code'], 0) + ( - row['traded_volume'] if row['side'] == 'BUY' else -row['traded_volume'] + net[row['stock_code']] = net.get(row['stock_code'], 0) + ( + row['volume'] if str(row['offset_flag']) in ('23', '48') else -row['volume'] ) for position in positions: code = position.stock_code @@ -115,14 +115,14 @@ class TState: self.items[code] = TStateItem(code, -delta) for row in rows: - item = self.items.setdefault(row['code'], TStateItem(row['code'])) - self._reset(item, row['insert_date']) - qty, amount = row['traded_volume'], row['trade_amount'] - if row['local_order_id'].startswith('zt-base-'): + item = self.items.setdefault(row['stock_code'], TStateItem(row['stock_code'])) + self._reset(item, row['trade_date']) + qty, amount = row['volume'], row['trade_amount'] + if row['order_local_id'].startswith('zt-base-'): total = item.base_qty + qty item.base_cost = (item.base_qty * item.base_cost + amount) / total item.base_qty = total - item.base_order_id = row['local_order_id'] + item.base_order_id = row['order_local_id'] else: self._apply_t_deal(item, row) self.deals.append(row) @@ -141,15 +141,10 @@ class TState: self._store.save( { code: { - 'code': item.code, - 'base_order_id': item.base_order_id, - 'base_qty': item.base_qty, - 'base_cost': item.base_cost, - 'added_order_id': item.added_order_id, - 'added_num': item.added_num, - 'added_qty': item.added_qty, - 'added_cost': item.added_cost, - 'status': item.phase, + 'stock_code': item.code, + 'volume': item.base_qty, + 'open_price': item.base_cost, + 'open_cost': item.base_qty * item.base_cost, } for code, item in self.items.items() }, @@ -163,19 +158,21 @@ class TState: self._store.load() self.items = {} for code, position in self._store.positions.items(): - position = dict(position) - position['phase'] = position.pop('status') - self.items[code] = TStateItem(**position) + self.items[code] = TStateItem(code, position['volume'], position['open_price'], id=position['id']) self.deals = [ {key: value for key, value in deal.items() if key != 'id'} for deal in self._store.deals.values() ] # 轮次明细不占用持仓表字段,从已保存的逐笔成交重建。 for deal in self.deals: - if not deal['local_order_id'].startswith('zt-base-') and deal['code'] in self.items: - self._apply_t_deal(self.items[deal['code']], deal) - for code, item in self.items.items(): - if self._store.positions[code]['status'] == READY: - item.phase, item.trade_date = READY, '' - item.sell_qty = item.buy_qty = 0 - item.sell_price = item.buy_cost = 0.0 + item = self.items.get(deal['stock_code']) + if item is None: + continue + local_order_id = deal['order_local_id'] + if local_order_id.startswith('zt-base-'): + item.base_order_id = local_order_id + else: + self._apply_t_deal(item, deal) + today = datetime.now().date().isoformat() + for item in self.items.values(): + self._reset(item, today) diff --git a/py-client/tests/test_deal_model.py b/py-client/tests/test_deal_model.py index 81b0fbe..34d465b 100644 --- a/py-client/tests/test_deal_model.py +++ b/py-client/tests/test_deal_model.py @@ -1,75 +1,89 @@ -import sqlite3 +import ast import tempfile import unittest -from contextlib import closing -from dataclasses import asdict +from dataclasses import asdict, fields +from datetime import datetime from pathlib import Path - +from types import SimpleNamespace +from unittest.mock import Mock +from libs.order import OrderBook as ActiveOrders from libs.orderbook import OrderBook -from sdk.models import DealItem +from sdk.models import Assets, DealItem, OrderItem, PositionItem from sdk.portfolio import PortfolioMixin -class DealModelTests(unittest.TestCase): +class ApiModelTests(unittest.TestCase): def setUp(self): - self.raw = { - 'm_strOrderSysID': 'sys-123', - 'm_strInstrumentID': '600000', - 'm_strExchangeID': 'SH', - 'm_strInstrumentName': 'Test stock', - 'm_nOffsetFlag': '24', - 'm_nOrderStatus': '56', - 'm_nVolumeTotal': '0', - 'm_nVolumeTraded': '200', - 'm_nOrderTime': '101530', - 'm_strInsertDate': '20260907', - 'm_strInsertTime': '10:15:30', - 'm_strRemark': 'zt-t-sell-local1|zt', - 'm_dPrice': '15.8', - 'm_dTradePrice': '15.6', - 'm_dTradeAmount': '3120', - } + source = Path(__file__).resolve().parents[2] / 'api' / 'qmt_rest_new.py' + names = {'format_assets', 'format_holding', 'format_orders', 'format_deals'} + nodes = [n for n in ast.parse(source.read_text(encoding='utf-8')).body + if isinstance(n, ast.FunctionDef) and n.name in names] + ns = {'HTTPError': RuntimeError} + exec(compile(ast.Module(body=nodes, type_ignores=[]), str(source), 'exec'), ns) + attrs = {n.attr: '' if n.attr.startswith('m_str') else 0 + for node in nodes for n in ast.walk(node) + if isinstance(n, ast.Attribute) and n.attr.startswith('m_')} + attrs.update(m_strInstrumentID='600000', m_strExchangeID='SH', + m_strOrderSysID='sys1', m_strRemark='trend-BUY-1|trend', + m_nOffsetFlag=23, m_nOrderStatus=56, m_nVolume=100, + m_nVolumeTraded=100, m_nVolumeTotalOriginal=100, + m_dPrice=10.0, m_dTradeAmount=1000.0, m_dBalance=2000.0, + m_dAvailable=1000.0, m_strInsertDate='20260907', + m_strInsertTime='100000', m_strTradeDate='20260907', m_strTradeTime='100000') + obj = SimpleNamespace(**attrs) + self.assets = ns['format_assets']([obj]) + self.positions = ns['format_holding']([obj]) + self.orders = ns['format_orders']([obj]) + self.deals = ns['format_deals']([obj]) + self.client = PortfolioMixin() + self.client._get_json = { + '/api/portfolio/assets': self.assets, '/api/portfolio/positions': self.positions, + '/api/portfolio/order': self.orders, '/api/portfolio/deal': self.deals, + '/api/portfolio': {'assets': self.assets, 'positions': self.positions, 'orders': self.orders}, + }.__getitem__ - def test_all_api_fields_are_parsed(self): - self.assertEqual(asdict(DealItem.from_trade_detail(self.raw)), { - 'sys_order_id': 'sys-123', 'local_order_id': 'zt-t-sell-local1', - 'code': '600000.SH', 'instrument_id': '600000', 'exchange_id': 'SH', - 'name': 'Test stock', 'offset_flag': '24', 'side': 'SELL', 'status': '56', - 'remaining_volume': 0, 'traded_volume': 200, 'order_time': 101530, - 'insert_date': '20260907', 'insert_time': '10:15:30', - 'remark': 'zt-t-sell-local1|zt', 'price': 15.8, - 'trade_price': 15.6, 'trade_amount': 3120.0, - }) + def test_models_exactly_match_api_keys_and_values(self): + for model, row in ((Assets, self.assets), (PositionItem, self.positions['600000.SH']), + (OrderItem, self.orders[0]), (DealItem, self.deals[0])): + self.assertEqual({field.name for field in fields(model)}, set(row)) + self.assertEqual(asdict(model(**row)), row) - def test_api_deals_response_returns_deal_items(self): - client = PortfolioMixin() - for response in ({'deals': [self.raw]}, [self.raw]): - with self.subTest(response_type=type(response).__name__): - client._get_json = lambda path: response - deals = client.deals() - self.assertIsInstance(deals[0], DealItem) - self.assertEqual(deals[0].sys_order_id, 'sys-123') - self.assertEqual(deals[0].traded_volume, 200) + def test_all_endpoints(self): + self.assertEqual(asdict(self.client.assets()), self.assets) + codes, positions = self.client.positions() + self.assertEqual(codes, ['600000.SH']) + self.assertEqual(asdict(positions[0]), self.positions[codes[0]]) + self.assertEqual(asdict(self.client.orders()[0]), self.orders[0]) + self.assertEqual(asdict(self.client.deals()[0]), self.deals[0]) + portfolio = self.client.portfolio() + self.assertEqual(asdict(portfolio.positions[codes[0]]), self.positions[codes[0]]) + self.assertEqual(asdict(portfolio.orders[0]), self.orders[0]) - def test_model_matches_sql_columns_and_restart(self): - deal = DealItem.from_trade_detail(self.raw) + def test_derived_properties_and_order_cache(self): + order = self.client.orders()[0] + self.assertEqual(order.side, 'BUY') + self.assertEqual(order.local_order_id, 'trend-BUY-1') + self.assertEqual(order.created_at, datetime(2026, 9, 7, 10)) + order.order_status = 50 + order.insert_date = '20000101' + client = Mock() + book = ActiveOrders('trend') + book.refresh(client, [order]) + client.cancel_by_id.assert_called_once_with('sys1') + self.assertTrue(book.busy('600000.SH', 'BUY')) + + def test_storage_and_price_fallback(self): + deal = self.client.deals()[0] with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / 'state.db' book = OrderBook(path) book.sync_deals([deal]) - with closing(sqlite3.connect(path)) as db: - columns = {row[1] for row in db.execute('PRAGMA table_info(deals)')} - self.assertEqual(columns, {'id', *asdict(deal)}) - loaded = OrderBook(path).deals['sys-123'] - expected = asdict(deal) - expected['insert_date'] = '2026-09-07' - self.assertEqual({key: value for key, value in loaded.items() if key != 'id'}, expected) - - def test_missing_amount_uses_execution_price_not_order_price(self): - deal = DealItem.from_trade_detail({**self.raw, 'm_dTradeAmount': '0'}) - row = OrderBook.deal_record(deal) - self.assertEqual(row['trade_amount'], 3120) - deal.trade_price = 0 + loaded = OrderBook(path).deals['sys1'] + self.assertEqual(loaded['volume'], deal.volume) + self.assertEqual(loaded['trade_date'], '2026-09-07') + deal.trade_amount = 0 + self.assertEqual(OrderBook.deal_record(deal)['trade_amount'], 1000) + deal.price = 0 with self.assertRaises(ValueError): OrderBook.deal_record(deal) diff --git a/py-client/tests/test_orderbook.py b/py-client/tests/test_orderbook.py index 7b7a027..9c644a4 100644 --- a/py-client/tests/test_orderbook.py +++ b/py-client/tests/test_orderbook.py @@ -2,6 +2,7 @@ import sqlite3 import tempfile import unittest from contextlib import closing +from dataclasses import asdict, fields from pathlib import Path from datetime import datetime from unittest.mock import patch @@ -23,18 +24,12 @@ class OrderBookTests(unittest.TestCase): def deal(self, kind, sys_order_id, qty, price, date='2026-09-01'): prefix = {'base': 'zt-base-', 'sell': 'zt-t-sell-', 'buy': 'zt-t-buy-'}[kind] - return DealItem.from_trade_detail({ - 'm_strOrderSysID': sys_order_id, - 'm_strInstrumentID': '600000', 'm_strExchangeID': 'SH', - 'm_strInstrumentName': 'Test stock', - 'm_nOffsetFlag': '24' if kind == 'sell' else '23', - 'm_nOrderStatus': '56', 'm_nVolumeTotal': '0', - 'm_nVolumeTraded': str(qty), 'm_nOrderTime': '100000', - 'm_strInsertDate': date, 'm_strInsertTime': '10:00:00', - 'm_strRemark': prefix + 'order1|zt', - 'm_dPrice': str(price + 1), 'm_dTradePrice': str(price), - 'm_dTradeAmount': str(qty * price), - }) + return DealItem( + order_sys_id=sys_order_id, stock_code='600000.SH', + offset_flag=24 if kind == 'sell' else 23, + volume=qty, price=price, trade_amount=qty * price, + trade_date=date, trade_time='10:00:00', remark=prefix + 'order1|zt', + ) def test_partial_fills_restart_dedup_and_daily_cycle(self): state = TState(self.path) @@ -83,10 +78,12 @@ class OrderBookTests(unittest.TestCase): insert.assert_called_once() self.assertEqual(len(insert.call_args.args[1]), 2) self.assertEqual(book.deals_sys_ids, {'d1', 'd2'}) - self.assertEqual(book.deals['d1']['insert_date'], '2026-09-01') - self.assertEqual(book.deals['d2']['traded_volume'], 60) + self.assertEqual(book.deals['d1']['trade_date'], '2026-09-01') + self.assertEqual(book.deals['d2']['volume'], 60) + self.assertEqual(book.deals['d2']['order_local_id'], 'zt-base-order1') book = OrderBook(self.path) self.assertEqual(book.deals_sys_ids, {'d1', 'd2'}) + self.assertEqual(book.deals['d1']['order_local_id'], 'zt-base-order1') with patch.object(book, '_insert_deals') as insert: book.sync_deals([first, second]) book.sync_deals([]) @@ -97,25 +94,25 @@ class OrderBookTests(unittest.TestCase): book = OrderBook(self.path) first = self.deal('base', 'd1', 100, 10) invalid = self.deal('base', 'd2', 100, 10) - invalid.side = 'INVALID' + invalid.offset_flag = -1 with self.assertRaises(sqlite3.IntegrityError): book.sync_deals([first, invalid]) self.assertEqual(book.deals, {}) self.assertEqual(book.deals_sys_ids, set()) self.assertEqual(OrderBook(self.path).deals, {}) - invalid.side = 'BUY' + invalid.offset_flag = 23 book.sync_deals([first, invalid]) self.assertEqual(book.deals_sys_ids, {'d1', 'd2'}) def test_load_refreshes_all_caches(self): book = OrderBook(self.path) writer = OrderBook(self.path) - writer.save({'600000.SH': {'code': '600000.SH', 'status': READY}}, []) + writer.sync_positions([PositionItem(stock_code='600000.SH', volume=100)]) writer.sync_deals([self.deal('base', 'd1', 100, 10)]) book.load() - self.assertEqual(book.positions['600000.SH']['status'], READY) + self.assertEqual(book.positions['600000.SH']['volume'], 100) self.assertEqual(book.deals_sys_ids, {'d1'}) - self.assertEqual(book.deals['d1']['local_order_id'], 'zt-base-order1') + self.assertEqual(book.deals['d1']['remark'], 'zt-base-order1|zt') def test_first_start_after_full_sale_keeps_buyback_quantity(self): state = TState(self.path) @@ -148,12 +145,17 @@ class OrderBookTests(unittest.TestCase): tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")} self.assertEqual(tables, {'positions', 'deals', 'sqlite_sequence'}) columns = {row[1] for row in db.execute('PRAGMA table_info(deals)')} - self.assertFalse({'kind', 'confirmed_date', 'deal_ids', 'deal_id', 'order_id', 'qty', 'filled_qty', 'filled_cost', 'amount', 'trade_date', 'trade_time'} & columns) - self.assertTrue({'sys_order_id', 'local_order_id'} <= columns) + self.assertEqual(columns, {'id', 'order_local_id', *(field.name for field in fields(DealItem))}) indexes = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='index'")} - self.assertTrue({'idx_positions_code', 'idx_positions_base_order_id', - 'idx_positions_added_order_id', 'idx_deals_local_order_id', - 'idx_deals_code_date', 'idx_deals_date_time'} <= indexes) + self.assertTrue({'idx_positions_stock_code', 'idx_deals_order_sys_id', + 'idx_deals_order_ref', 'idx_deals_stock_code_date', 'idx_deals_date_time'} <= indexes) + for index, expected in ( + ('idx_deals_order_sys_id', ['order_sys_id']), + ('idx_deals_order_ref', ['order_local_id']), + ('idx_deals_stock_code_date', ['stock_code']), + ('idx_deals_date_time', ['trade_date']), + ): + self.assertEqual([row[2] for row in db.execute(f'PRAGMA index_info({index})')], expected) self.assertNotIn('kind', state.deals[0]) self.assertEqual(TState(self.path).deals, state.deals) state.deals.append(dict(state.deals[0])) @@ -168,29 +170,23 @@ class OrderBookTests(unittest.TestCase): def test_position_columns_defaults_indexes_and_stable_id(self): store = OrderBook(self.path) with closing(sqlite3.connect(self.path)) as db: - columns = [row[1] for row in db.execute('PRAGMA table_info(positions)')] - self.assertEqual(columns, [ - 'id', 'code', 'base_order_id', 'base_qty', 'base_cost', - 'added_order_id', 'added_num', 'added_qty', 'added_cost', 'status', - ]) + columns = {row[1] for row in db.execute('PRAGMA table_info(positions)')} + self.assertEqual(columns, {'id', *(field.name for field in fields(PositionItem))}) indexes = {row[1] for row in db.execute('PRAGMA index_list(positions)')} - self.assertEqual(indexes, { - 'idx_positions_code', 'idx_positions_base_order_id', 'idx_positions_added_order_id', - }) - store.save({'600000.SH': {'code': '600000.SH', 'status': READY}}, []) - position = store.positions['600000.SH'] - first_id = position['id'] - self.assertGreater(first_id, 0) - self.assertEqual(position['base_order_id'], '') - self.assertEqual(position['base_qty'], 0) - self.assertEqual(position['added_cost'], 0.0) - position.update(base_order_id='base1', base_qty=200, base_cost=10.5, - added_order_id='add1', added_num=1, added_qty=100, added_cost=9.0, - status='ACTIVE') - store.save({'600000.SH': position}, []) - self.assertEqual(store.positions['600000.SH'], position) - store.save({}, []) - store.save({'600001.SH': {'code': '600001.SH', 'status': READY}}, []) + self.assertEqual(indexes, {'idx_positions_stock_code'}) + position = PositionItem(stock_code='600000.SH', volume=100, open_price=10, + stock_name='stock', can_use_volume=100, float_profit=-2.5) + store.sync_positions([position]) + saved = store.positions[position.stock_code] + first_id = saved['id'] + self.assertEqual({k: v for k, v in saved.items() if k != 'id'}, asdict(position)) + position.volume = 200 + store.sync_positions([position]) + self.assertEqual(store.positions[position.stock_code]['id'], first_id) + self.assertEqual(store.positions[position.stock_code]['volume'], 200) + store.sync_positions([]) + self.assertEqual(store.positions, {}) + store.sync_positions([PositionItem(stock_code='600001.SH')]) self.assertGreater(store.positions['600001.SH']['id'], first_id) def test_base_split_fills_and_snapshot_do_not_double_count(self): @@ -207,12 +203,12 @@ class OrderBookTests(unittest.TestCase): state = TState(self.path) first = self.deal('base', 'd1', 100, 10, '20260901') other = self.deal('base', 'd2', 100, 10) - other.local_order_id = 'trend-base-order' + other.remark = 'trend-base-order' state.reconcile([], [first, other]) - first.insert_date = '2026-09-01' + first.trade_date = '2026-09-01' state.reconcile([], [first]) self.assertEqual(len(state.deals), 1) - self.assertEqual(state.deals[0]['insert_date'], '2026-09-01') + self.assertEqual(state.deals[0]['trade_date'], '2026-09-01') if __name__ == '__main__': diff --git a/py-client/tests/test_trend_collector.py b/py-client/tests/test_trend_collector.py new file mode 100644 index 0000000..51b1d0d --- /dev/null +++ b/py-client/tests/test_trend_collector.py @@ -0,0 +1,88 @@ +import importlib +import io +import logging +import unittest +from concurrent.futures import Future +from contextlib import ExitStack, redirect_stdout +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from libs import collector +from sdk import Assets, PositionItem +from strategy.trend import boot + + +class TrendCollectorTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + with patch('logging.FileHandler', return_value=logging.NullHandler()): + cls.app = importlib.import_module('main') + + def setUp(self): + old_snapshot = boot._collector_snapshot + self.addCleanup(setattr, boot, '_collector_snapshot', old_snapshot) + boot._collector_snapshot = None + + def test_submission_reads_latest_cache_and_skips_empty(self): + with patch.object(collector, 'collector_push') as push: + collector.submit_trend_data() + push.assert_not_called() + boot._cache_portfolio('account', Assets(available=100), []) + assets = Assets(available=200) + positions = [PositionItem(stock_code='600000.SH', volume=100)] + boot._cache_portfolio('account', assets, positions) + collector.submit_trend_data() + push.assert_called_once_with('account', assets, positions) + uploaded = push.call_args.args + uploaded[1].available = 0 + uploaded[2].clear() + self.assertEqual(boot.get_collector_snapshot()[1].available, 200) + self.assertEqual(len(boot.get_collector_snapshot()[2]), 1) + + def test_run_once_caches_portfolio_without_submitting_data(self): + completed = Future() + completed.set_result(None) + run = SimpleNamespace( + client=Mock(), orders=Mock(), executor=Mock(), + account_cfg=SimpleNamespace(account_id='account', min_cash_ratio=0.1), + ) + assets = Assets(available=100, total=1000) + run.client.portfolio.return_value = SimpleNamespace(assets=assets, positions={}, orders=[]) + run.client.full_tick.return_value = {} + run.executor.submit.return_value = completed + with patch.object(boot, 'trading_time', return_value=True), \ + patch.object(boot, 'market_allow_open', return_value=True), \ + patch.object(collector, 'collector_push') as push, redirect_stdout(io.StringIO()): + boot.RunOnce(run, []) + self.assertEqual(boot.get_collector_snapshot(), ('account', assets, [])) + push.assert_not_called() + run.executor.submit.assert_called_once_with(boot.manage_positions, run, {}, [], True, 100) + + def test_main_registers_five_minute_collector_job(self): + for strategy in ('trend', 'zt'): + with self.subTest(strategy=strategy), ExitStack() as stack: + scheduler = Mock(running=True) + stack.enter_context(patch.object(self.app, 'BackgroundScheduler', return_value=scheduler)) + stack.enter_context(patch.object(self.app, 'require_windows', return_value=True)) + stack.enter_context(patch.object(self.app, 'check_single_instance', return_value=True)) + stack.enter_context(patch.object(self.app, 'wait_for_qmt_api')) + stack.enter_context(patch.object(self.app.config, 'load')) + stack.enter_context(patch.object(self.app.config, 'global_config', SimpleNamespace(api_host='unused'))) + stack.enter_context(patch.object(self.app.config, 'account_config', SimpleNamespace(strategy=strategy))) + stack.enter_context(patch.dict(self.app.STRATEGIES, { + strategy: SimpleNamespace(start_strategy=Mock()), + })) + self.assertEqual(self.app.main(), 0) + jobs = [call for call in scheduler.add_job.call_args_list + if call.kwargs.get('id') == 'trend_collector'] + self.assertEqual(len(jobs), 1) + if jobs: + self.assertIs(jobs[0].args[0], collector.submit_trend_data) + self.assertEqual(jobs[0].kwargs['trigger'], 'interval') + self.assertEqual(jobs[0].kwargs['minutes'], 5) + scheduler.start.assert_called_once() + scheduler.shutdown.assert_called_once_with(wait=True) + + +if __name__ == '__main__': + unittest.main()