This commit is contained in:
2026-09-06 11:34:23 +08:00
parent b38ff12f2a
commit ac9e9193ad
32 changed files with 235 additions and 61 deletions

View File

@@ -186,6 +186,24 @@ class HoldingHandler(BaseHandler):
holding = format_holding(positions)
self.write_json({"data": holding})
class OrgHandler(BaseHandler):
def get(self, handler_type):
result = safe_call(
get_trade_detail_data, self.acc(), 'stock', handler_type
) or []
data = []
for item in result:
fields = {}
for name in dir(item):
if name.startswith('_'):
continue
value = getattr(item, name)
if not callable(value):
fields[name] = value
data.append(fields)
self.write_json({"data": data})
# get_trade_detail_data('account') - Query account assets
class AssetsHandler(BaseHandler):
def get(self):
@@ -318,6 +336,7 @@ def format_holding(positions):
'Direction': position.m_nDirection,
'Volume': position.m_nVolume,
'OpenPrice': position.m_dOpenPrice,
'OpenCost':position.m_dOpenCost,
'FloatProfit': position.m_dFloatProfit,
'MarketValue': position.m_dMarketValue,
'StockHolder': position.m_strStockHolder,
@@ -372,6 +391,7 @@ def make_app():
# Portfolio
(r"/api/portfolio", PortfolioHandler),
(r"/api/portfolio/positions", HoldingHandler),
(r"/api/portfolio/org/(account|order|deal|position)",OrgHandler),
(r"/api/portfolio/assets", AssetsHandler),
(r"/api/portfolio/order", OrderHandler),
(r"/api/portfolio/deal", DealHandler),

View File

