Files
big-qmt/api/qmt_rest_new.py
2026-09-07 21:01:36 +08:00

448 lines
15 KiB
Python

# -*- coding: utf-8 -*-
import json
import locale
import os
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', 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)
logger = logging.getLogger(__name__)
locale.setlocale(locale.LC_CTYPE, 'chinese')
def safe_call(func, *args, **kwargs):
try:
return func(*args, **kwargs)
except HTTPError:
raise
except Exception as e:
raise HTTPError(
500,
reason="QMT: %s call failed." % func.__name__,
) 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()
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:
raise HTTPError(500, "Authentication failed: invalid or missing token")
def set_default_headers(self):
self.set_header("Content-Type", "application/json; charset=utf-8")
def write_error(self,status_code, **kwargs):
self.finish(self._reason)
def ctx(self):
return self.application.ContextInfo
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"
class ContextInfoHandler(BaseHandler):
def get(self):
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,
"timetag":ctx.timetag,
"universe": ctx.get_universe(),
}
self.write_json(data)
# ============= 2. Data queries (ContextInfo get_*) =============
STOCK_HANDLER = {
# 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),
}
# "/api/v2/get/*" Stock-related single-symbol queries
class StockGetHandler(BaseHandler):
def get(self, handler_type):
# 快速路径:配置查找
cfg = STOCK_HANDLER.get(handler_type)
if not cfg:
raise HTTPError(500, "Unknown API")
# 参数验证
query_vals = self.get_query_argument("stock_code", "").strip()
if not query_vals:
raise HTTPError(500, "stock_code required")
# 方法调用
method_name, use_context = cfg
method = getattr(self.ctx(), method_name) if use_context else globals()[method_name]
result = safe_call(method, query_vals)
# 响应
self.write_json({
"stock_code": query_vals,
"ref": result
}, default=str)
# Aggregate assets, positions, and orders in one request.
class PortfolioHandler(BaseHandler):
def get(self):
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": format_orders(orders),
}
self.write_json(result)
# 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 []
self.write_json(format_holding(positions))
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') or []
self.write_json(format_assets(_data))
class OrderHandler(BaseHandler):
def get(self):
ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') or []
self.write_json(format_orders(ret))
class DealHandler(BaseHandler):
def get(self):
deals = safe_call(get_trade_detail_data, self.acc(), 'stock', 'deal') or []
self.write_json(format_deals(deals))
# ContextInfo.get_full_tick() - Get full tick data
class FullTickHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stocks = data.get('stocks', [])
#if not stocks:
# raise HTTPError(400, "need args stocks")
ret = safe_call(self.ctx().get_full_tick, stocks)
if not ret:
raise HTTPError(500, "Failed to get tick data")
self.write_json(ret, default=str)
# passorder() - Submit a general trading order
class PassorderHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
opType = int(data['opType'])
orderType = int(data.get('orderType', 1101))
stockCode = data['stockCode']
pr_type = int(data.get('prType', 11))
price = float(data['price'])
volume = int(data['volume'])
quickTrade = int(data.get('quickTrade', 2))
strategy_name = str(data.get('strategyName', '')).strip()
order_id = str(data.get('orderId', '')).strip()
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:
order_ref = passorder(opType, orderType, self.acc(), stockCode, pr_type, price, volume, strategy_name, quickTrade,order_id, self.ctx())
except HTTPError:
raise
except Exception as e:
logger.exception("passorder failed")
raise HTTPError(500, reason="QMT order submission failed") from e
self.write_json({
"status": "success",
"opType": opType,
"stockCode": stockCode,
"strategy_name": strategy_name,
"local_order_id": order_id,
"order_ref": str(order_ref)
})
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")
cancelable = safe_call(can_cancel_order, order_id, self.acc(), 'stock')
if not cancelable:
self.write_json({
"status": "failed", "order_id": order_id,
"message": "Order does not exist or cannot currently be canceled"
})
return
result = safe_call(cancel, order_id, self.acc(), 'stock', self.ctx())
self.write_json({
"status": "success" if result is not False else "failed",
"order_id": order_id,
})
# 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):
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)
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] = {
'stock_code': stock,
'stock_name': position.m_strInstrumentName,
'direction': position.m_nDirection,
'volume': position.m_nVolume,
'open_price': position.m_dOpenPrice,
'open_cost':position.m_dOpenCost,
'float_profit': position.m_dFloatProfit,
'market_value': position.m_dMarketValue,
'stock_holder': position.m_strStockHolder,
'frozen_volume': position.m_nFrozenVolume,
'can_use_volume': position.m_nCanUseVolume,
'on_road_volume': position.m_nOnRoadVolume,
'yesterday_volume': position.m_nYesterdayVolume,
'last_price': position.m_dLastPrice,
'profit_rate': position.m_dProfitRate,
'future_trade_type': position.m_eFutureTradeType,
'expire_date': position.m_strExpireDate
}
return holding
def format_orders(orders):
"""显式转换委托/成交字段,保留原始数值类型。"""
result = []
for order in orders:
result.append({
'stock_code': order.m_strInstrumentID + '.' + order.m_strExchangeID,
'order_sys_id': order.m_strOrderSysID,
'ref': order.m_nRef,
'order_ref': order.m_strOrderRef,
'direction': order.m_nDirection,
'offset_flag': order.m_nOffsetFlag,
'limit_price': order.m_dLimitPrice,
'volume_total_original': order.m_nVolumeTotalOriginal,
'volume_traded': order.m_nVolumeTraded,
'volume_total': order.m_nVolumeTotal,
'traded_price': order.m_dTradedPrice,
'trade_amount': order.m_dTradeAmount,
'insert_date': order.m_strInsertDate,
'insert_time': order.m_strInsertTime,
'remark': order.m_strRemark,
'order_status': order.m_nOrderStatus,
})
return result
def format_deals(deals):
"""显式转换委托/成交字段,保留原始数值类型。"""
result = []
for d in deals:
result.append({
'stock_code': d.m_strInstrumentID + '.' + d.m_strExchangeID,
'order_sys_id': d.m_strOrderSysID,
'ref': d.m_nRef,
'order_ref': d.m_strOrderRef,
'direction': d.m_nDirection,
'offset_flag': d.m_nOffsetFlag,
'price': d.m_dPrice,
'volume': d.m_nVolume,
'trade_amount': d.m_dTradeAmount,
'trade_date': d.m_strTradeDate,
'trade_time': d.m_strTradeTime,
'remark': d.m_strRemark,
'close_profit': d.m_dCloseProfit,
})
return result
# ============= Route registration =============
def make_app():
return Application([
# 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),
# 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),
# System
(r"/api/sys/python_version", PythonVersionHandler),
], debug=False)
def init(ContextInfo):
if not (ACCOUNT_ID or "").strip():
msg = "ACCOUNT_ID is empty; startup aborted"
logger.error(msg)
raise ValueError(msg)
if not (DATA_DIR or "").strip():
msg = "DATA_DIR is empty; startup aborted"
logger.error(msg)
raise ValueError(msg)
try:
ContextInfo.accountID = ACCOUNT_ID
ContextInfo.set_account(ACCOUNT_ID)
codes = get_pass_codes(ContextInfo.accountID)
ContextInfo.set_universe(list(codes))
# 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}")
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)")
IOLoop.current().start()
except Exception as e:
logger.exception(f"server start failed: {e}")