This commit is contained in:
2026-09-07 21:22:51 +08:00
parent 9778d54f3d
commit ba61ed5de7
28 changed files with 303 additions and 64 deletions

3
py-client/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.venv/
__pycache__/
*.py[cod]

View File

@@ -0,0 +1 @@
3.14

65
py-client/README.md Normal file
View File

@@ -0,0 +1,65 @@
# Python 3.14 客户端
运行目标为 Windows x64、标准 CPython 3.14;本次验证版本为 3.14.7。
迁移仅针对本目录QMT 服务端及其内置 Python 不变。
## 安装与运行
`py-client` 目录执行 PowerShell 命令:
```powershell
py -3.14 -m venv .venv
.venv/Scripts/python.exe -m pip install -r requirements.txt
.venv/Scripts/python.exe -m pip check
.venv/Scripts/python.exe main.py
```
`requirements.txt` 锁定本次在 Python 3.14 下实际验证的完整依赖版本。
不要复用 Python 3.11 的虚拟环境。`.python-version` 为支持该文件的工具声明版本。
## 优化范围与行为约束
已审查本目录全部 39 个原有 Python 源文件;只修改有适用优化或迁移需求的文件。
- 移除旧的 `from __future__ import annotations`,使用 Python 3.14 原生延迟求值注解,前向引用不再手工加引号。验证所有业务模块、类及方法注解可正常解析。
- 订单时间解析采用上限 4096 项的 LRU 缓存;每次刷新只读取一次时间和转换一次订单状态。缓存键为日期与时间原值,订单字段变化立即生效,继续使用原 `strptime` 解析规则。
- 信号时间边界解析采用上限 256 项的 LRU 缓存,当前时刻与允许交易的结果不缓存。
- 趋势、做 T 信号筛选直接查持仓字典,避免逐信号扫描持仓列表;候选顺序、重复信号及行情请求顺序不变。
- 交易时段常量复用,订单方向映射复用;避免构建单元素集合、合并校验列表和已存在状态的默认对象。
- 保持原浮点计算、价格阈值、资金规则、调度频率、线程结构、HTTP 重试、SQLite 事务及深复制隔离语义。
未启用 free-threaded Python、JIT 或多解释器线程池。现有交易任务共享客户端、锁和可变状态,切换并发模型不是等价替换。
延迟注解的运行时读取语义由原字符串注解变为按需求值,外部 SDK 调用者若需要字符串形式,应使用 `annotationlib.get_annotations(..., format=Format.STRING)`
官方说明:[Python 3.14 延迟注解](https://docs.python.org/3.14/whatsnew/3.14.html#pep-649-pep-749-deferred-evaluation-of-annotations)。
## 验证与性能
```powershell
.venv/Scripts/python.exe -B -m unittest discover -s tests -v
.venv/Scripts/python.exe -B benchmarks/hotpaths.py
```
25 项离线测试通过,包括原 18 项测试和新增的时间边界、缓存上限、可变订单、信号顺序、原生注解回归测试。
测试使用模拟客户端、临时 SQLite 数据库,不启动真实交易。
同一 CPython 3.14.7、原算法与优化算法对比;每组重复 5 次取中位数:
| 场景 | 原算法 µs/次 | 优化后 µs/次 | 倍率 |
| --- | ---: | ---: | ---: |
| 订单日期解析(缓存命中) | 4.263 | 0.061 | 69.90× |
| 信号时间边界解析(缓存命中) | 0.230 | 0.045 | 5.09× |
| 交易时段判断(下午) | 0.413 | 0.183 | 2.25× |
| 订单方向解析 | 0.163 | 0.092 | 1.78× |
| 1000 持仓、2000 信号筛选 | 13127.276 | 67.827 | 193.54× |
以上是局部微基准,缓存未命中仍执行原解析逻辑;不是 3.11 对 3.14 的整轮交易加速数据。
网络及数据库耗时未纳入,未做实盘端到端性能测量。
## 基线问题
修改前 18 项测试中 12 项失败,原因是模型仅有 `get_local_order_id` 属性,调用处却使用缺失的 `local_order_id`,存储层还将属性当方法调用。
本次增加同一属性的兼容别名,并统一存储层属性访问,保留原属性名和 API 数据字段;这些是使既有撤单、成交对账测试恢复的接口修复。
审查还发现既有 `strategy/zt/boot.py` 向做 T 的 `manage_positions``open_signal` 提交的参数与函数签名不匹配。
本次未改其调度和资金流程,因此 25 项测试通过不代表该既有做 T 启动路径已可用于实盘。

View File

@@ -0,0 +1,66 @@
"""Offline microbenchmarks; run with .venv/Scripts/python benchmarks/hotpaths.py."""
import sys
from datetime import datetime, time
from pathlib import Path
from statistics import median
from timeit import repeat
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from libs.calc import trading_time
from sdk.models import _parse_datetime, _side
from strategy.trend.open import _parse_minutes
def original_date(date, clock):
clock = clock.replace(':', '').zfill(6)
try:
return datetime.strptime(date.replace('-', '') + clock, '%Y%m%d%H%M%S')
except ValueError:
return None
def original_minutes(value):
try:
hour_text, minute_text = value.strip().split(':')
hour, minute = int(hour_text), int(minute_text)
except (TypeError, ValueError):
return None
if not 0 <= hour <= 23 or not 0 <= minute <= 59:
return None
return hour * 60 + minute
def original_trading_time(now):
if now.weekday() >= 5:
return False
return time(9, 30) <= now.time() <= time(11, 30) or time(13) <= now.time() <= time(15)
def measure(name, before, after, number=10000):
assert before() == after(), name
old = median(repeat(before, number=number, repeat=5)) / number
new = median(repeat(after, number=number, repeat=5)) / number
print(f'{name:26} {old * 1e6:10.3f} -> {new * 1e6:10.3f} us {old / new:7.2f}x')
def main():
print(sys.version)
print('Same interpreter, original versus optimized; cache timings are warm.')
now = datetime(2026, 9, 7, 14)
measure('order date', lambda: original_date('20260907', '100000'),
lambda: _parse_datetime('20260907', '100000'))
measure('signal time bound', lambda: original_minutes('9:30'), lambda: _parse_minutes('9:30'))
measure('trading session', lambda: original_trading_time(now), lambda: trading_time(now))
measure('order side', lambda: {'23': 'BUY', '24': 'SELL', '48': 'BUY', '49': 'SELL'}.get(str(23), ''),
lambda: _side(23))
positions = {f'{i:06}.SH': None for i in range(1000)}
codes = list(positions)
signals = [f'{i:06}.SH' for i in range(500, 2500)]
measure('1000 positions/2000 signals', lambda: [c for c in signals if c not in codes],
lambda: [c for c in signals if c not in positions], number=100)
if __name__ == '__main__':
main()

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import socket import socket
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path

View File

@@ -2,9 +2,14 @@ from datetime import datetime, time
from math import floor from math import floor
_MORNING_START, _MORNING_END = time(9, 30), time(11, 30)
_AFTERNOON_START, _AFTERNOON_END = time(13), time(15)
def trading_time(now: datetime) -> bool: def trading_time(now: datetime) -> bool:
if now.weekday() >= 5: return False if now.weekday() >= 5: return False
return time(9, 30) <= now.time() <= time(11, 30) or time(13) <= now.time() <= time(15) clock = now.time()
return _MORNING_START <= clock <= _MORNING_END or _AFTERNOON_START <= clock <= _AFTERNOON_END
def calc_buy_volume(price: float, buy_value: float) -> int: def calc_buy_volume(price: float, buy_value: float) -> int:

View File

@@ -1,7 +1,5 @@
"""简单的文件锁标记工具。""" """简单的文件锁标记工具。"""
from __future__ import annotations
from os import PathLike from os import PathLike
from pathlib import Path from pathlib import Path

View File

@@ -1,7 +1,5 @@
"""策略共用委托簿。""" """策略共用委托簿。"""
from __future__ import annotations
import secrets import secrets
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -69,17 +67,19 @@ class OrderBook:
canceled = 0 canceled = 0
for item in orders: for item in orders:
status = str(item.order_status)
# 不处理状态不对的 # 不处理状态不对的
if str(item.order_status) not in TRACKED_STATUSES: if status not in TRACKED_STATUSES:
continue continue
if str(item.order_status) in BUSY_STATUSES: if status in BUSY_STATUSES:
busy_keys.add(self._busy_key(item.side, item.stock_code)) busy_keys.add(self._busy_key(item.side, item.stock_code))
# 清理过期的 # 清理过期的
created_at = item.created_at
if ( if (
item.created_at is not None created_at is not None
and item.local_order_id.startswith(f"{self.order_prefix}-") and item.local_order_id.startswith(f"{self.order_prefix}-")
and str(item.order_status) in CANCELABLE_STATUSES and status in CANCELABLE_STATUSES
and current - item.created_at > self.cancel_timeout_sec and current - created_at > self.cancel_timeout_sec
): ):
try: try:
client.cancel_by_id(item.order_sys_id) client.cancel_by_id(item.order_sys_id)

View File

@@ -1,13 +1,12 @@
"""SQLite positions and deals, aligned with SDK models; one writer per database.""" """SQLite positions and deals, aligned with SDK models; one writer per database."""
from __future__ import annotations
import math import math
import sqlite3 import sqlite3
from contextlib import closing from contextlib import closing
from dataclasses import asdict, fields from dataclasses import asdict, fields
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from itertools import chain
from sdk import DealItem, PositionItem from sdk import DealItem, PositionItem
@@ -106,7 +105,7 @@ class OrderBook:
raise ValueError('System order ID and positive volume are required') raise ValueError('System order ID and positive volume are required')
row = asdict(deal) row = asdict(deal)
row['order_local_id'] = deal.get_local_order_id() row['order_local_id'] = deal.local_order_id
if not row['order_local_id']: if not row['order_local_id']:
raise ValueError('Local order ID is required') raise ValueError('Local order ID is required')
@@ -145,7 +144,7 @@ class OrderBook:
raise ValueError('Execution history is append-only') raise ValueError('Execution history is append-only')
new_deals = deals[len(self.deals):] new_deals = deals[len(self.deals):]
positions = [{**POSITION_DEFAULTS, **item} for item in items.values()] positions = [{**POSITION_DEFAULTS, **item} for item in items.values()]
for row in [*positions, *new_deals]: for row in chain(positions, new_deals):
if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()): if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()):
raise ValueError('Numeric values must be finite') raise ValueError('Numeric values must be finite')
with closing(self._connect()) as db, db: with closing(self._connect()) as db, db:

