# -*- coding: utf-8 -*- 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 # Configuration 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) except HTTPError: raise except Exception as e: raise HTTPError( 500, reason="QMT: %s call failed." % func.__name__, ) from e # ============= 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 # ============= 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(data, separators=(',', ':'), ensure_ascii=False) # ============= 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.dumps({ "stock_code": query_vals, "ref": result }, separators=(',', ':'), ensure_ascii=False, 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 [] 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)) # 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)) # 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)) class OrderHandler(BaseHandler): def get(self): ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') if ret is None: ret = [] result = [fixed_fields(obj) for obj in ret] self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False)) 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)) # 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.dumps(ret, separators=(',', ':'), ensure_ascii=False, 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(502, reason="QMT order submission failed") from e self.write(json.dumps({ "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): """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.dumps({ "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({ "status": "success" if result is not False else "failed", "order_id": order_id, }, separators=(',', ':'), ensure_ascii=False)) # sys: Python version information 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, } } self.write(json.dumps(version_info, separators=(',', ':'), ensure_ascii=False)) # ============= 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), # 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), (r"/api/data/full_tick", FullTickHandler), (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) # 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)) # 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}")