@@ -2,17 +2,18 @@
import json
import locale
import os
import datetime
from pathlib import Path
import sys
from urllib.request import Request, urlopen
from tornado.web import Application, RequestHandler, HTTPError
from tornado.ioloop import IOLoop
import logging
# Configuration
ACCOUNT_ID = os.environ.get('QMT_ACCOUNT_ID', '')
DATA_DIR = os.environ.get('QMT_DATA_DIR', 'D:\qmt_strategy_data')
DATA_DIR = os.environ.get('QMT_DATA_DIR', r'D:\qmt_strategy_data')
TOKEN="QMTbyYanweidong"
PORT = 10086
PASS_CODES_URL = "http://139.224.247.176:13499/a/pass_codes"
# ===================================
logging.basicConfig(level=logging.INFO)
@@ -32,6 +33,36 @@ def safe_call(func, *args, **kwargs):
) from e
def get_pass_codes(account_id):
request = Request(
PASS_CODES_URL,
headers={"Accept": "application/json", "User-Agent": "big-qmt/1"},
)
with urlopen(request, timeout=10) as response:
payload = json.load(response)
remote_codes = payload.get("data")
if not isinstance(remote_codes, list):
raise ValueError("pass_codes response data must be an array")
positions = safe_call(
get_trade_detail_data, account_id, 'stock', 'position'
) or []
position_codes = [
position.m_strInstrumentID + '.' + position.m_strExchangeID
for position in positions
]
codes = []
seen = set()
for code in remote_codes + position_codes:
code = str(code).strip()
if code and code not in seen:
seen.add(code)
codes.append(code)
return codes
# ============= BaseHandler =============
AUTH_EXEMPT = set()
@@ -61,6 +92,14 @@ class BaseHandler(RequestHandler):
def acc(self):
return self.application.accountID
def write_json(self, data, default=None):
self.write(json.dumps(
data,
separators=(',', ':'),
ensure_ascii=False,
default=default,
))
# ============= 1. ContextInfo properties =============
# "/api/v2/context/info"
@@ -80,7 +119,7 @@ class ContextInfoHandler(BaseHandler):
"timetag":ctx.timetag,
"universe": ctx.get_universe(),
}
self.write(data, separators=(',', ':'), ensure_ascii=False)
self.write_json(data)
# ============= 2. Data queries (ContextInfo get_*) =============
STOCK_HANDLER = {
@@ -118,25 +157,26 @@ class StockGetHandler(BaseHandler):
# 响应
self.write(json.dumps({
self.write_json({
"stock_code": query_vals,
"ref": result
}, separators=(',', ':'), ensure_ascii=False, default=str))
}, default=str)
# Aggregate assets, positions, and orders in one request.
class PortfolioHandler(BaseHandler):
def get(self):
account_data = safe_call(get_trade_detail_data, self.acc(), 'stock', 'account')
positions = safe_call(get_trade_detail_data, self.acc(), 'stock', 'position') or []
orders = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') or []
account_id = self.acc()
account_data = safe_call(get_trade_detail_data, account_id, 'stock', 'account')
positions = safe_call(get_trade_detail_data, account_id, 'stock', 'position') or []
orders = safe_call(get_trade_detail_data, account_id, 'stock', 'order') or []
result = {
"assets": format_assets(account_data),
"positions": format_holding(positions),
"orders": [fixed_fields(order) for order in orders],
}
self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False))
self.write_json(result)
# get_trade_detail_data('position') - Query positions in the wrapped format
@@ -144,13 +184,31 @@ class HoldingHandler(BaseHandler):
def get(self):
positions = safe_call(get_trade_detail_data, self.acc(), 'stock', 'position') or []
holding = format_holding(positions)
self.write(json.dumps({"data": holding}, separators=(',', ':'), ensure_ascii=False))
self.write_json({"data": holding})
class OrgHandler(BaseHandler):
def get(self, handler_type):
result = safe_call(
get_trade_detail_data, self.acc(), 'stock', handler_type
) or []
data = []
for item in result:
fields = {}
for name in dir(item):
if name.startswith('_'):
continue
value = getattr(item, name)
if not callable(value):
fields[name] = value
data.append(fields)
self.write_json({"data": data})
# get_trade_detail_data('account') - Query account assets
class AssetsHandler(BaseHandler):
def get(self):
_data = safe_call(get_trade_detail_data, self.acc(), 'stock', 'account')
self.write(json.dumps(format_assets(_data), separators=(',', ':'), ensure_ascii=False))
self.write_json(format_assets(_data))
class OrderHandler(BaseHandler):
def get(self):
@@ -158,13 +216,13 @@ class OrderHandler(BaseHandler):
if ret is None:
ret = []
result = [fixed_fields(obj) for obj in ret]
self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False))
self.write_json(result)
class DealHandler(BaseHandler):
def get(self):
deals = safe_call(get_trade_detail_data, self.acc(), 'stock', 'deal') or []
rets = [fixed_fields(deal) for deal in deals]
self.write(json.dumps({"deals": rets}, separators=(',', ':'), ensure_ascii=False))
self.write_json({"deals": rets})
# ContextInfo.get_full_tick() - Get full tick data
class FullTickHandler(BaseHandler):
@@ -176,7 +234,7 @@ class FullTickHandler(BaseHandler):
ret = safe_call(self.ctx().get_full_tick, stocks)
if not ret:
raise HTTPError(500, "Failed to get tick data")
self.write(json.dumps(ret, separators=(',', ':'), ensure_ascii=False, default=str))
self.write_json(ret, default=str)
# passorder() - Submit a general trading order
class PassorderHandler(BaseHandler):
@@ -204,14 +262,14 @@ class PassorderHandler(BaseHandler):
logger.exception("passorder failed")
raise HTTPError(502, reason="QMT order submission failed") from e
self.write(json.dumps({
self.write_json({
"status": "success",
"opType": opType,
"stockCode": stockCode,
"strategy_name": strategy_name,
"local_order_id": order_id,
"order_ref": str(order_ref)
}, separators=(',', ':'), ensure_ascii=False))
})
class CancelByIdHandler(BaseHandler):
@@ -223,21 +281,28 @@ class CancelByIdHandler(BaseHandler):
raise HTTPError(400, "order_id cannot be empty")
cancelable = safe_call(can_cancel_order, order_id, self.acc(), 'stock')
if not cancelable:
self.write(json.dumps({
self.write_json({
"status": "failed", "order_id": order_id,
"message": "Order does not exist or cannot currently be canceled"
}, separators=(',', ':'), ensure_ascii=False))
})
return
result = safe_call(cancel, order_id, self.acc(), 'stock', self.ctx())
self.write(json.dumps({
self.write_json({
"status": "success" if result is not False else "failed",
"order_id": order_id,
}, separators=(',', ':'), ensure_ascii=False))
})
# get_ipo_data() - Get today's new stock and bond offerings
class IpoDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
typ = data.get('type', 'STOCK')
ret = safe_call(get_ipo_data, typ)
self.write_json(ret)
# sys: Python version information
class PythonVersionHandler(BaseHandler):
def get(self):
import sys
version_info = {
"python_version": sys.version,
"python_version_info": {
@@ -248,7 +313,73 @@ class PythonVersionHandler(BaseHandler):
"serial": sys.version_info.serial,
}
}
self.write(json.dumps(version_info, separators=(',', ':'), ensure_ascii=False))
self.write_json(version_info)
def format_assets(account_data):
info = account_data[0] if account_data else None
if not info:
raise HTTPError(500, "Failed to get account data")
return {
"total": round(info.m_dBalance, 2),
"available": round(info.m_dAvailable, 2),
}
def format_holding(positions):
holding = {}
for position in positions:
stock = position.m_strInstrumentID + '.' + position.m_strExchangeID
holding[stock] = {
'StockCode': stock,
'TradeID':position.m_strTradeID,
'StockName': position.m_strInstrumentName,
'Direction': position.m_nDirection,
'Volume': position.m_nVolume,
'OpenPrice': position.m_dOpenPrice,
'OpenCost':position.m_dOpenCost,
'FloatProfit': position.m_dFloatProfit,
'MarketValue': position.m_dMarketValue,
'StockHolder': position.m_strStockHolder,
'FrozenVolume': position.m_nFrozenVolume,
'CanUseVolume': position.m_nCanUseVolume,
'OnRoadVolume': position.m_nOnRoadVolume,
'YesterdayVolume': position.m_nYesterdayVolume,
'LastPrice': position.m_dLastPrice,
'ProfitRate': position.m_dProfitRate,
'FutureTradeType': position.m_eFutureTradeType,
'ExpireDate': position.m_strExpireDate
}
return holding
TRADE_DETAIL_FIELDS = (
'm_strOrderSysID', 'm_strInstrumentID', 'm_strExchangeID',
'm_strInstrumentName', 'm_nOffsetFlag', 'm_nOrderStatus',
'm_nVolumeTotal', 'm_nVolumeTraded', 'm_nOrderTime',
'm_strInsertDate', 'm_strInsertTime', 'm_strRemark',
'm_dPrice', 'm_dTradePrice', 'm_dTradeAmount',
)
MISSING = object()
def fixed_fields(obj, fields=TRADE_DETAIL_FIELDS):
result = {}
for field in fields:
try:
value = getattr(obj, field, MISSING)
except TypeError:
continue
if value is MISSING:
continue
if not callable(value):
result[field] = str(value)
if not result:
attrs = getattr(obj, '__dict__', {})
result = {
key: str(value) for key, value in attrs.items()
if not key.startswith('_') and not callable(value)
}
return result
# ============= Route registration =============
def make_app():
@@ -256,15 +387,17 @@ def make_app():
# ContextInfo properties
(r"/api/context/info", ContextInfoHandler),
(r"/api/get/(stock_name|open_date|last_volume|total_share|svol|bvol|divid_factors|etf_info|etf_iopv|instrumentdetail|his_st_data)", StockGetHandler),
# V2
# Portfolio
(r"/api/portfolio", PortfolioHandler),
(r"/api/portfolio/positions", HoldingHandler),
(r"/api/portfolio/org/(account|order|deal|position)",OrgHandler),
(r"/api/portfolio/assets", AssetsHandler),
(r"/api/portfolio/order", OrderHandler),
(r"/api/portfolio/deal", DealHandler),
(r"/api/data/full_tick", FullTickHandler),
(r"/api/trade/ipo_data", IpoDataHandler),
(r"/api/trade/cancel_by_id", CancelByIdHandler),
(r"/api/trade/passorder", PassorderHandler),
@@ -286,11 +419,9 @@ def init(ContextInfo):
try:
ContextInfo.accountID = ACCOUNT_ID
ContextInfo.set_account(ACCOUNT_ID)
# Load the symbol universe only when configured.
pass_codes_path = Path(DATA_DIR) / "pass_codes.json"
with pass_codes_path.open("r", encoding="utf-8") as stream:
codes = json.load(stream)
ContextInfo.set_universe(list(codes))
codes = get_pass_codes(ContextInfo.accountID)
ContextInfo.set_universe(list(codes))
# Api App
app = make_app()