View File

@@ -1,7 +1,5 @@
"""策略单次运行所需的公共上下文对象。""" """策略单次运行所需的公共上下文对象。"""
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass from dataclasses import dataclass

View File

@@ -1,4 +1,13 @@
httpx>=0.27,<1 # Validated on Windows x64, CPython 3.14.7. Install with pip -r.
PyYAML>=6.0 anyio==4.15.1
APScheduler>=3.10,<4 APScheduler==3.11.3
CacheLib>=0.13,<1 cachelib==0.17.0
certifi==2026.7.22
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.19
PyYAML==6.0.3
typing_extensions==4.16.0
tzdata==2026.3
tzlocal==5.4.4

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import json import json
from dataclasses import asdict, is_dataclass from dataclasses import asdict, is_dataclass
from typing import Any from typing import Any
@@ -33,13 +31,13 @@ class HTTPClient:
def close(self) -> None: def close(self) -> None:
self.http.close() self.http.close()
def __enter__(self) -> "HTTPClient": def __enter__(self) -> HTTPClient:
return self return self
def __exit__(self, *_args: object) -> None: def __exit__(self, *_args: object) -> None:
self.close() self.close()
def set_account_type(self, account_type: str) -> "HTTPClient": def set_account_type(self, account_type: str) -> HTTPClient:
if account_type.strip(): if account_type.strip():
self.account_type = account_type self.account_type = account_type
return self return self

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from typing import Any from typing import Any

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from .models import Tick from .models import Tick

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from typing import Any from typing import Any
from urllib.parse import urlencode from urllib.parse import urlencode

