This commit is contained in:
2026-09-03 00:54:57 +08:00
parent 07e74d054e
commit e2800fc193
17 changed files with 262 additions and 208 deletions

View File

@@ -1,4 +1,4 @@
# -*- coding: gbk -*-
# -*- coding: utf-8 -*-
import json
import locale
import os
@@ -26,10 +26,9 @@ def safe_call(func, *args, **kwargs):
except HTTPError:
raise
except Exception as e:
logger.exception("%s call failed", func.__name__)
raise HTTPError(
502,
reason="QMT upstream call failed: %s" % func.__name__,
500,
reason="QMT: %s call failed." % func.__name__,
) from e
@@ -48,16 +47,13 @@ class BaseHandler(RequestHandler):
if self.__class__ not in AUTH_EXEMPT:
token = self.request.headers.get('X-Token')
if token != TOKEN:
raise HTTPError(401, "Authentication failed: invalid or missing 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(json.dumps({
"error": self._reason,
"status_code": status_code
}, separators=(',', ':'), ensure_ascii=False))
def write_error(self, **kwargs):
self.finish(self._reason)
def ctx(self):
return self.application.ContextInfo
@@ -67,6 +63,7 @@ class BaseHandler(RequestHandler):
# ============= 1. ContextInfo properties =============
# "/api/v2/context/info"
class ContextInfoHandler(BaseHandler):
def get(self):
ctx = self.ctx()
@@ -80,38 +77,51 @@ class ContextInfoHandler(BaseHandler):
"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_*) =============
# ContextInfo.get_stock_name() - Get a stock name by symbol
class StockNameHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_stock_name, stockcode)
self.write(json.dumps({"stockcode": stockcode, "name": ret}, separators=(',', ':'), ensure_ascii=False))
# get_open_date() - Get the listing date by symbol
class OpenDateHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(get_open_date, stockcode)
self.write(json.dumps({"stockcode": stockcode, "open_date": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_last_volume() - Get the latest outstanding shares
class LastVolumeHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_last_volume, stockcode)
if ret is None:
raise HTTPError(500, "Failed to get outstanding shares")
self.write(json.dumps({"stockcode": stockcode, "last_volume": ret}, separators=(',', ':'), ensure_ascii=False))
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))
# ContextInfo.get_bar_timetag() - Get the bar timestamp
class BarTimetagHandler(BaseHandler):
@@ -121,12 +131,6 @@ class BarTimetagHandler(BaseHandler):
ret = safe_call(self.ctx().get_bar_timetag, index)
self.write(json.dumps({"index": index, "timetag": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_tick_timetag() - Get the latest tick timestamp
class TickTimetagHandler(BaseHandler):
def get(self):
ret = safe_call(self.ctx().get_tick_timetag)
self.write(json.dumps({"timetag": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_sector() - Get index constituents
class SectorHandler(BaseHandler):
def post(self):
@@ -260,14 +264,6 @@ class FullTickHandler(BaseHandler):
raise HTTPError(500, "Failed to get tick data")
self.write(json.dumps(ret, separators=(',', ':'), ensure_ascii=False, default=str))
# ContextInfo.get_divid_factors() - Get dividend and adjustment factors
class DividFactorsHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_divid_factors, stockcode)
self.write(json.dumps({"stockcode": stockcode, "factors": ret or {}}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_main_contract() - Get the main futures contract
class MainContractHandler(BaseHandler):
def post(self):
@@ -285,14 +281,6 @@ class TimetagToDatetimeHandler(BaseHandler):
ret = safe_call(timetag_to_datetime, timetag, fmt)
self.write(json.dumps({"timetag": timetag, "datetime": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_total_share() - Get total shares
class TotalShareHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_total_share, stockcode)
self.write(json.dumps({"stockcode": stockcode, "total_share": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_trading_dates() - Get the trading-day list
class TradingDatesHandler(BaseHandler):
def post(self):
@@ -306,22 +294,6 @@ class TradingDatesHandler(BaseHandler):
ret = safe_call(self.ctx().get_trading_dates, stockcode, start_date, end_date, count_int, period)
self.write(json.dumps({"dates": ret or []}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_svol() - Get sell-side volume
class SvolHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_svol, stockcode)
self.write(json.dumps({"stockcode": stockcode, "svol": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_bvol() - Get buy-side volume
class BvolHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_bvol, stockcode)
self.write(json.dumps({"stockcode": stockcode, "bvol": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_longhubang() - Get Dragon-Tiger List data
class LonghubangHandler(BaseHandler):
def post(self):
@@ -370,30 +342,6 @@ class TurnoverRateHandler(BaseHandler):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get turnover rate"}, separators=(',', ':'), ensure_ascii=False, default=str))
# get_etf_info() - Get ETF creation/redemption and constituent data
class EtfInfoHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(get_etf_info, stockcode)
self.write(json.dumps({"stockcode": stockcode, "info": ret or {}}, separators=(',', ':'), ensure_ascii=False, default=str))
# get_etf_iopv() - Get the ETF indicative optimized portfolio value
class EtfIopvHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(get_etf_iopv, stockcode)
self.write(json.dumps({"stockcode": stockcode, "iopv": ret}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_instrumentdetail() - Get instrument details
class InstrumentDetailHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_instrumentdetail, stockcode)
self.write(json.dumps({"stockcode": stockcode, "detail": ret or {}}, separators=(',', ':'), ensure_ascii=False, default=str))
# ContextInfo.get_contract_expire_date() - Get the futures contract expiration date
class ContractExpireDateHandler(BaseHandler):
def post(self):
@@ -454,14 +402,6 @@ class FactorDataHandler(BaseHandler):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret is not None else {"error": "Failed to get factor data"}, separators=(',', ':'), ensure_ascii=False, default=str))
# ContextInfo.get_his_st_data() - Get historical ST data
class HisStDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockCode = data.get('stockCode', '')
ret = safe_call(self.ctx().get_his_st_data, stockCode)
self.write(json.dumps({"stockCode": stockCode, "data": ret or {}}, separators=(',', ':'), ensure_ascii=False))
# ContextInfo.get_his_index_data() - Get historical index data
class HisIndexDataHandler(BaseHandler):
def post(self):
@@ -1272,6 +1212,9 @@ def make_app():
# V2
(r"/api/v2/positions", HoldingHandler),
(r"/api/v2/assets", AssetsHandler),
# ContextInfo properties
(r"/api/v2/context/info", ContextInfoHandler),
(r"/api/v2/get/(stock_name|open_date|last_volume|total_share|svol|bvol|divid_factors|etf_info|etf_iopv|instrumentdetail|his_st_data)", StockGetHandler),
# Legacy compatibility routes
(r"/api/holding", HoldingHandler),
@@ -1283,15 +1226,8 @@ def make_app():
(r"/api/order/cancel_by_id", CancelByIdHandler),
(r"/api/order/deal", DealHandler),
# ContextInfo properties
(r"/api/context/info", ContextInfoHandler),
# Data queries
(r"/api/data/stock_name", StockNameHandler),
(r"/api/data/open_date", OpenDateHandler),
(r"/api/data/last_volume", LastVolumeHandler),
(r"/api/data/bar_timetag", BarTimetagHandler),
(r"/api/data/tick_timetag", TickTimetagHandler),
(r"/api/data/sector", SectorHandler),
(r"/api/data/industry", IndustryHandler),
(r"/api/data/stock_list_in_sector", StockListInSectorHandler),
@@ -1303,25 +1239,17 @@ def make_app():
(r"/api/data/market_data", MarketDataHandler),
(r"/api/data/market_data_ex", MarketDataExHandler),
(r"/api/data/full_tick", FullTickHandler),
(r"/api/data/divid_factors", DividFactorsHandler),
(r"/api/data/main_contract", MainContractHandler),
(r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler),
(r"/api/data/total_share", TotalShareHandler),
(r"/api/data/trading_dates", TradingDatesHandler),
(r"/api/data/svol", SvolHandler),
(r"/api/data/bvol", BvolHandler),
(r"/api/data/longhubang", LonghubangHandler),
(r"/api/data/top10_share_holder", Top10ShareHolderHandler),
(r"/api/data/option_detail", OptionDetailHandler),
(r"/api/data/turnover_rate", TurnoverRateHandler),
(r"/api/data/etf_info", EtfInfoHandler),
(r"/api/data/etf_iopv", EtfIopvHandler),
(r"/api/data/instrumentdetail", InstrumentDetailHandler),
(r"/api/data/contract_expire_date", ContractExpireDateHandler),
(r"/api/data/option_undl_data", OptionUndlDataHandler),
(r"/api/data/financial_data", FinancialDataHandler),
(r"/api/data/factor_data", FactorDataHandler),
(r"/api/data/his_st_data", HisStDataHandler),
(r"/api/data/his_index_data", HisIndexDataHandler),
(r"/api/data/all_subscription", AllSubscriptionHandler),
(r"/api/data/option_list", OptionListHandler),