2026-09-07 14:04:26 +08:00
|
|
|
import ast
|
2026-09-07 00:27:33 +08:00
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
2026-09-07 14:04:26 +08:00
|
|
|
from dataclasses import asdict, fields
|
|
|
|
|
from datetime import datetime
|
2026-09-07 00:27:33 +08:00
|
|
|
from pathlib import Path
|
2026-09-07 14:04:26 +08:00
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from unittest.mock import Mock
|
|
|
|
|
from libs.order import OrderBook as ActiveOrders
|
2026-09-08 15:18:09 +08:00
|
|
|
from libs.state import State
|
2026-09-07 14:04:26 +08:00
|
|
|
from sdk.models import Assets, DealItem, OrderItem, PositionItem
|
2026-09-07 00:27:33 +08:00
|
|
|
from sdk.portfolio import PortfolioMixin
|
|
|
|
|
|
|
|
|
|
|
2026-09-07 14:04:26 +08:00
|
|
|
class ApiModelTests(unittest.TestCase):
|
2026-09-07 00:27:33 +08:00
|
|
|
def setUp(self):
|
2026-09-07 14:04:26 +08:00
|
|
|
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_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)
|
2026-09-07 00:27:33 +08:00
|
|
|
|
2026-09-07 14:04:26 +08:00
|
|
|
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])
|
2026-09-07 00:27:33 +08:00
|
|
|
|
2026-09-07 14:04:26 +08:00
|
|
|
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'))
|
2026-09-07 00:27:33 +08:00
|
|
|
|
2026-09-07 14:04:26 +08:00
|
|
|
def test_storage_and_price_fallback(self):
|
|
|
|
|
deal = self.client.deals()[0]
|
2026-09-07 00:27:33 +08:00
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
path = Path(tmp) / 'state.db'
|
2026-09-08 15:18:09 +08:00
|
|
|
book = State(path)
|
2026-09-07 00:27:33 +08:00
|
|
|
book.sync_deals([deal])
|
2026-09-08 15:18:09 +08:00
|
|
|
loaded = State(path).deals['sys1']
|
2026-09-07 14:04:26 +08:00
|
|
|
self.assertEqual(loaded['volume'], deal.volume)
|
|
|
|
|
self.assertEqual(loaded['trade_date'], '2026-09-07')
|
2026-09-08 15:18:09 +08:00
|
|
|
deal.order_sys_id = 'sys2'
|
|
|
|
|
deal.trade_amount = 0
|
|
|
|
|
book.sync_deals([deal, deal])
|
|
|
|
|
self.assertEqual(book.deals['sys2']['trade_amount'], 1000)
|
|
|
|
|
self.assertEqual(deal.trade_amount, 0)
|
|
|
|
|
deal.order_sys_id = 'sys3'
|
|
|
|
|
deal.price = 0
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
book.sync_deals([deal])
|
|
|
|
|
self.assertEqual(set(State(path).deals), {'sys1', 'sys2'})
|
2026-09-07 00:27:33 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
unittest.main()
|