View File

@@ -1,7 +1,10 @@
from __future__ import annotations
from datetime import datetime from datetime import datetime
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
from functools import lru_cache
_SIDES = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
def _number(value: Any, kind: type = float) -> Any: def _number(value: Any, kind: type = float) -> Any:
@@ -40,6 +43,9 @@ class OrderItem:
def get_local_order_id(self) -> str: def get_local_order_id(self) -> str:
return self.remark.split("|", 1)[0] return self.remark.split("|", 1)[0]
# Keep the existing property name available to SDK callers.
local_order_id = get_local_order_id
@property @property
def created_at(self) -> datetime | None: def created_at(self) -> datetime | None:
return _parse_datetime(self.insert_date, self.insert_time) return _parse_datetime(self.insert_date, self.insert_time)
@@ -71,6 +77,8 @@ class DealItem:
def get_local_order_id(self) -> str: def get_local_order_id(self) -> str:
return self.remark.split("|", 1)[0] return self.remark.split("|", 1)[0]
local_order_id = get_local_order_id
@dataclass(slots=True) @dataclass(slots=True)
class PositionItem: class PositionItem:
@@ -111,9 +119,10 @@ class Portfolio:
def _side(offset_flag: int) -> str: def _side(offset_flag: int) -> str:
return {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}.get(str(offset_flag), "") return _SIDES.get(str(offset_flag), "")
@lru_cache(maxsize=4096)
def _parse_datetime(date: str, clock: str) -> datetime | None: def _parse_datetime(date: str, clock: str) -> datetime | None:
clock = clock.replace(":", "").zfill(6) clock = clock.replace(":", "").zfill(6)
try: try:
@@ -129,7 +138,7 @@ class Tick:
raw: dict[str, Any] = field(default_factory=dict) raw: dict[str, Any] = field(default_factory=dict)
@classmethod @classmethod
def from_dict(cls, data: Any) -> "Tick": def from_dict(cls, data: Any) -> Tick:
if not isinstance(data, dict): if not isinstance(data, dict):
return cls() return cls()
return cls( return cls(

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from typing import Any from typing import Any
from .models import Assets, DealItem, OrderItem, Portfolio, PositionItem from .models import Assets, DealItem, OrderItem, Portfolio, PositionItem

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from typing import Any from typing import Any

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from typing import Any from typing import Any

View File

@@ -1,7 +1,5 @@
"""新股自动申购,提供交易日校验、券商对账和本地幂等保护。""" """新股自动申购,提供交易日校验、券商对账和本地幂等保护。"""
from __future__ import annotations
import json import json
import logging import logging
from datetime import datetime, time from datetime import datetime, time

View File

@@ -3,8 +3,6 @@
该模块负责组合 SDK、配置、状态存储和趋势策略组件供 main.py 调用。 该模块负责组合 SDK、配置、状态存储和趋势策略组件供 main.py 调用。
""" """
from __future__ import annotations
import time import time
import logging as log import logging as log
from concurrent.futures import Future, ThreadPoolExecutor from concurrent.futures import Future, ThreadPoolExecutor
@@ -161,7 +159,7 @@ def RunOnce(run: Runtime, signals: list[SignalItem]) -> None:
allow_open: list[SignalItem] = [] allow_open: list[SignalItem] = []
allow_codes: list[str] = [] allow_codes: list[str] = []
for signal in signals: for signal in signals:
if signal.code not in position_codes: if signal.code not in portfolio.positions:
allow_open.append(signal) allow_open.append(signal)
allow_codes.append(signal.code) allow_codes.append(signal.code)

View File

@@ -1,8 +1,7 @@
"""趋势策略开仓逻辑。""" """趋势策略开仓逻辑。"""
from __future__ import annotations
from datetime import datetime from datetime import datetime
from functools import lru_cache
from libs import calc_buy_volume from libs import calc_buy_volume
from sdk import OP_BUY from sdk import OP_BUY
@@ -164,6 +163,7 @@ def check_timezone(timezone: str, now: datetime | None = None) -> bool:
return False return False
@lru_cache(maxsize=256)
def _parse_minutes(value: str) -> int | None: def _parse_minutes(value: str) -> int | None:
"""把 ``时:分`` 转换为当天分钟数,无效值返回 None。""" """把 ``时:分`` 转换为当天分钟数,无效值返回 None。"""
try: try:

View File

@@ -1,7 +1,5 @@
"""趋势策略持仓止盈与分级补仓。""" """趋势策略持仓止盈与分级补仓。"""
from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from libs.calc import calc_buy_volume, calculate_min_profit_rate from libs.calc import calc_buy_volume, calculate_min_profit_rate
@@ -34,7 +32,7 @@ def manage_positions(
# 遍历处理每个持仓 # 遍历处理每个持仓
for position in positions: for position in positions:
try: try:
available = max(0, 0, available) available = max(0, available)
code = position.stock_code code = position.stock_code
tick = ticks.get(code) tick = ticks.get(code)
if code in runtime.account_cfg.excluded_codes: if code in runtime.account_cfg.excluded_codes:
@@ -130,7 +128,7 @@ def handle_profit(
False, False,
f"上涨至 {pnl_rate:.2f}%,峰值网格={observation.current_grid}", f"上涨至 {pnl_rate:.2f}%,峰值网格={observation.current_grid}",
) )
if observation.state in {GridState.STEADY}: if observation.state == GridState.STEADY:
return TradeDecision(False) return TradeDecision(False)
if runtime.orders.busy(position.stock_code, "SELL"): if runtime.orders.busy(position.stock_code, "SELL"):
return TradeDecision(False, "卖出委托处理中") return TradeDecision(False, "卖出委托处理中")

View File

@@ -3,8 +3,6 @@
该模块负责组合 SDK、配置、状态存储和做 T 策略组件,供 main.py 调用。 该模块负责组合 SDK、配置、状态存储和做 T 策略组件,供 main.py 调用。
""" """
from __future__ import annotations
from concurrent.futures import Future, ThreadPoolExecutor from concurrent.futures import Future, ThreadPoolExecutor
import logging as log import logging as log
import time import time
@@ -143,7 +141,7 @@ def RunOnce(run: Runtime, state: TState, signals: list[SignalItem]) -> None:
allow_open: list[SignalItem] = [] allow_open: list[SignalItem] = []
allow_codes: list[str] = [] allow_codes: list[str] = []
for signal in signals: for signal in signals:
if signal.code not in position_codes: if signal.code not in portfolio.positions:
allow_open.append(signal) allow_open.append(signal)
allow_codes.append(signal.code) allow_codes.append(signal.code)

View File

@@ -1,7 +1,5 @@
"""使用 dcm 信号建立做 T 底仓。""" """使用 dcm 信号建立做 T 底仓。"""
from __future__ import annotations
from datetime import datetime from datetime import datetime
import logging as log import logging as log
import math import math

View File

@@ -1,7 +1,5 @@
"""日内先卖后买的做 T 规则,不包含趋势补仓或整仓止盈。""" """日内先卖后买的做 T 规则,不包含趋势补仓或整仓止盈。"""
from __future__ import annotations
import logging as log import logging as log
import math import math

View File

@@ -1,7 +1,5 @@
"""做 T 策略的持仓状态和逐笔实际成交记录。""" """做 T 策略的持仓状态和逐笔实际成交记录。"""
from __future__ import annotations
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
@@ -115,7 +113,10 @@ class TState:
self.items[code] = TStateItem(code, -delta) self.items[code] = TStateItem(code, -delta)
for row in rows: for row in rows:
item = self.items.setdefault(row['stock_code'], TStateItem(row['stock_code'])) code = row['stock_code']
item = self.items.get(code)
if item is None:
item = self.items[code] = TStateItem(code)
self._reset(item, row['trade_date']) self._reset(item, row['trade_date'])
qty, amount = row['volume'], row['trade_amount'] qty, amount = row['volume'], row['trade_amount']
if row['order_local_id'].startswith('zt-base-'): if row['order_local_id'].startswith('zt-base-'):

View File

@@ -0,0 +1,113 @@
"""Offline regression checks for the Python 3.14 performance changes."""
import importlib
import inspect
import unittest
from annotationlib import Format, get_annotations
from concurrent.futures import Future
from datetime import datetime, time, timedelta
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
from libs.calc import trading_time
from libs.signal import SignalItem
from sdk.models import OrderItem, _parse_datetime
from strategy.trend import boot
from strategy.trend.open import _parse_minutes, check_timezone
class PerformanceRegressionTests(unittest.TestCase):
def test_trading_time_matches_original_for_week_and_boundaries(self):
start = datetime(2026, 9, 7)
for minute in range(7 * 24 * 60):
now = start + timedelta(minutes=minute)
expected = now.weekday() < 5 and (
time(9, 30) <= now.time() <= time(11, 30)
or time(13) <= now.time() <= time(15)
)
self.assertEqual(trading_time(now), expected, now)
for clock in ((11, 30), (15, 0)):
self.assertFalse(trading_time(start.replace(hour=clock[0], minute=clock[1], microsecond=1)))
def test_date_parser_preserves_strptime_acceptance(self):
for date in ('20260907', '2026-09-07', '', '20260229', '20240229', '202691'):
for clock in ('100000', '10:00:00', '93000', '', '240000', 'bad', '1'):
try:
expected = datetime.strptime(date.replace('-', '') + clock.replace(':', '').zfill(6), '%Y%m%d%H%M%S')
except ValueError:
expected = None
self.assertEqual(_parse_datetime(date, clock), expected, (date, clock))
def test_date_cache_tracks_mutable_order_fields(self):
order = OrderItem(insert_date='20260907', insert_time='100000', remark='first|trend')
self.assertEqual(order.created_at, datetime(2026, 9, 7, 10))
order.insert_time = '110000'
order.remark = 'second|trend'
self.assertEqual(order.created_at, datetime(2026, 9, 7, 11))
self.assertEqual(order.local_order_id, 'second')
self.assertEqual(order.get_local_order_id, 'second')
def test_caches_are_bounded(self):
_parse_minutes.cache_clear()
_parse_datetime.cache_clear()
for i in range(4200):
_parse_datetime('invalid', str(i))
_parse_minutes(str(i))
self.assertLessEqual(_parse_datetime.cache_info().currsize, 4096)
self.assertLessEqual(_parse_minutes.cache_info().currsize, 256)
def test_timezone_boundaries_and_current_time_not_cached(self):
for hour in range(24):
for minute in range(60):
now = datetime(2026, 9, 7, hour, minute)
m = hour * 60 + minute
self.assertEqual(check_timezone('9:30-10:30,invalid,23:00-1:00', now),
570 <= m <= 630 or m >= 1380 or m <= 60)
self.assertTrue(check_timezone('*'))
self.assertFalse(check_timezone('24:00-25:00'))
self.assertFalse(check_timezone(''))
def test_signal_order_duplicates_and_request_order_preserved(self):
future = Future()
future.set_result(None)
assets = SimpleNamespace(available=100, total=100)
portfolio = SimpleNamespace(assets=assets, positions={'held': object()}, orders=[])
run = SimpleNamespace(client=Mock(), orders=Mock(), executor=Mock(),
account_cfg=SimpleNamespace(account_id='test', min_cash_ratio=0.1))
run.client.portfolio.return_value = portfolio
run.client.full_tick.return_value = {}
run.executor.submit.return_value = future
signals = [SignalItem(code=c) for c in ('new-b', 'held', 'new-a', 'new-b')]
with patch.object(boot, 'trading_time', return_value=True), \
patch.object(boot, 'market_allow_open', return_value=True), \
patch.object(boot, '_cache_portfolio'), patch('builtins.print'):
boot.RunOnce(run, signals)
run.client.full_tick.assert_called_once_with(['held', 'new-b', 'new-a'])
self.assertEqual(run.executor.submit.call_args_list[1].args,
(boot.open_signal, run, {}, [signals[0], signals[2], signals[3]]))
def test_native_annotations_resolve_for_all_application_modules(self):
root = Path(__file__).resolve().parents[1]
paths = [p for name in ('config', 'sdk', 'libs', 'strategy')
for p in (root / name).rglob('*.py')]
for path in paths:
parts = list(path.relative_to(root).with_suffix('').parts)
if parts[-1] == '__init__':
parts.pop()
module = importlib.import_module('.'.join(parts))
for obj in vars(module).values():
if (inspect.isclass(obj) or inspect.isfunction(obj)) and obj.__module__ == module.__name__:
get_annotations(obj, format=Format.VALUE)
if inspect.isclass(obj):
for member in vars(obj).values():
if isinstance(member, (classmethod, staticmethod)):
member = member.__func__
elif isinstance(member, property):
member = member.fget
if inspect.isfunction(member):
get_annotations(member, format=Format.VALUE)
if __name__ == '__main__':
unittest.main()