This commit is contained in:
2026-09-15 20:02:05 +08:00
parent bfe89ba122
commit 04daeff141
37 changed files with 2674 additions and 1426 deletions

View File

@@ -0,0 +1,177 @@
"""ZT 归属过滤:非本策略成交通知不得进入账本,也不得中断策略。"""
import logging
import unittest
from sdk import DealItem
from strategy.zt import boot
from strategy.zt.ownership import OWNED_PREFIX, owned_deals, owns_local_order_id
from strategy.zt.rounds import RoundStore, start_round
from tests.zt_harness import Fixture
TODAY = '2026-09-15'
class OwnershipPredicateTests(unittest.TestCase):
def test_only_local_order_ids_generated_by_zt_are_owned(self):
owned = ['zt-base-8e9da97a42e957408489', 'zt-added-9239083181eb39712994',
'zt-entry-0b10c994b8242682983f', 'zt-exit-1']
foreign = ['', None, ' ', 'TREN-BUY-1', 'MORN-2', 'IPO-abc', 'DCM-3',
'zt', 'azt-base-1', 'ztbase-1']
for value in owned:
with self.subTest(value=value):
self.assertTrue(owns_local_order_id(value))
for value in foreign:
with self.subTest(value=value):
self.assertFalse(owns_local_order_id(value))
self.assertEqual(OWNED_PREFIX, 'zt-')
def test_owned_deals_splits_and_counts(self):
def deal(remark):
return DealItem(stock_code='600000.SH', order_sys_id=remark or 'none',
remark=remark)
deals = [deal('zt-entry-a'), deal(''), deal('TREN-BUY-1'), deal('zt-exit-b')]
owned, ignored = owned_deals(deals)
self.assertEqual([d.get_local_order_id for d in owned],
['zt-entry-a', 'zt-exit-b'])
self.assertEqual(ignored, 2)
class ForeignDealIsolationTests(unittest.TestCase):
"""手工单与其他策略单既不进轮次,也不影响本策略的判断。"""
def setUp(self):
self.fx = Fixture()
self.addCleanup(self.fx.cleanup)
logging.disable(logging.CRITICAL)
self.addCleanup(logging.disable, logging.NOTSET)
self.fx.hold('600000.SH', volume=1000, price=10.0)
self.fx.quote('600000.SH', 10.0)
def run_tick(self):
self.fx.tick()
return self.fx.store.get('600000.SH')
def test_manual_deal_without_remark_neither_raises_nor_blocks(self):
self.fx.open_round()
self.fx.deals = [DealItem(stock_code='600000.SH', order_sys_id='m1',
remark='', offset_flag=48, volume=100, price=9.0,
trade_amount=900.0, trade_date='20260915',
trade_time='100000')]
item = self.run_tick() # 旧实现在这里抛 IntegrityError
self.assertEqual(item.entry_filled_qty, 0)
self.assertEqual(item.outcome, 'aborted') # 开仓腿无成交且已不在途
self.assertEqual(self.fx.placed, [])
def test_foreign_strategy_deal_cannot_touch_the_round(self):
self.fx.open_round()
self.fx.deals = [DealItem(stock_code='600000.SH', order_sys_id='t1',
remark='TREN-BUY-9|trend', offset_flag=48,
volume=500, price=20.0, trade_amount=10000.0,
trade_date='20260915', trade_time='100000')]
item = self.run_tick() # 旧实现把外部买入当补仓写进 added 桶
self.assertEqual(item.entry_filled_qty, 0)
self.assertEqual(item.entry_amount, 0.0)
self.assertEqual(item.base_qty, 1000)
def test_owned_deals_are_still_counted(self):
self.fx.open_round()
self.fx.deals = [self.fx.deal('zt-entry-1', 300, 9.0)]
item = self.run_tick()
self.assertEqual(item.entry_filled_qty, 300)
self.assertEqual(item.entry_avg_price, 9.0)
# 现价 10.0 对买入均价 9.0 已超过一个网格步长,同一轮 tick 内即挂出卖单。
self.assertEqual(item.phase, 'CLOSING')
self.assertEqual([p['stock_code'] for p in self.fx.placed], ['600000.SH'])
self.assertTrue(self.fx.placed[0]['order_id'].startswith('zt-exit-'))
def test_mixed_batch_keeps_only_owned_deals(self):
self.fx.open_round()
self.fx.deals = [
self.fx.deal('zt-entry-1', 100, 9.0, sys_id='own'),
DealItem(stock_code='600000.SH', order_sys_id='manual', remark='',
offset_flag=48, volume=100, price=9.0, trade_amount=900.0,
trade_date='20260915', trade_time='100000'),
DealItem(stock_code='600000.SH', order_sys_id='trend',
remark='TREN-BUY-1', offset_flag=48, volume=100, price=9.0,
trade_amount=900.0, trade_date='20260915', trade_time='100000'),
]
item = self.run_tick()
self.assertEqual(item.entry_filled_qty, 100)
self.assertEqual(len(item.seen_deal_ids), 1)
def test_repeated_ticks_never_double_count(self):
self.fx.open_round()
self.fx.deals = [self.fx.deal('zt-entry-1', 300, 9.0)]
self.assertEqual(self.run_tick().entry_filled_qty, 300)
self.assertEqual(self.run_tick().entry_filled_qty, 300)
class RunOnceResilienceTests(unittest.TestCase):
"""任何单点失败都只能跳过本轮,不能打断唯一的交易定时线程。"""
def setUp(self):
self.fx = Fixture()
self.addCleanup(self.fx.cleanup)
logging.disable(logging.CRITICAL)
self.addCleanup(logging.disable, logging.NOTSET)
self.fx.hold('600000.SH', volume=1000, price=10.0)
self.fx.quote('600000.SH', 10.0)
def test_snapshot_failure_skips_the_round_quietly(self):
from unittest.mock import patch
with patch.object(boot, 'trading_time', return_value=True):
self.fx.client.portfolio.side_effect = RuntimeError('api down')
boot.RunOnce(self.fx.run, self.fx.store, [])
self.assertEqual(self.fx.placed, [])
def test_round_advance_failure_skips_trading(self):
from unittest.mock import patch
with patch.object(boot, 'trading_time', return_value=True), \
patch.object(boot, '_advance_rounds', side_effect=RuntimeError('broken')):
boot.RunOnce(self.fx.run, self.fx.store, [])
self.assertEqual(self.fx.placed, [])
def test_market_data_failure_skips_trading(self):
from unittest.mock import patch
with patch.object(boot, 'trading_time', return_value=True):
self.fx.client.full_tick.side_effect = RuntimeError('no ticks')
boot.RunOnce(self.fx.run, self.fx.store, [])
self.assertEqual(self.fx.placed, [])
def test_startup_state_failure_closes_client_without_raising(self):
from types import SimpleNamespace as NS
from unittest.mock import patch
client = self.fx.client
with patch.object(boot.config, 'account_config', self.fx.account_cfg), \
patch.object(boot.config, 'global_config',
NS(qmt_data_dir=str(self.fx.path), qmt_base_url='u',
qmt_token='', api_host='u')), \
patch.object(boot, 'Client', return_value=client), \
patch.object(boot, '_open_store', side_effect=RuntimeError('disk')):
boot.StartZT() # 不抛异常
client.close.assert_called_once()
def test_corrupt_state_is_backed_up_and_rebuilt(self):
from types import SimpleNamespace as NS
from unittest.mock import patch
path = self.fx.rounds_path
path.write_text('{not json', encoding='utf-8')
client = self.fx.client
with patch.object(boot.config, 'account_config', self.fx.account_cfg), \
patch.object(boot.config, 'global_config',
NS(qmt_data_dir=str(self.fx.path), qmt_base_url='u',
qmt_token='', api_host='u')) , \
patch.object(boot, 'Client', return_value=client), \
patch.object(boot, 'init_signals', return_value=[]), \
patch.object(boot.time, 'localtime',
return_value=NS(tm_hour=15, tm_min=0, tm_sec=0)):
boot.StartZT()
self.assertTrue(path.with_name(path.name + '.corrupt').is_file())
self.assertEqual(RoundStore(path).rounds, {})
client.close.assert_called_once()
if __name__ == '__main__':
unittest.main()