Files
big-qmt/api/QMT_API.py
2026-08-29 00:44:41 +08:00

1557 lines
70 KiB
Python

# -*- coding: gbk -*-
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:
logger.exception("%s call failed", func.__name__)
raise HTTPError(
502,
reason="QMT upstream call failed: %s" % 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(401, "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
}, ensure_ascii=False))
def ctx(self):
return self.application.ContextInfo
def acc(self):
return self.application.accountID
# ============= 1. ContextInfo properties =============
# ContextInfo.period - Get the current period
class ContextPeriodHandler(BaseHandler):
def get(self):
self.write(json.dumps({"period": self.ctx().period}, ensure_ascii=False))
# ContextInfo.barpos - Get the current bar index
class ContextBarposHandler(BaseHandler):
def get(self):
self.write(json.dumps({"barpos": self.ctx().barpos}, ensure_ascii=False))
# ContextInfo.time_tick_size - Get the current bar count
class ContextTimeTickSizeHandler(BaseHandler):
def get(self):
self.write(json.dumps({"time_tick_size": self.ctx().time_tick_size}, ensure_ascii=False))
# ContextInfo.stockcode - Get the current chart symbol
class ContextStockCodeHandler(BaseHandler):
def get(self):
self.write(json.dumps({"stockcode": self.ctx().stockcode}, ensure_ascii=False))
# ContextInfo.dividend_type - Get the current adjustment mode
class ContextDividendTypeHandler(BaseHandler):
def get(self):
self.write(json.dumps({"dividend_type": self.ctx().dividend_type}, ensure_ascii=False))
# ContextInfo.market - Get the current chart market
class ContextMarketHandler(BaseHandler):
def get(self):
self.write(json.dumps({"market": self.ctx().market}, ensure_ascii=False))
# ContextInfo.do_back_test - Check whether backtesting is enabled
class ContextDoBackTestHandler(BaseHandler):
def get(self):
self.write(json.dumps({"do_back_test": self.ctx().do_back_test}, ensure_ascii=False))
# ContextInfo.benchmark - Get the backtest benchmark
class ContextBenchmarkHandler(BaseHandler):
def get(self):
self.write(json.dumps({"benchmark": self.ctx().benchmark}, ensure_ascii=False))
# ContextInfo.capital - Get the initial backtest capital
class ContextCapitalHandler(BaseHandler):
def get(self):
self.write(json.dumps({"capital": self.ctx().capital}, ensure_ascii=False))
# ContextInfo.get_universe() - Get symbols in the universe
class ContextUniverseHandler(BaseHandler):
def get(self):
self.write(json.dumps({"universe": self.ctx().get_universe()}, 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}, 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}, 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}, ensure_ascii=False))
# ContextInfo.get_bar_timetag() - Get the bar timestamp
class BarTimetagHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
index = int(data.get('index', -1))
ret = safe_call(self.ctx().get_bar_timetag, index)
self.write(json.dumps({"index": index, "timetag": ret}, 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}, ensure_ascii=False))
# ContextInfo.get_sector() - Get index constituents
class SectorHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
sector = data.get('sector', '')
realtime = data.get('realtime', '0')
if not sector:
raise HTTPError(400, "need args sector")
ret = safe_call(self.ctx().get_sector, sector, int(realtime) if realtime != '0' else 0)
self.write(json.dumps({"sector": sector, "stocks": ret or []}, ensure_ascii=False))
# ContextInfo.get_industry() - Get industry constituents
class IndustryHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
industry = data.get('industry', '')
if not industry:
raise HTTPError(400, "need args industry")
print(industry)
ret = safe_call(self.ctx().get_industry, industry)
self.write(json.dumps({"industry": industry, "stocks": ret or []}, ensure_ascii=False))
# ContextInfo.get_stock_list_in_sector() - Get sector constituents
class StockListInSectorHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
sectorname = data.get('sectorname', '')
if not sectorname:
raise HTTPError(400, "need args sectorname")
ret = safe_call(self.ctx().get_stock_list_in_sector, sectorname)
self.write(json.dumps({"sectorname": sectorname, "stocks": ret or []}, ensure_ascii=False))
# ContextInfo.get_weight_in_index() - Get weights in an index
class WeightInIndexHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
indexcode = data.get('indexcode', '')
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().get_weight_in_index, indexcode, stockcode)
self.write(json.dumps({"indexcode": indexcode, "stockcode": stockcode, "weight": ret}, ensure_ascii=False))
# ContextInfo.get_contract_multiplier() - Get the contract multiplier
class ContractMultiplierHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
contractcode = data.get('contractcode', '')
ret = safe_call(self.ctx().get_contract_multiplier, contractcode)
self.write(json.dumps({"contractcode": contractcode, "multiplier": ret}, ensure_ascii=False))
# ContextInfo.get_risk_free_rate() - Get the risk-free rate
class RiskFreeRateHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
index = int(data.get('index', '-1'))
ret = safe_call(self.ctx().get_risk_free_rate, index)
self.write(json.dumps({"index": index, "risk_free_rate": ret}, ensure_ascii=False))
# ContextInfo.get_date_location() - Get the bar index for a date
class DateLocationHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
strdate = data.get('strdate', '')
ret = safe_call(self.ctx().get_date_location, strdate)
self.write(json.dumps({"strdate": strdate, "location": ret}, ensure_ascii=False))
# ContextInfo.get_history_data() - Get historical market data for multiple symbols
class HistoryDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
length = int(data.get('len', '10'))
period = data.get('period', '1d')
field = data.get('field', 'close')
dividend_type = int(data.get('dividend_type', '0'))
skip_paused = data.get('skip_paused', 'true').lower() == 'true'
ret = safe_call(self.ctx().get_history_data, length, period, field, dividend_type, skip_paused)
self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get historical data"}, ensure_ascii=False))
# ContextInfo.get_market_data() - Get market data as a DataFrame
class MarketDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
fields = data.get('fields', '')
stock_code = data.get('stock_code', '')
start_time = data.get('start_time', '')
end_time = data.get('end_time', '')
period = data.get('period', '1d')
dividend_type = data.get('dividend_type', 'none')
count = int(data.get('count', '-1'))
fields_list = [f.strip() for f in fields.split(',')] if fields else []
stock_list = [s.strip() for s in stock_code.split(',')] if stock_code else []
ret = safe_call(self.ctx().get_market_data, fields_list, stock_list, start_time, end_time, True, period, dividend_type, count)
if ret is None:
raise HTTPError(500, "Failed to get market data")
if hasattr(ret, 'to_dict'):
ret = ret.to_dict()
self.write(json.dumps({"data": ret}, ensure_ascii=False, default=str))
# ContextInfo.get_market_data_ex() - Get extended Level 2 market data
class MarketDataExHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
fields = data.get('fields', '')
stock_code = data.get('stock_code', '')
period = data.get('period', 'follow')
start_time = data.get('start_time', '')
end_time = data.get('end_time', '')
count = int(data.get('count', '-1'))
dividend_type = data.get('dividend_type', 'follow')
fields_list = [f.strip() for f in fields.split(',')] if fields else []
stock_list = [s.strip() for s in stock_code.split(',')] if stock_code else []
ret = safe_call(self.ctx().get_market_data_ex, fields_list, stock_list, period, start_time, end_time, count, dividend_type)
if ret is None:
raise HTTPError(500, "Failed to get extended market data")
result = {}
for k, v in ret.items():
if hasattr(v, 'to_dict'):
result[k] = v.to_dict()
else:
result[k] = str(v)
self.write(json.dumps({"data": result}, ensure_ascii=False, default=str))
# 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, 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 {}}, ensure_ascii=False))
# ContextInfo.get_main_contract() - Get the main futures contract
class MainContractHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
codemarket = data.get('codemarket', '')
ret = safe_call(self.ctx().get_main_contract, codemarket)
self.write(json.dumps({"codemarket": codemarket, "main_contract": ret}, ensure_ascii=False))
# timetag_to_datetime() - Convert a millisecond timestamp to datetime
class TimetagToDatetimeHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
timetag = int(data.get('timetag', '0'))
fmt = data.get('format', '%Y-%m-%d %H:%M:%S')
ret = safe_call(timetag_to_datetime, timetag, fmt)
self.write(json.dumps({"timetag": timetag, "datetime": ret}, 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}, ensure_ascii=False))
# ContextInfo.get_trading_dates() - Get the trading-day list
class TradingDatesHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
start_date = data.get('start_date', '')
end_date = data.get('end_date', '')
count = data.get('count', '')
period = data.get('period', '1d')
count_int = int(count) if count else -1
ret = safe_call(self.ctx().get_trading_dates, stockcode, start_date, end_date, count_int, period)
self.write(json.dumps({"dates": ret or []}, 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}, 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}, ensure_ascii=False))
# ContextInfo.get_longhubang() - Get Dragon-Tiger List data
class LonghubangHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stock_list = data.get('stock_list', '')
startTime = data.get('startTime', '')
endTime = data.get('endTime', '')
slist = [s.strip() for s in stock_list.split(',')] if stock_list else []
ret = safe_call(self.ctx().get_longhubang, slist, startTime, endTime)
if hasattr(ret, 'to_dict'):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get Dragon-Tiger List data"}, ensure_ascii=False, default=str))
# get_top10_share_holder() - Get top-ten shareholder data
class Top10ShareHolderHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stock_list = data.get('stock_list', '')
data_name = data.get('data_name', 'holder')
start_time = data.get('start_time', '')
end_time = data.get('end_time', '')
slist = [s.strip() for s in stock_list.split(',')] if stock_list else []
ret = safe_call(get_top10_share_holder, slist, data_name, start_time, end_time)
if hasattr(ret, 'to_dict'):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get top-ten shareholder data"}, ensure_ascii=False, default=str))
# ContextInfo.get_option_detail_data() - Get option details
class OptionDetailHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
optioncode = data.get('optioncode', '')
ret = safe_call(self.ctx().get_option_detail_data, optioncode)
self.write(json.dumps({"optioncode": optioncode, "detail": ret or {}}, ensure_ascii=False))
# ContextInfo.get_turnover_rate() - Get turnover rate
class TurnoverRateHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stock_list = data.get('stock_list', '')
startTime = data.get('startTime', '')
endTime = data.get('endTime', '')
slist = [s.strip() for s in stock_list.split(',')] if stock_list else []
ret = safe_call(self.ctx().get_turnover_rate, slist, startTime, endTime)
if hasattr(ret, 'to_dict'):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get turnover rate"}, 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 {}}, 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}, 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 {}}, ensure_ascii=False, default=str))
# ContextInfo.get_contract_expire_date() - Get the futures contract expiration date
class ContractExpireDateHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
codemarket = data.get('codemarket', '')
ret = safe_call(self.ctx().get_contract_expire_date, codemarket)
self.write(json.dumps({"codemarket": codemarket, "expire_date": ret}, ensure_ascii=False))
# ContextInfo.get_option_undl_data() - Get option products for an underlying asset
class OptionUndlDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
undl_code_ref = data.get('undl_code_ref', '')
ret = safe_call(self.ctx().get_option_undl_data, undl_code_ref)
self.write(json.dumps({"data": ret or []}, ensure_ascii=False, default=str))
# ContextInfo.get_financial_data() - Get financial data
class FinancialDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
tabname = data.get('tabname', '')
colname = data.get('colname', '')
market = data.get('market', '')
code = data.get('code', '')
report_type = data.get('report_type', 'report_time')
barpos = int(data.get('barpos', '-1'))
if tabname and colname and market and code:
ret = safe_call(self.ctx().get_financial_data, tabname, colname, market, code, report_type, barpos)
else:
field_list = data.get('fieldList', '')
stock_list = data.get('stockList', '')
start_date = data.get('startDate', '')
end_date = data.get('endDate', '')
fields = [f.strip() for f in field_list.split(',')] if field_list else []
stocks = [s.strip() for s in stock_list.split(',')] if stock_list else []
rtype = data.get('report_type', 'announce_time')
ret = safe_call(self.ctx().get_financial_data, fields, stocks, start_date, end_date, rtype)
if hasattr(ret, 'to_dict'):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret is not None else {"error": "Failed to get financial data"}, ensure_ascii=False, default=str))
# ContextInfo.get_factor_data() - Get multi-factor data
class FactorDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
field_list = data.get('fieldList', '')
stock_list = data.get('stockList', '')
stock_code = data.get('stockCode', '')
start_date = data.get('startDate', '')
end_date = data.get('endDate', '')
fields = [f.strip() for f in field_list.split(',')] if field_list else []
if stock_code:
ret = safe_call(self.ctx().get_factor_data, fields, stock_code, start_date, end_date)
else:
stocks = [s.strip() for s in stock_list.split(',')] if stock_list else []
ret = safe_call(self.ctx().get_factor_data, fields, stocks, start_date, end_date)
if hasattr(ret, 'to_dict'):
ret = ret.to_dict()
self.write(json.dumps({"data": ret} if ret is not None else {"error": "Failed to get factor data"}, 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 {}}, ensure_ascii=False))
# ContextInfo.get_his_index_data() - Get historical index data
class HisIndexDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
index = data.get('index', '')
ret = safe_call(self.ctx().get_his_index_data, index)
self.write(json.dumps({"index": index, "data": ret or {}}, ensure_ascii=False, default=str))
# ContextInfo.get_all_subscription() - Get all current market-data subscriptions
class AllSubscriptionHandler(BaseHandler):
def get(self):
ret = safe_call(self.ctx().get_all_subscription)
self.write(json.dumps({"subscriptions": ret or {}}, ensure_ascii=False, default=str))
# ContextInfo.get_option_list() - Get a specified option list
class OptionListHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
undl_code = data.get('undl_code', '')
dedate = data.get('dedate', '')
opttype = data.get('opttype', '')
isavailable = data.get('isavailable', 'true').lower() == 'true'
ret = safe_call(self.ctx().get_option_list, undl_code, dedate, opttype, isavailable)
self.write(json.dumps({"option_list": ret or []}, ensure_ascii=False))
# ContextInfo.get_his_contract_list() - Get expired contracts
class HisContractListHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
market = data.get('market', '')
ret = safe_call(self.ctx().get_his_contract_list, market)
self.write(json.dumps({"market": market, "contracts": ret or []}, ensure_ascii=False))
# ContextInfo.get_option_iv() - Get real-time option implied volatility
class OptionIvHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
optioncode = data.get('optioncode', '')
ret = safe_call(self.ctx().get_option_iv, optioncode)
self.write(json.dumps({"optioncode": optioncode, "iv": ret}, ensure_ascii=False))
# ContextInfo.bsm_price() - Calculate a European option price with the Black-Scholes model
class BsmPriceHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
optionType = data.get('optionType', 'C')
objectPrices = data.get('objectPrices', '')
strikePrice = float(data.get('strikePrice', '0'))
riskFree = float(data.get('riskFree', '0'))
sigma = float(data.get('sigma', '0'))
days = int(data.get('days', '0'))
dividend = float(data.get('dividend', '0'))
try:
op = float(objectPrices)
except ValueError:
op = [float(x) for x in objectPrices.split(',')]
ret = safe_call(self.ctx().bsm_price, optionType, op, strikePrice, riskFree, sigma, days, dividend)
self.write(json.dumps({"price": ret}, ensure_ascii=False, default=str))
# ContextInfo.bsm_iv() - Calculate European option implied volatility
class BsmIvHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
optionType = data.get('optionType', 'C')
objectPrices = float(data.get('objectPrices', '0'))
strikePrice = float(data.get('strikePrice', '0'))
optionPrice = float(data.get('optionPrice', '0'))
riskFree = float(data.get('riskFree', '0'))
days = int(data.get('days', '0'))
dividend = float(data.get('dividend', '0'))
ret = safe_call(self.ctx().bsm_iv, optionType, objectPrices, strikePrice, optionPrice, riskFree, days, dividend)
self.write(json.dumps({"iv": ret}, ensure_ascii=False))
# ContextInfo.get_local_data() - Get market data from local storage
class LocalDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stock_code = data.get('stock_code', '')
start_time = data.get('start_time', '')
end_time = data.get('end_time', '')
period = data.get('period', '1d')
divid_type = data.get('divid_type', 'none')
count = int(data.get('count', '-1'))
ret = safe_call(self.ctx().get_local_data, stock_code, start_time, end_time, period, divid_type, count)
if ret is None:
raise HTTPError(500, "Failed to get local market data")
self.write(json.dumps({"data": ret}, ensure_ascii=False, default=str))
# ContextInfo.subscribe_quote() - Subscribe to market data
class SubscribeQuoteHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stock_code = data.get('stock_code', '')
period = data.get('period', 'follow')
dividend_type = data.get('dividend_type', 'follow')
ret = safe_call(self.ctx().subscribe_quote, stock_code, period, dividend_type)
self.write(json.dumps({"status": "success" if ret is not None else "failed", "sub_id": ret}, ensure_ascii=False))
# ContextInfo.unsubscribe_quote() - Unsubscribe from market data
class UnsubscribeQuoteHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
sub_id = int(data.get('sub_id', '0'))
safe_call(self.ctx().unsubscribe_quote, sub_id)
self.write(json.dumps({"status": "success", "sub_id": sub_id}, ensure_ascii=False))
# ============= 3. Predicate functions (is_*) =============
# ContextInfo.is_last_bar() - Check whether this is the last bar
class IsLastBarHandler(BaseHandler):
def get(self):
ret = safe_call(self.ctx().is_last_bar)
self.write(json.dumps({"is_last_bar": ret}, ensure_ascii=False))
# ContextInfo.is_new_bar() - Check whether this is a new bar
class IsNewBarHandler(BaseHandler):
def get(self):
ret = safe_call(self.ctx().is_new_bar)
self.write(json.dumps({"is_new_bar": ret}, ensure_ascii=False))
# ContextInfo.is_suspended_stock() - Check whether a stock is suspended
class IsSuspendedStockHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stockcode = data.get('stockcode', '')
ret = safe_call(self.ctx().is_suspended_stock, stockcode)
self.write(json.dumps({"stockcode": stockcode, "is_suspended": ret}, ensure_ascii=False))
# is_sector_stock() - Check whether a stock belongs to a sector
class IsSectorStockHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
sectorname = data.get('sectorname', '')
market = data.get('market', '')
stockcode = data.get('stockcode', '')
ret = safe_call(is_sector_stock, sectorname, market, stockcode)
self.write(json.dumps({"sectorname": sectorname, "stockcode": stockcode, "is_in_sector": ret}, ensure_ascii=False))
# is_typed_stock() - Check whether a stock belongs to a category
class IsTypedStockHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
stocktypenum = int(data.get('stocktypenum', '0'))
market = data.get('market', '')
stockcode = data.get('stockcode', '')
ret = safe_call(is_typed_stock, stocktypenum, market, stockcode)
self.write(json.dumps({"stocktypenum": stocktypenum, "stockcode": stockcode, "result": ret}, ensure_ascii=False))
# get_industry_name_of_stock() - Get a stock's industry classification
class GetIndustryNameOfStockHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
industryType = data.get('industryType', '')
stockcode = data.get('stockcode', '')
ret = safe_call(get_industry_name_of_stock, industryType, stockcode)
self.write(json.dumps({"industryType": industryType, "stockcode": stockcode, "industry_name": ret}, ensure_ascii=False))
# ============= 4. Trading functions =============
# 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))
stock = data['stock']
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.
# Put the local ID first so restart reconciliation still works if QMT truncates the remark.
remark = '|'.join(part for part in (order_id, strategy_name) if part)[:24]
try:
order_ref = passorder(opType, orderType, self.acc(), stock, pr_type, price, volume, remark, quickTrade, self.ctx())
except HTTPError:
raise
except Exception as e:
logger.exception("passorder failed")
raise HTTPError(502, reason="QMT order submission failed") from e
if not order_ref:
raise HTTPError(502, reason="QMT did not return a valid order reference")
self.write(json.dumps({
"status": "success",
"opType": opType,
"stock": stock,
"strategy_name": strategy_name,
"local_order_id": order_id,
"order_ref": str(order_ref)
}, ensure_ascii=False))
# algo_passorder() - Submit an algorithmic order
class AlgoPassorderHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_ref = algo_passorder(
int(data['opType']), int(data.get('orderType', 1101)),
self.acc(), data['stock'], int(data.get('prType', -1)),
float(data['price']), int(data['volume']),
data.get('strategyName', ''), int(data.get('quickTrade', 2)),
data.get('userOrderId', ''), data.get('userOrderParam', {}),
self.ctx()
)
self.write(json.dumps({"status": "success", "order_ref": str(order_ref) if order_ref else "unknown"}, ensure_ascii=False))
except Exception as e:
logger.exception("algo_passorder failed")
raise HTTPError(400, f"Algorithmic order submission failed: {str(e)}")
# smart_algo_passorder() - Submit a smart algorithmic order
class SmartAlgoPassorderHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_ref = smart_algo_passorder(
int(data['opType']), int(data.get('orderType', 1101)),
self.acc(), data['stock'], int(data.get('prType', -1)),
float(data['price']), int(data['volume']),
data['smartAlgoType'], int(data.get('limitOverRate', 0)),
int(data.get('minAmountPerOrder', 0)),
data.get('startTime', ''), data.get('endTime', ''),
self.ctx()
)
self.write(json.dumps({"status": "success", "order_ref": str(order_ref) if order_ref else "unknown"}, ensure_ascii=False))
except Exception as e:
logger.exception("smart_algo_passorder failed")
raise HTTPError(400, f"Smart algorithmic order submission failed: {str(e)}")
# order_lots() - Trade a specified number of lots
class OrderLotsHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_lots(data['stock'], int(data['lots']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "order_lots", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("order_lots failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# order_value() - Trade a specified value
class OrderValueHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_value(data['stock'], float(data['value']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "order_value", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("order_value failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# order_percent() - Trade a specified percentage
class OrderPercentHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_percent(data['stock'], float(data['percent']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "order_percent", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("order_percent failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# order_target_value() - Trade to a target value
class OrderTargetValueHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_target_value(data['stock'], float(data['tar_value']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "order_target_value", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("order_target_value failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# order_target_percent() - Trade to a target percentage
class OrderTargetPercentHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_target_percent(data['stock'], float(data['tar_percent']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "order_target_percent", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("order_target_percent failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# order_shares() - Trade a specified number of shares
class OrderSharesHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
order_shares(data['stock'], int(data['shares']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "order_shares", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("order_shares failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# ============= 5. Futures trading =============
# buy_open() - Open a long futures position
class FuturesBuyOpenHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
buy_open(data['stock'], int(data['amount']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "buy_open", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("buy_open failed")
raise HTTPError(400, f"Failed to open a long futures position: {str(e)}")
# buy_close_tdayfirst() - Close a short futures position, today first
class FuturesBuyCloseTdayFirstHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
buy_close_tdayfirst(data['stock'], int(data['amount']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "buy_close_tdayfirst", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("buy_close_tdayfirst failed")
raise HTTPError(400, f"Failed to close a short futures position (today first): {str(e)}")
# buy_close_ydayfirst() - Close a short futures position, yesterday first
class FuturesBuyCloseYdayFirstHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
buy_close_ydayfirst(data['stock'], int(data['amount']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "buy_close_ydayfirst", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("buy_close_ydayfirst failed")
raise HTTPError(400, f"Failed to close a short futures position (yesterday first): {str(e)}")
# sell_open() - Open a short futures position
class FuturesSellOpenHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
sell_open(data['stock'], int(data['amount']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "sell_open", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("sell_open failed")
raise HTTPError(400, f"Failed to open a short futures position: {str(e)}")
# sell_close_tdayfirst() - Close a long futures position, today first
class FuturesSellCloseTdayFirstHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
sell_close_tdayfirst(data['stock'], int(data['amount']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "sell_close_tdayfirst", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("sell_close_tdayfirst failed")
raise HTTPError(400, f"Failed to close a long futures position (today first): {str(e)}")
# sell_close_ydayfirst() - Close a long futures position, yesterday first
class FuturesSellCloseYdayFirstHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
sell_close_ydayfirst(data['stock'], int(data['amount']), data.get('style', 'LATEST'),
float(data.get('price', 0)), self.ctx(), data.get('accId', self.acc()))
self.write(json.dumps({"status": "success", "action": "sell_close_ydayfirst", "stock": data['stock']}, ensure_ascii=False))
except Exception as e:
logger.exception("sell_close_ydayfirst failed")
raise HTTPError(400, f"Failed to close a long futures position (yesterday first): {str(e)}")
# ============= 6. Task management =============
# cancel_task() - Cancel a task
class CancelTaskHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
taskId = data['taskId']
accountType = data.get('accountType', 'stock')
ret = cancel_task(taskId, self.acc(), accountType, self.ctx())
self.write(json.dumps({"status": "success" if ret else "failed", "taskId": taskId}, ensure_ascii=False))
except Exception as e:
logger.exception("cancel_task failed")
raise HTTPError(400, f"Failed to cancel task: {str(e)}")
# pause_task() - Pause a task
class PauseTaskHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
taskId = data['taskId']
accountType = data.get('accountType', 'stock')
ret = pause_task(taskId, self.acc(), accountType, self.ctx())
self.write(json.dumps({"status": "success" if ret else "failed", "taskId": taskId}, ensure_ascii=False))
except Exception as e:
logger.exception("pause_task failed")
raise HTTPError(400, f"Failed to pause task: {str(e)}")
# resume_task() - Resume a task
class ResumeTaskHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
taskId = data['taskId']
accountType = data.get('accountType', 'stock')
ret = resume_task(taskId, self.acc(), accountType, self.ctx())
self.write(json.dumps({"status": "success" if ret else "failed", "taskId": taskId}, ensure_ascii=False))
except Exception as e:
logger.exception("resume_task failed")
raise HTTPError(400, f"Failed to resume task: {str(e)}")
# do_order() - Trigger the previous bar's signal in real time
class DoOrderHandler(BaseHandler):
def post(self):
try:
do_order(self.ctx())
self.write(json.dumps({"status": "success", "message": "Signal triggered"}, ensure_ascii=False))
except Exception as e:
logger.exception("do_order failed")
raise HTTPError(400, f"Failed to trigger signal: {str(e)}")
# ============= 7. Account and order queries =============
# Use fixed fields for high-frequency objects to avoid full dir/getattr reflection.
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',
)
def fixed_fields(obj, fields=TRADE_DETAIL_FIELDS):
result = {}
for field in fields:
try:
value = getattr(obj, field)
except (AttributeError, TypeError):
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)
}
return result
# get_trade_detail_data() - Get positions, orders, trades, or account details
class TradeDetailDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
datatype = data.get('datatype', 'position')
ret = safe_call(get_trade_detail_data, self.acc(), account, datatype)
if ret is None:
ret = []
result = [fixed_fields(obj) for obj in ret]
self.write(json.dumps({"data": result}, ensure_ascii=False))
# get_value_by_order_id() - Get order or trade details by order ID
class ValueByOrderIdHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
orderId = data.get('orderId', '')
accountType = data.get('accountType', 'stock')
datatype = data.get('datatype', 'ORDER')
ret = safe_call(get_value_by_order_id, orderId, self.acc(), accountType, datatype)
attrs = fixed_fields(ret) if ret else {}
self.write(json.dumps({"orderId": orderId, "data": attrs}, ensure_ascii=False))
# get_last_order_id() - Get the latest order ID
class LastOrderIdHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
datatype = data.get('datatype', 'ORDER')
ret = safe_call(get_last_order_id, self.acc(), account, datatype)
self.write(json.dumps({"last_order_id": ret}, ensure_ascii=False))
# can_cancel_order() - Check whether an order can be canceled
class CanCancelOrderHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
orderId = data.get('orderId', '')
accountType = data.get('accountType', 'stock')
ret = safe_call(can_cancel_order, orderId, self.acc(), accountType)
self.write(json.dumps({"orderId": orderId, "can_cancel": ret}, 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()
account_type = data.get('account_type', 'stock')
if not order_id:
raise HTTPError(400, "order_id cannot be empty")
cancelable = safe_call(can_cancel_order, order_id, self.acc(), account_type)
if not cancelable:
self.write(json.dumps({
"status": "failed", "order_id": order_id,
"message": "Order does not exist or cannot currently be canceled"
}, ensure_ascii=False))
return
result = safe_call(cancel, order_id, self.acc(), account_type, self.ctx())
self.write(json.dumps({
"status": "success" if result is not False else "failed",
"order_id": order_id,
}, ensure_ascii=False))
# get_debt_contract() - Get margin financing and securities lending debt contracts
class DebtContractHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
accId = data.get('accId', self.acc())
ret = safe_call(get_debt_contract, accId)
result = [fixed_fields(obj) for obj in (ret or [])]
self.write(json.dumps({"data": result}, ensure_ascii=False))
# get_assure_contract() - Get margin collateral instruments
class AssureContractHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
accId = data.get('accId', self.acc())
ret = safe_call(get_assure_contract, accId)
result = [fixed_fields(obj) for obj in (ret or [])]
self.write(json.dumps({"data": result}, ensure_ascii=False))
# get_enable_short_contract() - Get securities available for short selling
class EnableShortContractHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
accId = data.get('accId', self.acc())
ret = safe_call(get_enable_short_contract, accId)
result = [fixed_fields(obj) for obj in (ret or [])]
self.write(json.dumps({"data": result}, ensure_ascii=False))
# 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', '')
ret = safe_call(get_ipo_data, typ)
self.write(json.dumps({"data": ret or {}}, ensure_ascii=False, default=str))
# get_new_purchase_limit() - Get the new-share subscription quota
class NewPurchaseLimitHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
accid = data.get('accid', self.acc())
ret = safe_call(get_new_purchase_limit, accid)
self.write(json.dumps({"data": ret or {}}, ensure_ascii=False, default=str))
# ============= 8. Reference functions (ext_data) =============
# ext_data() - Get an extended-data value
class ExtDataHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
extdataname = data.get('extdataname', '')
stockcode = data.get('stockcode', '')
deviation = int(data.get('deviation', '0'))
ret = safe_call(ext_data, extdataname, stockcode, deviation, self.ctx())
self.write(json.dumps({"extdataname": extdataname, "stockcode": stockcode, "value": ret}, ensure_ascii=False))
# ext_data_rank() - Get an extended-data ranking
class ExtDataRankHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
extdataname = data.get('extdataname', '')
stockcode = data.get('stockcode', '')
deviation = int(data.get('deviation', '0'))
ret = safe_call(ext_data_rank, extdataname, stockcode, deviation, self.ctx())
self.write(json.dumps({"extdataname": extdataname, "stockcode": stockcode, "rank": ret}, ensure_ascii=False))
# get_factor_value() - Get a factor value
class GetFactorValueHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
factorname = data.get('factorname', '')
stockcode = data.get('stockcode', '')
deviation = int(data.get('deviation', '0'))
ret = safe_call(get_factor_value, factorname, stockcode, deviation, self.ctx())
self.write(json.dumps({"factorname": factorname, "stockcode": stockcode, "value": ret}, ensure_ascii=False))
# get_factor_rank() - Get a factor ranking
class GetFactorRankHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
factorname = data.get('factorname', '')
stockcode = data.get('stockcode', '')
deviation = int(data.get('deviation', '0'))
ret = safe_call(get_factor_rank, factorname, stockcode, deviation, self.ctx())
self.write(json.dumps({"factorname": factorname, "stockcode": stockcode, "rank": ret}, ensure_ascii=False))
# ============= 9. Legacy handlers (compatibility) =============
# get_trade_detail_data('position') - Query positions in the wrapped format
class HoldingHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
positions = safe_call(get_trade_detail_data, self.acc(), account, 'position') or []
holding = {}
for position in positions:
stock = position.m_strInstrumentID + '.' + position.m_strExchangeID
holding[stock] = {
'StockCode': stock,
'StockName': position.m_strInstrumentName,
'Direction': position.m_nDirection,
'Volume': position.m_nVolume,
'OpenPrice': position.m_dOpenPrice,
'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
}
self.write(json.dumps(holding, ensure_ascii=False))
# get_trade_detail_data('account') - Query account assets
class AssetsHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
_data = safe_call(get_trade_detail_data, self.acc(), account, 'account')
info = _data[0] if _data else None
if not info:
raise HTTPError(500, "Failed to get account data")
self.write(json.dumps({"total": round(info.m_dBalance, 2),"available": round(info.m_dAvailable, 2)}, ensure_ascii=False))
# get_trade_detail_data('account') - Query total assets
class TotalMoneyHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
_data = safe_call(get_trade_detail_data, self.acc(), account, 'account')
info = _data[0] if _data else None
if not info:
raise HTTPError(500, "Failed to get account data")
self.write(json.dumps({"total_money": round(info.m_dBalance, 2)}, ensure_ascii=False))
# get_trade_detail_data('account') - Query available cash
class AvailableMoneyHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
_data = safe_call(get_trade_detail_data, self.acc(), account, 'account')
info = _data[0] if _data else None
if not info:
raise HTTPError(500, "Failed to get account data")
self.write(json.dumps({"available_money": round(info.m_dAvailable, 2)}, ensure_ascii=False))
# passorder(23) - Simplified buy order wrapper
class BuyHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
stock = data['stock']
price = float(data['price'])
volume = int(data['volume'])
pr_type = data.get('prType', 11)
sn = data.get('sn','')
order_ref = passorder(23, 1101, self.acc(), stock, pr_type, price, volume, sn, 2, self.ctx())
self.write(json.dumps({
"status": "success", "action": "buy", "stock": stock,
"order_ref": str(order_ref) if order_ref else "unknown"
}, ensure_ascii=False))
except Exception as e:
logger.exception("Buy order submission failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# passorder(24) - Simplified sell order wrapper
class SellHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
stock = data['stock']
price = float(data['price'])
volume = int(data['volume'])
pr_type = data.get('prType', 11)
sn = data.get('sn','')
order_ref = passorder(24, 1101, self.acc(), stock, pr_type, price, volume, sn, 2, self.ctx())
self.write(json.dumps({
"status": "success", "action": "sell", "stock": stock,
"order_ref": str(order_ref) if order_ref else "unknown"
}, ensure_ascii=False))
except Exception as e:
logger.exception("Sell order submission failed")
raise HTTPError(400, f"Order submission failed: {str(e)}")
# get_trade_detail_data('order') - Query order statuses
class OrderStatusHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
sn = data.get('sn','')
orders = safe_call(get_trade_detail_data, self.acc(), account, 'order', sn) or []
rets = []
for order in orders:
rets.append({
"order_sys_id": order.m_strOrderSysID,
"status": order.m_nOrderStatus,
"volume_left": order.m_nVolumeTotal,
"volume_traded": order.m_nVolumeTraded,
})
self.write(json.dumps({"orders": rets}, ensure_ascii=False))
# cancel() - Cancel all orders
class CancelAllHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
account = data.get('account', 'stock')
sn = data.get('sn','')
orders = safe_call(get_trade_detail_data, self.acc(), account, 'order', sn) or []
canceled_list = []
for order in orders:
if can_cancel_order(order.m_strOrderSysID, self.acc(), account):
cancel(order.m_strOrderSysID, self.acc(), account, self.ctx())
canceled_list.append({
"order_sys_id": order.m_strOrderSysID,
"stock": order.m_strInstrumentID,
"volume_left": order.m_nVolumeTotal
})
self.write(json.dumps({
"status": "success",
"message": f"Submitted {len(canceled_list)} cancellation requests",
"canceled_orders": canceled_list
}, ensure_ascii=False))
except Exception as e:
logger.exception("Cancel-all operation failed")
raise HTTPError(500, f"Order cancellation failed: {str(e)}")
class CancelByRuleHandler(BaseHandler):
def post(self):
try:
data = json.loads(self.request.body)
stock = data.get('stock')
sn = data.get('sn','')
cancel_volume = int(data.get('volume', 0))
account = data.get('account', 'stock')
if not stock or cancel_volume <= 0:
raise HTTPError(400, "Invalid parameters: stock is required and volume must be greater than zero")
orders = safe_call(get_trade_detail_data, self.acc(), account, 'order', sn) or []
target_orders = []
for order in orders:
order_code = f"{order.m_strInstrumentID}.{order.m_strExchangeID}"
if order.m_nVolumeTotal + order.m_nVolumeTraded == cancel_volume and order_code == stock and can_cancel_order(order.m_strOrderSysID, self.acc(), account):
target_orders.append(order)
if not target_orders:
self.write(json.dumps({"status": "failed", "message": "No matching active orders found"}, ensure_ascii=False))
return
canceled_ids = []
for t_order in target_orders:
cancel(t_order.m_strOrderSysID, self.acc(), account, self.ctx())
canceled_ids.append(t_order.m_strOrderSysID)
self.write(json.dumps({
"status": "success",
"message": f"Matched {len(target_orders)} orders and submitted cancellation requests",
"canceled_sys_ids": canceled_ids
}, ensure_ascii=False))
except Exception as e:
logger.exception("Rule-based cancellation failed")
raise HTTPError(500, f"Order cancellation failed: {str(e)}")
# cancel() - Cancel orders by symbol and volume matching rules
# 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, ensure_ascii=False))
# sys: Shut down the HTTP service
class ShutdownHandler(BaseHandler):
def post(self):
logger.info("Shutdown request received; the server is stopping...")
self.write(json.dumps({"status": "success", "message": "Server is shutting down..."}, ensure_ascii=False))
self.finish()
IOLoop.current().add_callback(IOLoop.current().stop)
# get_trade_detail_data('deal') - Query trade details
class DealHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body)
account = data.get('account', 'stock')
sn = data.get('sn','')
deals = safe_call(get_trade_detail_data, self.acc(), account, 'deal', sn) or []
rets = [fixed_fields(deal) for deal in deals]
self.write(json.dumps({"deals": rets}, ensure_ascii=False))
# ============= Route registration =============
def make_app():
return Application([
# V2
(r"/api/v2/positions", HoldingHandler),
(r"/api/v2/assets", AssetsHandler),
# Legacy compatibility routes
(r"/api/holding", HoldingHandler),
(r"/api/money/total", TotalMoneyHandler),
(r"/api/money/available", AvailableMoneyHandler),
(r"/api/order/buy", BuyHandler),
(r"/api/order/sell", SellHandler),
(r"/api/order/status", OrderStatusHandler),
(r"/api/order/cancel_all", CancelAllHandler),
(r"/api/order/cancel_order", CancelByRuleHandler),
(r"/api/order/cancel_by_id", CancelByIdHandler),
(r"/api/order/deal", DealHandler),
# ContextInfo properties
(r"/api/context/period", ContextPeriodHandler),
(r"/api/context/barpos", ContextBarposHandler),
(r"/api/context/time_tick_size", ContextTimeTickSizeHandler),
(r"/api/context/stockcode", ContextStockCodeHandler),
(r"/api/context/dividend_type", ContextDividendTypeHandler),
(r"/api/context/market", ContextMarketHandler),
(r"/api/context/do_back_test", ContextDoBackTestHandler),
(r"/api/context/benchmark", ContextBenchmarkHandler),
(r"/api/context/capital", ContextCapitalHandler),
(r"/api/context/universe", ContextUniverseHandler),
# 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),
(r"/api/data/weight_in_index", WeightInIndexHandler),
(r"/api/data/contract_multiplier", ContractMultiplierHandler),
(r"/api/data/risk_free_rate", RiskFreeRateHandler),
(r"/api/data/date_location", DateLocationHandler),
(r"/api/data/history_data", HistoryDataHandler),
(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),
(r"/api/data/his_contract_list", HisContractListHandler),
(r"/api/data/option_iv", OptionIvHandler),
(r"/api/data/bsm_price", BsmPriceHandler),
(r"/api/data/bsm_iv", BsmIvHandler),
(r"/api/data/local_data", LocalDataHandler),
# Subscriptions
(r"/api/data/subscribe_quote", SubscribeQuoteHandler),
(r"/api/data/unsubscribe_quote", UnsubscribeQuoteHandler),
# Predicate functions
(r"/api/check/is_last_bar", IsLastBarHandler),
(r"/api/check/is_new_bar", IsNewBarHandler),
(r"/api/check/is_suspended_stock", IsSuspendedStockHandler),
(r"/api/check/is_sector_stock", IsSectorStockHandler),
(r"/api/check/is_typed_stock", IsTypedStockHandler),
(r"/api/check/get_industry_name_of_stock", GetIndustryNameOfStockHandler),
# Trading
(r"/api/trade/passorder", PassorderHandler),
(r"/api/trade/algo_passorder", AlgoPassorderHandler),
(r"/api/trade/smart_algo_passorder", SmartAlgoPassorderHandler),
(r"/api/trade/order_lots", OrderLotsHandler),
(r"/api/trade/order_value", OrderValueHandler),
(r"/api/trade/order_percent", OrderPercentHandler),
(r"/api/trade/order_target_value", OrderTargetValueHandler),
(r"/api/trade/order_target_percent", OrderTargetPercentHandler),
(r"/api/trade/order_shares", OrderSharesHandler),
# Futures trading
(r"/api/trade/futures/buy_open", FuturesBuyOpenHandler),
(r"/api/trade/futures/buy_close_tdayfirst", FuturesBuyCloseTdayFirstHandler),
(r"/api/trade/futures/buy_close_ydayfirst", FuturesBuyCloseYdayFirstHandler),
(r"/api/trade/futures/sell_open", FuturesSellOpenHandler),
(r"/api/trade/futures/sell_close_tdayfirst", FuturesSellCloseTdayFirstHandler),
(r"/api/trade/futures/sell_close_ydayfirst", FuturesSellCloseYdayFirstHandler),
# Task management
(r"/api/trade/cancel_task", CancelTaskHandler),
(r"/api/trade/pause_task", PauseTaskHandler),
(r"/api/trade/resume_task", ResumeTaskHandler),
(r"/api/trade/do_order", DoOrderHandler),
# Account and order queries
(r"/api/trade/trade_detail_data", TradeDetailDataHandler),
(r"/api/trade/value_by_order_id", ValueByOrderIdHandler),
(r"/api/trade/last_order_id", LastOrderIdHandler),
(r"/api/trade/can_cancel_order", CanCancelOrderHandler),
(r"/api/trade/debt_contract", DebtContractHandler),
(r"/api/trade/assure_contract", AssureContractHandler),
(r"/api/trade/enable_short_contract", EnableShortContractHandler),
(r"/api/trade/ipo_data", IpoDataHandler),
(r"/api/trade/new_purchase_limit", NewPurchaseLimitHandler),
# Reference functions
(r"/api/ext/ext_data", ExtDataHandler),
(r"/api/ext/ext_data_rank", ExtDataRankHandler),
(r"/api/ext/get_factor_value", GetFactorValueHandler),
(r"/api/ext/get_factor_rank", GetFactorRankHandler),
# System
(r"/api/sys/python_version", PythonVersionHandler),
(r"/api/sys/shutdown", ShutdownHandler),
], debug=False)
# ============= Callback registration =============
def json_serializer(obj):
if isinstance(obj, datetime.datetime):
return obj.strftime("%Y-%m-%d %H:%M:%S")
if isinstance(obj, datetime.date):
return obj.strftime("%Y-%m-%d")
if hasattr(obj, 'to_dict'):
return obj.to_dict()
attrs = getattr(obj, '__dict__', None)
if attrs:
return {key: value for key, value in attrs.items() if not key.startswith('_')}
return str(obj)
def write_json(file_key, data,order_id:str=''):
"""
Write data to a JSON file.
Args:
file_key (str): File-name template.
data: Data to write.
order_id (str): Optional order ID.
"""
now=datetime.datetime.now()
day = now.strftime("%Y%m%d")
body = {
"created_at": now,
"body":data
}
# Build the target path.
if order_id:
file_path = Path(DATA_DIR) / (file_key % (day,order_id))
else:
file_path = Path(DATA_DIR) / (file_key % day)
# Create the target directory.
file_path.parent.mkdir(parents=True, exist_ok=True)
temporary_path = file_path.with_suffix(file_path.suffix + '.tmp')
with open(temporary_path, 'w', encoding='utf-8') as f:
json.dump(body, f, ensure_ascii=False, separators=(',', ':'), default=json_serializer)
f.flush()
os.fsync(f.fileno())
os.replace(temporary_path, file_path)
# Account update callback
def account_callback(ContextInfo, accountInfo):
write_json("acount_%s.json",accountInfo)
# Order update callback
def order_callback(ContextInfo, orderInfo):
write_json("order_%s_%s.json",orderInfo,orderInfo.m_strOrderSysID)
# Trade update callback
def deal_callback(ContextInfo, dealInfo):
write_json("deal_%s_%s.json",dealInfo,dealInfo.m_strOrderSysID)
# Position update callback
def position_callback(ContextInfo, positonInfo):
write_json("position_%s.json",positonInfo)
# Order error callback
def orderError_callback(ContextInfo, passOrderInfo, msg):
print('orderError_callback')
# Log order and error details.
print (passOrderInfo.orderCode)
print (msg)
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}")