fix bug
This commit is contained in:
177
py-client/tests/test_zt_ownership.py
Normal file
177
py-client/tests/test_zt_ownership.py
Normal 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()
|
||||
Reference in New Issue
Block a user