feat libs,sdk,trend

This commit is contained in:
2026-09-07 14:04:26 +08:00
parent d37f9edefc
commit e320de3241
30 changed files with 515 additions and 507 deletions

View File

@@ -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)