Files
big-qmt/api/qmt_rest_rele.py

308 lines
11 KiB
Python
Raw Normal View History

2026-09-03 00:54:57 +08:00
# -*- coding: utf-8 -*-
2026-08-25 16:40:18 +08:00
import json
import locale
import os
import datetime
from pathlib import Path
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', '')
DATA_DIR = os.environ.get('QMT_DATA_DIR', 'D:\qmt_strategy_data')
TOKEN="QMTbyYanweidong"
PORT = 10086
# ===================================
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-03 00:54:57 +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
# ============= 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-03 00:54:57 +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-03 00:54:57 +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-08-28 22:46:04 +08:00
# ============= 1. ContextInfo properties =============
2026-09-03 00:54:57 +08:00
# "/api/v2/context/info"
2026-09-02 21:03:45 +08:00
class ContextInfoHandler(BaseHandler):
2026-08-25 16:40:18 +08:00
def get(self):
2026-09-02 21:03:45 +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-03 00:54:57 +08:00
"timetag":ctx.timetag,
2026-09-02 21:03:45 +08:00
"universe": ctx.get_universe(),
}
self.write(data, separators=(',', ':'), ensure_ascii=False)
2026-08-25 16:40:18 +08:00
2026-08-28 22:46:04 +08:00
# ============= 2. Data queries (ContextInfo get_*) =============
2026-09-03 00:54:57 +08:00
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.dumps({
"stock_code": query_vals,
"ref": result
}, separators=(',', ':'), ensure_ascii=False, default=str))
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
# 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 []
2026-08-25 16:40:18 +08:00
2026-09-03 11:33:43 +08:00
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))
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.dumps({"data": holding}, separators=(',', ':'), ensure_ascii=False))
2026-08-25 16:40:18 +08:00
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.dumps(format_assets(_data), separators=(',', ':'), ensure_ascii=False))
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.dumps(result, separators=(',', ':'), ensure_ascii=False))
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.dumps({"deals": rets}, separators=(',', ':'), ensure_ascii=False))
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-08-31 23:18:04 +08:00
self.write(json.dumps(ret, separators=(',', ':'), ensure_ascii=False, 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
self.write(json.dumps({
"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-08-31 23:18:04 +08:00
}, separators=(',', ':'), ensure_ascii=False))
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:
self.write(json.dumps({
"status": "failed", "order_id": order_id,
"message": "Order does not exist or cannot currently be canceled"
2026-08-31 23:18:04 +08:00
}, separators=(',', ':'), ensure_ascii=False))
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())
2026-08-28 22:46:04 +08:00
self.write(json.dumps({
"status": "success" if result is not False else "failed",
"order_id": order_id,
2026-08-31 23:18:04 +08:00
}, separators=(',', ':'), ensure_ascii=False))
2026-08-28 22:46:04 +08:00
# sys: Python version information
2026-08-25 16:40:18 +08:00
class PythonVersionHandler(BaseHandler):
def get(self):
import sys
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,
}
}
2026-08-31 23:18:04 +08:00
self.write(json.dumps(version_info, separators=(',', ':'), ensure_ascii=False))
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-03 00:54:57 +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),
# V2
(r"/api/portfolio", PortfolioHandler),
(r"/api/portfolio/positions", HoldingHandler),
(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-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-08-28 22:46:04 +08:00
# Load the symbol universe only when configured.
2026-08-25 16:40:18 +08:00
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))
# 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}")