Files
big-qmt/api/qmt_rest_new.py

439 lines
15 KiB
Python
Raw Normal View History

2026-09-02 21:19:33 +08:00
# -*- coding: utf-8 -*-
2026-08-25 16:40:18 +08:00
import json
import locale
import os
2026-09-03 11:33:43 +08:00
import sys
2026-09-03 19:58:04 +08:00
from urllib.request import Request, urlopen
2026-08-25 16:40:18 +08:00
from tornado.web import Application, RequestHandler, HTTPError
from tornado.ioloop import IOLoop
import logging
2026-08-28 22:46:04 +08:00
# Configuration
2026-08-25 16:40:18 +08:00
ACCOUNT_ID = os.environ.get('QMT_ACCOUNT_ID', '')
2026-09-03 11:33:43 +08:00
DATA_DIR = os.environ.get('QMT_DATA_DIR', r'D:\qmt_strategy_data')
2026-08-25 16:40:18 +08:00
TOKEN="QMTbyYanweidong"
PORT = 10086
2026-09-03 19:58:04 +08:00
PASS_CODES_URL = "http://139.224.247.176:13499/a/pass_codes"
2026-08-25 16:40:18 +08:00
# ===================================
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
locale.setlocale(locale.LC_CTYPE, 'chinese')
def safe_call(func, *args, **kwargs):
try:
return func(*args, **kwargs)
2026-08-29 00:44:41 +08:00
except HTTPError:
raise
2026-08-25 16:40:18 +08:00
except Exception as e:
2026-08-29 00:44:41 +08:00
raise HTTPError(
2026-09-02 21:03:45 +08:00
500,
reason="QMT: %s call failed." % func.__name__,
2026-08-29 00:44:41 +08:00
) from e
2026-08-25 16:40:18 +08:00
2026-09-03 19:58:04 +08:00
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
2026-08-25 16:40:18 +08:00
# ============= BaseHandler =============
AUTH_EXEMPT = set()
def no_auth(cls):
AUTH_EXEMPT.add(cls)
return cls
class BaseHandler(RequestHandler):
def prepare(self):
if self.__class__ not in AUTH_EXEMPT:
token = self.request.headers.get('X-Token')
if token != TOKEN:
2026-09-02 21:03:45 +08:00
raise HTTPError(500, "Authentication failed: invalid or missing token")
2026-08-25 16:40:18 +08:00
def set_default_headers(self):
self.set_header("Content-Type", "application/json; charset=utf-8")
2026-09-03 11:33:43 +08:00
def write_error(self,status_code, **kwargs):
2026-09-02 21:03:45 +08:00
self.finish(self._reason)
2026-08-25 16:40:18 +08:00
def ctx(self):
return self.application.ContextInfo
def acc(self):
return self.application.accountID
2026-09-03 11:33:43 +08:00
def write_json(self, data, default=None):
self.write(json.dumps(
data,
separators=(',', ':'),
ensure_ascii=False,
default=default,
))
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
# ============= 1. ContextInfo properties =============
2026-09-02 21:03:45 +08:00
# "/api/v2/context/info"
2026-08-31 23:18:04 +08:00
class ContextInfoHandler(BaseHandler):
2026-08-25 16:40:18 +08:00
def get(self):
2026-08-31 23:18:04 +08:00
ctx = self.ctx()
data = {
"period": ctx.period,
"barpos": ctx.barpos,
"time_tick_size": ctx.time_tick_size,
"stockcode": ctx.stockcode,
"dividend_type": ctx.dividend_type,
"market": ctx.market,
"do_back_test": ctx.do_back_test,
"benchmark": ctx.benchmark,
"capital": ctx.capital,
2026-09-02 21:03:45 +08:00
"timetag":ctx.timetag,
2026-08-31 23:18:04 +08:00
"universe": ctx.get_universe(),
}
2026-09-03 11:33:43 +08:00
self.write_json(data)
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
# ============= 2. Data queries (ContextInfo get_*) =============
2026-09-02 21:03:45 +08:00
STOCK_HANDLER = {
2026-09-02 21:19:33 +08:00
# handler_type: (method_name, use_context)
"stock_name": ("get_stock_name", True),
"open_date": ("get_open_date", True),
"last_volume": ("get_last_volume", True),
"total_share": ("get_total_share", True),
"svol": ("get_svol", True),
"bvol": ("get_bvol", True),
"divid_factors": ("get_divid_factors", True),
"etf_info": ("get_etf_info", False),
"etf_iopv": ("get_etf_iopv", False),
"instrumentdetail": ("get_instrumentdetail", True),
"his_st_data": ("get_his_st_data", True),
2026-09-02 21:03:45 +08:00
}
2026-09-02 21:19:33 +08:00
# "/api/v2/get/*" Stock-related single-symbol queries
2026-09-02 21:03:45 +08:00
class StockGetHandler(BaseHandler):
2026-09-02 21:19:33 +08:00
def get(self, handler_type):
2026-09-02 21:03:45 +08:00
# 快速路径:配置查找
cfg = STOCK_HANDLER.get(handler_type)
if not cfg:
2026-09-02 21:19:33 +08:00
raise HTTPError(500, "Unknown API")
2026-09-02 21:03:45 +08:00
# 参数验证
2026-09-02 21:19:33 +08:00
query_vals = self.get_query_argument("stock_code", "").strip()
2026-09-02 21:03:45 +08:00
if not query_vals:
2026-09-02 21:19:33 +08:00
raise HTTPError(500, "stock_code required")
2026-09-02 21:03:45 +08:00
# 方法调用
2026-09-02 21:19:33 +08:00
method_name, use_context = cfg
method = getattr(self.ctx(), method_name) if use_context else globals()[method_name]
2026-09-02 21:03:45 +08:00
result = safe_call(method, query_vals)
# 响应
2026-09-03 11:33:43 +08:00
self.write_json({
2026-09-02 21:03:45 +08:00
"stock_code": query_vals,
"ref": result
2026-09-03 11:33:43 +08:00
}, default=str)
2026-08-25 16:40:18 +08:00
2026-09-03 00:54:57 +08:00
# Aggregate assets, positions, and orders in one request.
class PortfolioHandler(BaseHandler):
def get(self):
2026-09-03 11:33:43 +08:00
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 []
2026-09-03 00:54:57 +08:00
result = {
"assets": format_assets(account_data),
"positions": format_holding(positions),
"orders": [fixed_fields(order) for order in orders],
}
2026-09-03 11:33:43 +08:00
self.write_json(result)
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
# get_trade_detail_data('position') - Query positions in the wrapped format
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({"data": holding})
2026-08-25 16:40:18 +08:00
2026-09-06 11:34:23 +08:00
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})
2026-09-03 11:33:43 +08:00
# 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(format_assets(_data))
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
class OrderHandler(BaseHandler):
def get(self):
ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order')
2026-08-25 16:40:18 +08:00
if ret is None:
2026-09-03 11:33:43 +08:00
ret = []
result = [fixed_fields(obj) for obj in ret]
self.write_json(result)
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
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({"deals": rets})
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
# ContextInfo.get_full_tick() - Get full tick data
2026-08-25 16:40:18 +08:00
class FullTickHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
2026-08-25 18:59:18 +08:00
stocks = data.get('stocks', [])
2026-08-26 16:37:06 +08:00
#if not stocks:
# raise HTTPError(400, "need args stocks")
2026-08-25 18:59:18 +08:00
ret = safe_call(self.ctx().get_full_tick, stocks)
2026-08-25 16:40:18 +08:00
if not ret:
2026-08-28 22:46:04 +08:00
raise HTTPError(500, "Failed to get tick data")
2026-09-03 11:33:43 +08:00
self.write_json(ret, default=str)
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
# passorder() - Submit a general trading order
2026-08-25 16:40:18 +08:00
class PassorderHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
opType = int(data['opType'])
orderType = int(data.get('orderType', 1101))
2026-09-03 11:33:43 +08:00
stockCode = data['stockCode']
2026-08-25 16:40:18 +08:00
pr_type = int(data.get('prType', 11))
price = float(data['price'])
volume = int(data['volume'])
quickTrade = int(data.get('quickTrade', 2))
2026-08-28 22:46:04 +08:00
strategy_name = str(data.get('strategyName', '')).strip()
order_id = str(data.get('orderId', '')).strip()
2026-08-29 00:44:41 +08:00
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
raise HTTPError(400, reason="Invalid order parameters: %s" % e) from e
# QMT stores strategyName in the order remark; preserve the signal key and local order ID.
try:
2026-09-03 11:33:43 +08:00
order_ref = passorder(opType, orderType, self.acc(), stockCode, pr_type, price, volume, strategy_name, quickTrade,order_id, self.ctx())
2026-08-29 00:44:41 +08:00
except HTTPError:
raise
2026-08-25 16:40:18 +08:00
except Exception as e:
2026-08-28 22:46:04 +08:00
logger.exception("passorder failed")
2026-08-29 00:44:41 +08:00
raise HTTPError(502, reason="QMT order submission failed") from e
2026-09-03 11:33:43 +08:00
self.write_json({
2026-08-29 00:44:41 +08:00
"status": "success",
"opType": opType,
2026-09-03 11:33:43 +08:00
"stockCode": stockCode,
2026-08-29 00:44:41 +08:00
"strategy_name": strategy_name,
"local_order_id": order_id,
"order_ref": str(order_ref)
2026-09-03 11:33:43 +08:00
})
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
class CancelByIdHandler(BaseHandler):
"""Cancel an order by its actual system order ID."""
def post(self):
data = json.loads(self.request.body)
order_id = str(data.get('order_id', '')).strip()
if not order_id:
raise HTTPError(400, "order_id cannot be empty")
2026-09-03 11:33:43 +08:00
cancelable = safe_call(can_cancel_order, order_id, self.acc(), 'stock')
2026-08-28 22:46:04 +08:00
if not cancelable:
2026-09-03 11:33:43 +08:00
self.write_json({
2026-08-28 22:46:04 +08:00
"status": "failed", "order_id": order_id,
"message": "Order does not exist or cannot currently be canceled"
2026-09-03 11:33:43 +08:00
})
2026-08-28 22:46:04 +08:00
return
2026-09-03 11:33:43 +08:00
result = safe_call(cancel, order_id, self.acc(), 'stock', self.ctx())
self.write_json({
2026-08-28 22:46:04 +08:00
"status": "success" if result is not False else "failed",
"order_id": order_id,
2026-09-03 11:33:43 +08:00
})
2026-08-25 16:40:18 +08:00
2026-09-04 21:04:34 +08:00
# 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)
2026-09-03 11:33:43 +08:00
# sys: Python version information
class PythonVersionHandler(BaseHandler):
def get(self):
version_info = {
"python_version": sys.version,
"python_version_info": {
"major": sys.version_info.major,
"minor": sys.version_info.minor,
"micro": sys.version_info.micro,
"releaselevel": sys.version_info.releaselevel,
"serial": sys.version_info.serial,
}
}
self.write_json(version_info)
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
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),
}
2026-08-25 16:40:18 +08:00
2026-09-03 00:54:57 +08:00
def format_holding(positions):
holding = {}
for position in positions:
stock = position.m_strInstrumentID + '.' + position.m_strExchangeID
holding[stock] = {
'StockCode': stock,
2026-09-05 00:23:55 +08:00
'TradeID':position.m_strTradeID,
2026-09-03 00:54:57 +08:00
'StockName': position.m_strInstrumentName,
'Direction': position.m_nDirection,
'Volume': position.m_nVolume,
'OpenPrice': position.m_dOpenPrice,
2026-09-06 11:34:23 +08:00
'OpenCost':position.m_dOpenCost,
2026-09-03 00:54:57 +08:00
'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
2026-09-03 11:33:43 +08:00
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()
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
def fixed_fields(obj, fields=TRADE_DETAIL_FIELDS):
result = {}
for field in fields:
2026-08-25 16:40:18 +08:00
try:
2026-09-03 11:33:43 +08:00
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)
2026-08-25 16:40:18 +08:00
}
2026-09-03 11:33:43 +08:00
return result
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
# ============= Route registration =============
2026-08-25 16:40:18 +08:00
def make_app():
return Application([
2026-09-02 21:03:45 +08:00
# ContextInfo properties
2026-09-03 11:33:43 +08:00
(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),
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
# Portfolio
(r"/api/portfolio", PortfolioHandler),
(r"/api/portfolio/positions", HoldingHandler),
2026-09-06 11:34:23 +08:00
(r"/api/portfolio/org/(account|order|deal|position)",OrgHandler),
2026-09-03 11:33:43 +08:00
(r"/api/portfolio/assets", AssetsHandler),
(r"/api/portfolio/order", OrderHandler),
(r"/api/portfolio/deal", DealHandler),
2026-08-25 16:40:18 +08:00
(r"/api/data/full_tick", FullTickHandler),
2026-09-04 21:04:34 +08:00
(r"/api/trade/ipo_data", IpoDataHandler),
2026-09-03 11:33:43 +08:00
(r"/api/trade/cancel_by_id", CancelByIdHandler),
2026-08-25 16:40:18 +08:00
(r"/api/trade/passorder", PassorderHandler),
2026-08-28 22:46:04 +08:00
# System
2026-08-25 16:40:18 +08:00
(r"/api/sys/python_version", PythonVersionHandler),
], debug=False)
def init(ContextInfo):
if not (ACCOUNT_ID or "").strip():
2026-08-28 22:46:04 +08:00
msg = "ACCOUNT_ID is empty; startup aborted"
2026-08-25 16:40:18 +08:00
logger.error(msg)
raise ValueError(msg)
if not (DATA_DIR or "").strip():
2026-08-28 22:46:04 +08:00
msg = "DATA_DIR is empty; startup aborted"
2026-08-25 16:40:18 +08:00
logger.error(msg)
raise ValueError(msg)
try:
ContextInfo.accountID = ACCOUNT_ID
ContextInfo.set_account(ACCOUNT_ID)
2026-09-03 19:58:04 +08:00
codes = get_pass_codes(ContextInfo.accountID)
ContextInfo.set_universe(list(codes))
2026-08-25 16:40:18 +08:00
# Api App
app = make_app()
app.ContextInfo = ContextInfo
app.accountID = ContextInfo.accountID
app.listen(PORT, address='0.0.0.0')
logger.info(f"ACCOUNT_ID: {ACCOUNT_ID}")
logger.info(f"DATA_DIR: {DATA_DIR}")
logger.info(f"TOKEN: {TOKEN}")
2026-08-28 22:46:04 +08:00
logger.info(f"Initialized symbol universe with {len(codes)} instruments")
logger.info(f"QMT HTTP Server started at http://0.0.0.0:{PORT} (all APIs loaded)")
2026-08-25 16:40:18 +08:00
IOLoop.current().start()
except Exception as e:
logger.exception(f"server start failed: {e}")