diff --git a/api/qmt_rest_new.py b/api/qmt_rest_new.py index 4f2d316..78f0727 100644 --- a/api/qmt_rest_new.py +++ b/api/qmt_rest_new.py @@ -2,7 +2,7 @@ import json import locale import os -import datetime +import sys from pathlib import Path from tornado.web import Application, RequestHandler, HTTPError from tornado.ioloop import IOLoop @@ -10,7 +10,7 @@ import logging # Configuration ACCOUNT_ID = os.environ.get('QMT_ACCOUNT_ID', '') -DATA_DIR = os.environ.get('QMT_DATA_DIR', 'D:\qmt_strategy_data') +DATA_DIR = os.environ.get('QMT_DATA_DIR', r'D:\qmt_strategy_data') TOKEN="QMTbyYanweidong" PORT = 10086 @@ -52,7 +52,7 @@ class BaseHandler(RequestHandler): def set_default_headers(self): self.set_header("Content-Type", "application/json; charset=utf-8") - def write_error(self, **kwargs): + def write_error(self,status_code, **kwargs): self.finish(self._reason) def ctx(self): @@ -61,6 +61,14 @@ class BaseHandler(RequestHandler): def acc(self): return self.application.accountID + def write_json(self, data, default=None): + self.write(json.dumps( + data, + separators=(',', ':'), + ensure_ascii=False, + default=default, + )) + # ============= 1. ContextInfo properties ============= # "/api/v2/context/info" @@ -80,7 +88,7 @@ class ContextInfoHandler(BaseHandler): "timetag":ctx.timetag, "universe": ctx.get_universe(), } - self.write(data, separators=(',', ':'), ensure_ascii=False) + self.write_json(data) # ============= 2. Data queries (ContextInfo get_*) ============= STOCK_HANDLER = { @@ -118,157 +126,54 @@ class StockGetHandler(BaseHandler): # 响应 - self.write(json.dumps({ + self.write_json({ "stock_code": query_vals, "ref": result - }, separators=(',', ':'), ensure_ascii=False, default=str)) + }, 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 [] + account_id = self.acc() + account_data = safe_call(get_trade_detail_data, account_id, 'stock', 'account') + positions = safe_call(get_trade_detail_data, account_id, 'stock', 'position') or [] + orders = safe_call(get_trade_detail_data, account_id, 'stock', 'order') or [] result = { "assets": format_assets(account_data), "positions": format_holding(positions), "orders": [fixed_fields(order) for order in orders], } - self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False)) + self.write_json(result) -# 以下未处理 +# get_trade_detail_data('position') - Query positions in the wrapped format +class HoldingHandler(BaseHandler): + def get(self): + positions = safe_call(get_trade_detail_data, self.acc(), 'stock', 'position') or [] + holding = format_holding(positions) + self.write_json({"data": holding}) -# 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}, 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(format_assets(_data)) -# 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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"}, separators=(',', ':'), 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) +class OrderHandler(BaseHandler): + def get(self): + ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') 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}, separators=(',', ':'), ensure_ascii=False, default=str)) + ret = [] + result = [fixed_fields(obj) for obj in ret] + self.write_json(result) -# 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}, separators=(',', ':'), ensure_ascii=False, default=str)) +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({"deals": rets}) # ContextInfo.get_full_tick() - Get full tick data class FullTickHandler(BaseHandler): @@ -280,305 +185,8 @@ class FullTickHandler(BaseHandler): 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)) + self.write_json(ret, default=str) -# 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), ensure_ascii=False, default=str)) - -# 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 {}}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) - - -# ============= 4. Trading functions ============= # passorder() - Submit a general trading order class PassorderHandler(BaseHandler): def post(self): @@ -586,7 +194,7 @@ class PassorderHandler(BaseHandler): data = json.loads(self.request.body) opType = int(data['opType']) orderType = int(data.get('orderType', 1101)) - stock = data['stock'] + stockCode = data['stockCode'] pr_type = int(data.get('prType', 11)) price = float(data['price']) volume = int(data['volume']) @@ -597,332 +205,22 @@ class PassorderHandler(BaseHandler): 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()) + 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 - if not order_ref: - raise HTTPError(502, reason="QMT did not return a valid order reference") - - self.write(json.dumps({ + self.write_json({ "status": "success", "opType": opType, - "stock": stock, + "stockCode": stockCode, "strategy_name": strategy_name, "local_order_id": order_id, "order_ref": str(order_ref) - }, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) + }) class CancelByIdHandler(BaseHandler): @@ -930,109 +228,46 @@ class CancelByIdHandler(BaseHandler): 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) + cancelable = safe_call(can_cancel_order, order_id, self.acc(), 'stock') if not cancelable: - self.write(json.dumps({ + self.write_json({ "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(), account_type, self.ctx()) - self.write(json.dumps({ + result = safe_call(cancel, order_id, self.acc(), 'stock', self.ctx()) + self.write_json({ "status": "success" if result is not False else "failed", "order_id": order_id, - }, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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 {}}, separators=(',', ':'), ensure_ascii=False, default=str)) +# sys: Python version information +class PythonVersionHandler(BaseHandler): + def get(self): + version_info = { + "python_version": sys.version, + "python_version_info": { + "major": sys.version_info.major, + "minor": sys.version_info.minor, + "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial, + } + } + self.write_json(version_info) -# ============= 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}, separators=(',', ':'), ensure_ascii=False)) +def format_assets(account_data): + info = account_data[0] if account_data else None + if not info: + raise HTTPError(500, "Failed to get account data") + return { + "total": round(info.m_dBalance, 2), + "available": round(info.m_dAvailable, 2), + } -# 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) - - -# ============= 9. Legacy handlers (compatibility) ============= def format_holding(positions): holding = {} for position in positions: @@ -1057,367 +292,59 @@ def format_holding(positions): } return holding - -def format_assets(account_data): - info = account_data[0] if account_data else None - if not info: - raise HTTPError(500, "Failed to get account data") - return { - "total": round(info.m_dBalance, 2), - "available": round(info.m_dAvailable, 2), - } +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', +) +MISSING = object() -# 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 = 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 post(self): - data = json.loads(self.request.body) - account = data.get('account', 'stock') - _data = safe_call(get_trade_detail_data, self.acc(), account, 'account') - self.write(json.dumps(format_assets(_data), separators=(',', ':'), ensure_ascii=False)) - - - - -# passorder(23) - Simplified buy order wrapper -class BuyHandler(BaseHandler): - def post(self): +def fixed_fields(obj, fields=TRADE_DETAIL_FIELDS): + result = {} + for field in fields: 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" - }, separators=(',', ':'), 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" - }, separators=(',', ':'), 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}, separators=(',', ':'), 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 - }, separators=(',', ':'), 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"}, separators=(',', ':'), 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 - }, separators=(',', ':'), 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, - } + value = getattr(obj, field, MISSING) + except TypeError: + continue + if value is MISSING: + 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) } - self.write(json.dumps(version_info, separators=(',', ':'), 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..."}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) + return result # ============= Route registration ============= def make_app(): return Application([ - # V2 - (r"/api/v2/portfolio", PortfolioHandler), - (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), + (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), - # Legacy compatibility routes - (r"/api/holding", HoldingHandler), - (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), + # Portfolio + (r"/api/portfolio", PortfolioHandler), + (r"/api/portfolio/positions", HoldingHandler), + (r"/api/portfolio/assets", AssetsHandler), + (r"/api/portfolio/order", OrderHandler), + (r"/api/portfolio/deal", DealHandler), - # Data queries - (r"/api/data/bar_timetag", BarTimetagHandler), - (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/main_contract", MainContractHandler), - (r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler), - (r"/api/data/trading_dates", TradingDatesHandler), - (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/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_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/cancel_by_id", CancelByIdHandler), (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(): diff --git a/api/qmt_rest_old.py b/api/qmt_rest_old.py new file mode 100644 index 0000000..49945d5 --- /dev/null +++ b/api/qmt_rest_old.py @@ -0,0 +1,1452 @@ +# -*- 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)) + + +# 以下未处理 + +# 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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, separators=(',', ':'), ensure_ascii=False, default=str)) + +# 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), ensure_ascii=False, default=str)) + +# 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 {}}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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) + }, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, 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() + 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" + }, separators=(',', ':'), 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, + }, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) + + +# ============= 9. Legacy handlers (compatibility) ============= +def format_holding(positions): + 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 + } + return holding + + +def format_assets(account_data): + info = account_data[0] if account_data else None + if not info: + raise HTTPError(500, "Failed to get account data") + return { + "total": round(info.m_dBalance, 2), + "available": round(info.m_dAvailable, 2), + } + + +# 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 = 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 post(self): + data = json.loads(self.request.body) + account = data.get('account', 'stock') + _data = safe_call(get_trade_detail_data, self.acc(), account, 'account') + self.write(json.dumps(format_assets(_data), separators=(',', ':'), 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" + }, separators=(',', ':'), 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" + }, separators=(',', ':'), 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}, separators=(',', ':'), 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 + }, separators=(',', ':'), 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"}, separators=(',', ':'), 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 + }, separators=(',', ':'), 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, separators=(',', ':'), 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..."}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) + + +# ============= Route registration ============= +def make_app(): + return Application([ + # V2 + (r"/api/v2/portfolio", PortfolioHandler), + (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), + (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), + + # Data queries + (r"/api/data/bar_timetag", BarTimetagHandler), + (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/main_contract", MainContractHandler), + (r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler), + (r"/api/data/trading_dates", TradingDatesHandler), + (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/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_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}") diff --git a/api/qmt_rest_rele.py b/api/qmt_rest_rele.py index ac2f529..26c36f0 100644 --- a/api/qmt_rest_rele.py +++ b/api/qmt_rest_rele.py @@ -52,7 +52,7 @@ class BaseHandler(RequestHandler): def set_default_headers(self): self.set_header("Content-Type", "application/json; charset=utf-8") - def write_error(self, **kwargs): + def write_error(self,status_code, **kwargs): self.finish(self._reason) def ctx(self): @@ -123,134 +123,48 @@ class StockGetHandler(BaseHandler): "ref": result }, separators=(',', ':'), ensure_ascii=False, default=str)) -# 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), ensure_ascii=False)) +# 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 [] -# 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 []}, separators=(',', ':'), ensure_ascii=False)) + 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)) -# 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 []}, separators=(',', ':'), 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}, 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)) -# 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}, 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)) -# 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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"}, separators=(',', ':'), 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) +class OrderHandler(BaseHandler): + def get(self): + ret = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') 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}, separators=(',', ':'), ensure_ascii=False, default=str)) + ret = [] + result = [fixed_fields(obj) for obj in ret] + self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False)) -# 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}, separators=(',', ':'), ensure_ascii=False, default=str)) +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): @@ -264,303 +178,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_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}, separators=(',', ':'), 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), ensure_ascii=False, default=str)) - -# 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 {}}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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 []}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) - - -# ============= 4. Trading functions ============= # passorder() - Submit a general trading order class PassorderHandler(BaseHandler): def post(self): @@ -568,7 +185,7 @@ class PassorderHandler(BaseHandler): data = json.loads(self.request.body) opType = int(data['opType']) orderType = int(data.get('orderType', 1101)) - stock = data['stock'] + stockCode = data['stockCode'] pr_type = int(data.get('prType', 11)) price = float(data['price']) volume = int(data['volume']) @@ -579,598 +196,44 @@ class PassorderHandler(BaseHandler): 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()) + 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 - 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, + "stockCode": stockCode, "strategy_name": strategy_name, "local_order_id": order_id, "order_ref": str(order_ref) }, separators=(',', ':'), 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"}, separators=(',', ':'), 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"}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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']}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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"}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, 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() - 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) + 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(), account_type, self.ctx()) + 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)) -# 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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 {}}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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}, separators=(',', ':'), 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({"data": holding}, separators=(',', ':'), 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)}, separators=(',', ':'), 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" - }, separators=(',', ':'), 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" - }, separators=(',', ':'), 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}, separators=(',', ':'), 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 - }, separators=(',', ':'), 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"}, separators=(',', ':'), 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 - }, separators=(',', ':'), 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): @@ -1187,204 +250,29 @@ class PythonVersionHandler(BaseHandler): } self.write(json.dumps(version_info, separators=(',', ':'), 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..."}, separators=(',', ':'), 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}, separators=(',', ':'), ensure_ascii=False)) - - # ============= Route registration ============= def make_app(): return Application([ - # 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), - (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), - - # Data queries - (r"/api/data/bar_timetag", BarTimetagHandler), - (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/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/data/main_contract", MainContractHandler), - (r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler), - (r"/api/data/trading_dates", TradingDatesHandler), - (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/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_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/cancel_by_id", CancelByIdHandler), (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(): diff --git a/py-client/sdk/__init__.py b/py-client/sdk/__init__.py index 6fdb18c..7ead348 100644 --- a/py-client/sdk/__init__.py +++ b/py-client/sdk/__init__.py @@ -1,15 +1,27 @@ -from .account import AccountMixin -from .client import Client as _HTTPClient -from .data import DataMixin +from .client import Client from .errors import APIError, BusinessError -from .misc import MiscMixin -from .models import * -from .trade import * -from .v2 import Client as V2Client, Portfolio - - -class Client(AccountMixin, DataMixin, TradeMixin, MiscMixin, _HTTPClient): - """big-qmt 同步 HTTP 客户端。""" - - -__all__ = ["Client", "V2Client", "Portfolio", "APIError", "BusinessError", "OP_BUY", "OP_SELL", "ORDER_TYPE_VOLUME", "PR_TYPE_LATEST", "QUICK_TRADE_NOW", "ORDER_SIDE_BY_OFFSET", "OrderItem", "PositionItem", "parse_order"] +from .models import Assets, OrderItem, Portfolio, PositionItem, Tick +from .trade import ( + OP_BUY, + OP_SELL, + ORDER_SIDE_BY_OFFSET, + ORDER_TYPE_VOLUME, + PR_TYPE_LATEST, + QUICK_TRADE_NOW, +) +__all__ = [ + "APIError", + "Assets", + "BusinessError", + "Client", + "OP_BUY", + "OP_SELL", + "ORDER_SIDE_BY_OFFSET", + "ORDER_TYPE_VOLUME", + "OrderItem", + "PR_TYPE_LATEST", + "Portfolio", + "PositionItem", + "QUICK_TRADE_NOW", + "Tick", +] diff --git a/py-client/sdk/__pycache__/__init__.cpython-311.pyc b/py-client/sdk/__pycache__/__init__.cpython-311.pyc index 2bba3f5..a33517f 100644 Binary files a/py-client/sdk/__pycache__/__init__.cpython-311.pyc and b/py-client/sdk/__pycache__/__init__.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/account.cpython-311.pyc b/py-client/sdk/__pycache__/account.cpython-311.pyc deleted file mode 100644 index b29005b..0000000 Binary files a/py-client/sdk/__pycache__/account.cpython-311.pyc and /dev/null differ diff --git a/py-client/sdk/__pycache__/client.cpython-311.pyc b/py-client/sdk/__pycache__/client.cpython-311.pyc index a3022fa..4a426e0 100644 Binary files a/py-client/sdk/__pycache__/client.cpython-311.pyc and b/py-client/sdk/__pycache__/client.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/context.cpython-311.pyc b/py-client/sdk/__pycache__/context.cpython-311.pyc new file mode 100644 index 0000000..262300d Binary files /dev/null and b/py-client/sdk/__pycache__/context.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/data.cpython-311.pyc b/py-client/sdk/__pycache__/data.cpython-311.pyc index 0068014..ccbfb86 100644 Binary files a/py-client/sdk/__pycache__/data.cpython-311.pyc and b/py-client/sdk/__pycache__/data.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/errors.cpython-311.pyc b/py-client/sdk/__pycache__/errors.cpython-311.pyc index 1df1be8..84b22ce 100644 Binary files a/py-client/sdk/__pycache__/errors.cpython-311.pyc and b/py-client/sdk/__pycache__/errors.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/get.cpython-311.pyc b/py-client/sdk/__pycache__/get.cpython-311.pyc new file mode 100644 index 0000000..b028d5e Binary files /dev/null and b/py-client/sdk/__pycache__/get.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/misc.cpython-311.pyc b/py-client/sdk/__pycache__/misc.cpython-311.pyc deleted file mode 100644 index 32b8591..0000000 Binary files a/py-client/sdk/__pycache__/misc.cpython-311.pyc and /dev/null differ diff --git a/py-client/sdk/__pycache__/models.cpython-311.pyc b/py-client/sdk/__pycache__/models.cpython-311.pyc index 805c3d1..e17059c 100644 Binary files a/py-client/sdk/__pycache__/models.cpython-311.pyc and b/py-client/sdk/__pycache__/models.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/portfolio.cpython-311.pyc b/py-client/sdk/__pycache__/portfolio.cpython-311.pyc new file mode 100644 index 0000000..664573f Binary files /dev/null and b/py-client/sdk/__pycache__/portfolio.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/sys.cpython-311.pyc b/py-client/sdk/__pycache__/sys.cpython-311.pyc new file mode 100644 index 0000000..f139a2b Binary files /dev/null and b/py-client/sdk/__pycache__/sys.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/trade.cpython-311.pyc b/py-client/sdk/__pycache__/trade.cpython-311.pyc index b990680..d8d8ad7 100644 Binary files a/py-client/sdk/__pycache__/trade.cpython-311.pyc and b/py-client/sdk/__pycache__/trade.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/v2.cpython-311.pyc b/py-client/sdk/__pycache__/v2.cpython-311.pyc deleted file mode 100644 index 59e8118..0000000 Binary files a/py-client/sdk/__pycache__/v2.cpython-311.pyc and /dev/null differ diff --git a/py-client/sdk/account.py b/py-client/sdk/account.py deleted file mode 100644 index 046fee0..0000000 --- a/py-client/sdk/account.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Any - -from .models import Assets, PositionItem - - -class AccountMixin: - account_type: str - - def _positions(self, path: str) -> tuple[list[str], list[PositionItem]]: - payload = self._post_json(path, {"account": self.account_type}) or {} - raw = payload.get("data", payload) if isinstance(payload, dict) else payload - if isinstance(raw, list): - positions = [PositionItem.from_trade_detail(item) for item in raw] - return [item.stock_code for item in positions], positions - return list(raw), [PositionItem.from_dict(value, code) for code, value in raw.items()] - - def positions(self): return self._positions("/api/v2/positions") - def holding(self): return self._positions("/api/holding") - - def assets(self) -> Assets: - payload = self._post_json("/api/v2/assets", {"account": self.account_type}) or {} - data = payload.get("data", payload) if isinstance(payload, dict) else {} - return Assets.from_dict(data) - - def total_money(self) -> float: return float(self._post_json("/api/money/total", {"account": self.account_type}).get("total_money", 0)) - def available_money(self) -> float: return float(self._post_json("/api/money/available", {"account": self.account_type}).get("available_money", 0)) - def buy(self, stock: str, price: float, volume: int, pr_type: int = 0): return self._order("/api/order/buy", stock, price, volume, pr_type) - def sell(self, stock: str, price: float, volume: int, pr_type: int = 0): return self._order("/api/order/sell", stock, price, volume, pr_type) - - def _order(self, path, stock, price, volume, pr_type): - body = {"stock": stock, "price": price, "volume": volume} - if pr_type: body["prType"] = pr_type - return self._post_json(path, body) - - def order_status_list(self): return self._post_json("/api/order/status", {"account": self.account_type}).get("orders", []) - def cancel_all(self): return self._post_json("/api/order/cancel_all", {"account": self.account_type}) - def cancel_by_rule(self, stock: str, volume: int): return self._post_json("/api/order/cancel_order", {"stock": stock, "volume": volume, "account": self.account_type}) - def deals(self): return self._post_json("/api/order/deal", {"account": self.account_type}).get("deals", []) diff --git a/py-client/sdk/client.py b/py-client/sdk/client.py index e8a65e2..04c2bcb 100644 --- a/py-client/sdk/client.py +++ b/py-client/sdk/client.py @@ -6,14 +6,16 @@ from typing import Any import httpx -from .errors import APIError, BusinessError +from .context import ContextMixin +from .data import DataMixin +from .errors import APIError +from .get import GetMixin +from .portfolio import PortfolioMixin +from .sys import SysMixin +from .trade import TradeMixin -def csv_join(items: list[str]) -> str: - return ",".join(item.strip() for item in items if item.strip()) - - -class Client: +class HTTPClient: """复用连接池的同步 QMT HTTP 客户端。""" def __init__(self, base_url: str, token: str, timeout: float = 15.0) -> None: @@ -31,13 +33,13 @@ class Client: def close(self) -> None: self.http.close() - def __enter__(self) -> "Client": + def __enter__(self) -> "HTTPClient": return self def __exit__(self, *_args: object) -> None: self.close() - def set_account_type(self, account_type: str) -> "Client": + def set_account_type(self, account_type: str) -> "HTTPClient": if account_type.strip(): self.account_type = account_type return self @@ -91,28 +93,19 @@ class Client: f"invalid JSON from {path}: {content[:512]!r}" ) from exc - def _get_field(self, path: str, key: str) -> Any: - return self._get_json(path).get(key) - - def _post_field(self, path: str, body: Any, key: str) -> Any: - result = self._post_json(path, body) - if isinstance(result, dict) and result.get("error"): - raise BusinessError(result["error"]) - return result.get(key, result) if key and isinstance(result, dict) else result +class Client( + ContextMixin, + GetMixin, + PortfolioMixin, + DataMixin, + TradeMixin, + SysMixin, + HTTPClient, +): + """Client for the API exposed by ``qmt_rest_new.py``.""" def _is_idempotent(method: str, path: str) -> bool: if method == "GET": return True - prefixes = ( - "/api/v2/", - "/api/holding", - "/api/money/", - "/api/context/", - "/api/check/", - "/api/data/", - "/api/trade/trade_detail_data", - "/api/order/deal", - ) - unsafe = ("subscribe", "unsubscribe") - return path.startswith(prefixes) and not any(word in path for word in unsafe) + return path == "/api/data/full_tick" diff --git a/py-client/sdk/context.py b/py-client/sdk/context.py new file mode 100644 index 0000000..95942a8 --- /dev/null +++ b/py-client/sdk/context.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from typing import Any + + +class ContextMixin: + def context_info(self) -> dict[str, Any]: + return self._get_json("/api/context/info") or {} diff --git a/py-client/sdk/data.py b/py-client/sdk/data.py index bcd7164..131269d 100644 --- a/py-client/sdk/data.py +++ b/py-client/sdk/data.py @@ -1,83 +1,9 @@ -from dataclasses import asdict -from typing import Any +from __future__ import annotations -from .client import csv_join -from .models import * +from .models import Tick class DataMixin: - def _one(self, endpoint, arg, value, key): return self._post_field(f"/api/data/{endpoint}", {arg: value}, key) - - def stock_name(self, code): return self._one("stock_name", "stockcode", code, "name") - def open_date(self, code): return self._one("open_date", "stockcode", code, "open_date") - def last_volume(self, code): return self._one("last_volume", "stockcode", code, "last_volume") - def bar_timetag(self, index): return self._one("bar_timetag", "index", index, "timetag") - def tick_timetag(self): return self._get_field("/api/data/tick_timetag", "timetag") - def sector(self, sector, realtime): return self._post_json("/api/data/sector", {"sector": sector, "realtime": realtime}).get("stocks", []) - def industry(self, industry): return self._post_json("/api/data/industry", {"industry": industry}).get("stocks", []) - def stock_list_in_sector(self, name): return self._post_json("/api/data/stock_list_in_sector", {"sectorname": name}).get("stocks", []) - def weight_in_index(self, indexcode, stockcode): return self._post_field("/api/data/weight_in_index", locals_body(indexcode=indexcode, stockcode=stockcode), "weight") - def contract_multiplier(self, code): return self._one("contract_multiplier", "contractcode", code, "multiplier") - def risk_free_rate(self, index): return self._one("risk_free_rate", "index", index, "risk_free_rate") - def date_location(self, date): return self._one("date_location", "strdate", date, "location") - - def history_data(self, req: HistoryDataRequest): - return self._post_field("/api/data/history_data", {"len": req.length or 10, "period": req.period, "field": req.field, "dividend_type": req.dividend_type, "skip_paused": str(req.skip_paused).lower()}, "data") - def _market_body(self, req): return {"fields": csv_join(req.fields), "stock_code": csv_join(req.stocks), "start_time": req.start_time, "end_time": req.end_time, "period": req.period, "dividend_type": req.dividend_type, "count": req.count} - def market_data(self, req): return self._post_field("/api/data/market_data", self._market_body(req), "data") - def market_data_ex(self, req): return self._post_field("/api/data/market_data_ex", self._market_body(req), "data") - - def full_tick(self, stocks): - raw = self._post_json("/api/data/full_tick", {"stocks": stocks}) or {} - def number(data, *names): - for name in names: - try: return float(data[name]) - except (KeyError, TypeError, ValueError): pass - return 0.0 - return {code: Tick(number(value, "lastPrice", "last_price", "LastPrice"), number(value, "lastClose", "last_close", "LastClose"), value if isinstance(value, dict) else {}) for code, value in raw.items()} - - def divid_factors(self, code): return self._one("divid_factors", "stockcode", code, "factors") - def main_contract(self, code): return self._one("main_contract", "codemarket", code, "main_contract") - def timetag_to_datetime(self, timetag, format=""): - body = {"timetag": timetag} - if format: body["format"] = format - return self._post_field("/api/data/timetag_to_datetime", body, "datetime") - def total_share(self, code): return self._one("total_share", "stockcode", code, "total_share") - def trading_dates(self, stockcode, start_date, end_date, period, count=0): - body = locals_body(stockcode=stockcode, start_date=start_date, end_date=end_date, period=period) - if count: body["count"] = count - return self._post_json("/api/data/trading_dates", body).get("dates", []) - def svol(self, code): return self._one("svol", "stockcode", code, "svol") - def bvol(self, code): return self._one("bvol", "stockcode", code, "bvol") - def longhubang(self, stocks, start, end): return self._post_field("/api/data/longhubang", {"stock_list": csv_join(stocks), "startTime": start, "endTime": end}, "data") - def top10_share_holder(self, stocks, name, start, end): return self._post_field("/api/data/top10_share_holder", {"stock_list": csv_join(stocks), "data_name": name, "start_time": start, "end_time": end}, "data") - def option_detail(self, code): return self._one("option_detail", "optioncode", code, "detail") - def turnover_rate(self, stocks, start, end): return self._post_field("/api/data/turnover_rate", {"stock_list": csv_join(stocks), "startTime": start, "endTime": end}, "data") - def etf_info(self, code): return self._one("etf_info", "stockcode", code, "info") - def etf_iopv(self, code): return self._one("etf_iopv", "stockcode", code, "iopv") - def instrument_detail(self, code): return self._one("instrumentdetail", "stockcode", code, "detail") - def contract_expire_date(self, code): return self._one("contract_expire_date", "codemarket", code, "expire_date") - def option_undl_data(self, code): return self._one("option_undl_data", "undl_code_ref", code, "data") - - def financial_data(self, req): - return self._post_field("/api/data/financial_data", {"tabname": req.tabname, "colname": req.colname, "market": req.market, "code": req.code, "report_type": req.report_type, "barpos": req.barpos, "fieldList": csv_join(req.field_list), "stockList": csv_join(req.stock_list), "startDate": req.start_date, "endDate": req.end_date}, "data") - def factor_data(self, req): return self._post_field("/api/data/factor_data", {"fieldList": csv_join(req.field_list), "stockList": csv_join(req.stock_list), "stockCode": req.stock_code, "startDate": req.start_date, "endDate": req.end_date}, "data") - def his_st_data(self, code): return self._one("his_st_data", "stockCode", code, "data") - def his_index_data(self, index): return self._one("his_index_data", "index", index, "data") - def all_subscription(self): return self._get_field("/api/data/all_subscription", "subscriptions") - def option_list(self, code, dedate, opttype, available): return self._post_field("/api/data/option_list", {"undl_code": code, "dedate": dedate, "opttype": opttype, "isavailable": str(available).lower()}, "option_list") - def his_contract_list(self, market): return self._one("his_contract_list", "market", market, "contracts") - def option_iv(self, code): return self._one("option_iv", "optioncode", code, "iv") - def bsm_price(self, req): - prices = ",".join(str(v) for v in req.object_prices) if isinstance(req.object_prices, list) else req.object_prices - return self._post_field("/api/data/bsm_price", {"optionType": req.option_type, "objectPrices": prices, "strikePrice": req.strike_price, "riskFree": req.risk_free, "sigma": req.sigma, "days": req.days, "dividend": req.dividend}, "price") - def bsm_iv(self, req): return self._post_field("/api/data/bsm_iv", camel_request(req), "iv") - def local_data(self, req): return self._post_field("/api/data/local_data", {"stock_code": req.stock_code, "start_time": req.start_time, "end_time": req.end_time, "period": req.period, "divid_type": req.divid_type, "count": req.count}, "data") - def subscribe_quote(self, code, period, dividend_type): return self._post_json("/api/data/subscribe_quote", {"stock_code": code, "period": period, "dividend_type": dividend_type}) - def unsubscribe_quote(self, sub_id): return self._post_json("/api/data/unsubscribe_quote", {"sub_id": sub_id}) - - -def locals_body(**kwargs): return kwargs -def camel_request(req): - data = asdict(req) - return {"optionType": data["option_type"], "objectPrices": data["object_prices"], "strikePrice": data["strike_price"], "optionPrice": data["option_price"], "riskFree": data["risk_free"], "days": data["days"], "dividend": data["dividend"]} + def full_tick(self, stocks: list[str]) -> dict[str, Tick]: + data = self._post_json("/api/data/full_tick", {"stocks": stocks}) or {} + return {code: Tick.from_dict(value) for code, value in data.items()} diff --git a/py-client/sdk/get.py b/py-client/sdk/get.py new file mode 100644 index 0000000..6a8a27a --- /dev/null +++ b/py-client/sdk/get.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Any +from urllib.parse import urlencode + + +class GetMixin: + def stock_name(self, stock_code: str) -> Any: + return self._stock_ref("stock_name", stock_code) + + def open_date(self, stock_code: str) -> Any: + return self._stock_ref("open_date", stock_code) + + def last_volume(self, stock_code: str) -> Any: + return self._stock_ref("last_volume", stock_code) + + def total_share(self, stock_code: str) -> Any: + return self._stock_ref("total_share", stock_code) + + def svol(self, stock_code: str) -> Any: + return self._stock_ref("svol", stock_code) + + def bvol(self, stock_code: str) -> Any: + return self._stock_ref("bvol", stock_code) + + def divid_factors(self, stock_code: str) -> Any: + return self._stock_ref("divid_factors", stock_code) + + def etf_info(self, stock_code: str) -> Any: + return self._stock_ref("etf_info", stock_code) + + def etf_iopv(self, stock_code: str) -> Any: + return self._stock_ref("etf_iopv", stock_code) + + def instrument_detail(self, stock_code: str) -> Any: + return self._stock_ref("instrumentdetail", stock_code) + + def his_st_data(self, stock_code: str) -> Any: + return self._stock_ref("his_st_data", stock_code) + + def _stock_ref(self, endpoint: str, stock_code: str) -> Any: + query = urlencode({"stock_code": stock_code}) + data = self._get_json(f"/api/get/{endpoint}?{query}") or {} + return data.get("ref") diff --git a/py-client/sdk/misc.py b/py-client/sdk/misc.py deleted file mode 100644 index 4ef8f6f..0000000 --- a/py-client/sdk/misc.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Any - - -class MiscMixin: - def context_period(self): return self._get_field("/api/context/period", "period") - def context_barpos(self): return self._get_field("/api/context/barpos", "barpos") - def context_time_tick_size(self): return self._get_field("/api/context/time_tick_size", "time_tick_size") - def context_stockcode(self): return self._get_field("/api/context/stockcode", "stockcode") - def context_dividend_type(self): return self._get_field("/api/context/dividend_type", "dividend_type") - def context_market(self): return self._get_field("/api/context/market", "market") - def context_do_back_test(self): return self._get_field("/api/context/do_back_test", "do_back_test") - def context_benchmark(self): return self._get_field("/api/context/benchmark", "benchmark") - def context_capital(self): return self._get_field("/api/context/capital", "capital") - def context_universe(self): - value = self._get_field("/api/context/universe", "universe") - if value is None: return [] - return [str(v) for v in value if str(v)] if isinstance(value, list) else [str(value)] - - def is_last_bar(self): return self._get_field("/api/check/is_last_bar", "is_last_bar") - def is_new_bar(self): return self._get_field("/api/check/is_new_bar", "is_new_bar") - def is_suspended_stock(self, stockcode): return self._post_field("/api/check/is_suspended_stock", {"stockcode": stockcode}, "is_suspended") - def is_sector_stock(self, sectorname, market, stockcode): return self._post_field("/api/check/is_sector_stock", {"sectorname": sectorname, "market": market, "stockcode": stockcode}, "is_in_sector") - def is_typed_stock(self, stocktypenum, market, stockcode): return self._post_field("/api/check/is_typed_stock", {"stocktypenum": stocktypenum, "market": market, "stockcode": stockcode}, "result") - def industry_name_of_stock(self, industry_type, stockcode): return self._post_field("/api/check/get_industry_name_of_stock", {"industryType": industry_type, "stockcode": stockcode}, "industry_name") - - def ext_data(self, name, stockcode, deviation): return self._post_field("/api/ext/ext_data", {"extdataname": name, "stockcode": stockcode, "deviation": deviation}, "value") - def ext_data_rank(self, name, stockcode, deviation): return self._post_field("/api/ext/ext_data_rank", {"extdataname": name, "stockcode": stockcode, "deviation": deviation}, "rank") - def get_factor_value(self, name, stockcode, deviation): return self._post_field("/api/ext/get_factor_value", {"factorname": name, "stockcode": stockcode, "deviation": deviation}, "value") - def get_factor_rank(self, name, stockcode, deviation): return self._post_field("/api/ext/get_factor_rank", {"factorname": name, "stockcode": stockcode, "deviation": deviation}, "rank") - def python_version(self): return self._get_json("/api/sys/python_version") - def shutdown(self): return self._post_json("/api/sys/shutdown", {}) diff --git a/py-client/sdk/models.py b/py-client/sdk/models.py index 3de94e4..5975405 100644 --- a/py-client/sdk/models.py +++ b/py-client/sdk/models.py @@ -133,6 +133,13 @@ class Assets: ) +@dataclass(slots=True) +class Portfolio: + assets: Assets + positions: dict[str, PositionItem] + orders: list[OrderItem] + + def _trade_datetime(data: dict[str, Any]) -> datetime | None: date = str(data.get("m_strInsertDate") or "") clock = str(data.get("m_strInsertTime") or "").replace(":", "").zfill(6) @@ -148,6 +155,16 @@ class Tick: last_close: float = 0.0 raw: dict[str, Any] = field(default_factory=dict) + @classmethod + def from_dict(cls, data: Any) -> "Tick": + if not isinstance(data, dict): + return cls() + return cls( + last_price=_number(data.get("lastPrice", data.get("last_price", data.get("LastPrice")))), + last_close=_number(data.get("lastClose", data.get("last_close", data.get("LastClose")))), + raw=data, + ) + @dataclass(slots=True) class HistoryDataRequest: diff --git a/py-client/sdk/portfolio.py b/py-client/sdk/portfolio.py new file mode 100644 index 0000000..e5b90e5 --- /dev/null +++ b/py-client/sdk/portfolio.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import Any + +from .models import Assets, OrderItem, Portfolio, PositionItem + + +class PortfolioMixin: + def portfolio(self) -> Portfolio: + data = self._get_json("/api/portfolio") or {} + positions = { + code: PositionItem.from_dict(value, code) + for code, value in data.get("positions", {}).items() + } + return Portfolio( + assets=Assets.from_dict(data.get("assets", {})), + positions=positions, + orders=[OrderItem.from_trade_detail(row) for row in data.get("orders", [])], + ) + + def positions(self) -> tuple[list[str], list[PositionItem]]: + data = self._get_json("/api/portfolio/positions") or {} + positions = [ + PositionItem.from_dict(value, code) + for code, value in data.get("data", {}).items() + ] + return [item.stock_code for item in positions], positions + + def assets(self) -> Assets: + return Assets.from_dict(self._get_json("/api/portfolio/assets") or {}) + + def orders(self) -> list[OrderItem]: + data = self._get_json("/api/portfolio/order") or [] + return [OrderItem.from_trade_detail(row) for row in data] + + def deals(self) -> list[dict[str, Any]]: + data = self._get_json("/api/portfolio/deal") or {} + return data.get("deals", []) + + def trade_detail_data(self, datatype: str) -> Any: + datatype = str(datatype).strip().lower() + handlers = { + "account": self.assets, + "position": lambda: self.positions()[1], + "order": self.orders, + "deal": self.deals, + } + handler = handlers.get(datatype) + if handler is None: + raise ValueError(f"unsupported trade detail datatype: {datatype}") + result = handler() + return [result] if datatype == "account" else result diff --git a/py-client/sdk/sys.py b/py-client/sdk/sys.py new file mode 100644 index 0000000..114efdf --- /dev/null +++ b/py-client/sdk/sys.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from typing import Any + + +class SysMixin: + def python_version(self) -> dict[str, Any]: + return self._get_json("/api/sys/python_version") or {} diff --git a/py-client/sdk/trade.py b/py-client/sdk/trade.py index 310ba6b..9e3c3dd 100644 --- a/py-client/sdk/trade.py +++ b/py-client/sdk/trade.py @@ -1,82 +1,62 @@ -from .models import * +from __future__ import annotations + from typing import Any + OP_BUY = 23 OP_SELL = 24 -ORDER_TYPE_VOLUME, PR_TYPE_LATEST, QUICK_TRADE_NOW = 1101, 5, 2 +ORDER_TYPE_VOLUME = 1101 +PR_TYPE_LATEST = 5 +QUICK_TRADE_NOW = 2 ORDER_SIDE_BY_OFFSET = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"} class TradeMixin: - account_type: str + def passorder( + self, + op_type: int, + stock_code: str, + volume: int, + order_type: int = ORDER_TYPE_VOLUME, + pr_type: int = PR_TYPE_LATEST, + price: float = -1, + quick_trade: int = QUICK_TRADE_NOW, + strategy_name: str = "", + order_id: str = "", + ) -> dict[str, Any]: + return self._post_json( + "/api/trade/passorder", + { + "opType": op_type, + "orderType": order_type, + "stockCode": stock_code, + "prType": pr_type, + "price": price, + "volume": volume, + "quickTrade": quick_trade, + "strategyName": strategy_name, + "orderId": order_id, + }, + ) - def passorder(self, op_type, stock, volume, order_type=0, pr_type=0, price=0.0, quick_trade=0, strategy_name=""): - body = {"opType": op_type, "stock": stock, "price": price, "volume": volume} - for key, value in (("orderType", order_type), ("prType", pr_type), ("quickTrade", quick_trade), ("strategyName", strategy_name)): - if value: body[key] = value - return self._post_json("/api/trade/passorder", body) + def passorder_latest(self, side: int, stock_code: str, volume: int) -> dict[str, Any]: + return self.passorder(side, stock_code, volume) - def passorder_latest(self, side, stock, volume): return self.passorder_latest_tagged(side, stock, volume, "", "") - def passorder_latest_tagged(self, side, stock_code, volume, strategy_name, order_id): - body = { - "opType": side, - "orderType": ORDER_TYPE_VOLUME, - "stockCode": stock_code, - "prType": PR_TYPE_LATEST, - "price": -1, - "volume": volume, - "quickTrade": QUICK_TRADE_NOW, - "strategyName": strategy_name, - "orderId": order_id, - } - return self._post_json("/api/trade/passorder", body) + def passorder_latest_tagged( + self, + side: int, + stock_code: str, + volume: int, + strategy_name: str, + order_id: str, + ) -> dict[str, Any]: + return self.passorder( + side, + stock_code, + volume, + strategy_name=strategy_name, + order_id=order_id, + ) - def algo_passorder(self, **kwargs): return self._post_json("/api/trade/algo_passorder", kwargs) - def smart_algo_passorder(self, **kwargs): return self._post_json("/api/trade/smart_algo_passorder", kwargs) - - def _style_order(self, path, stock, value_key, value, style, price): - return self._post_json(path, {"stock": stock, value_key: value, "style": style, "price": price}) - def order_lots(self, stock, lots, style, price): return self._style_order("/api/trade/order_lots", stock, "lots", lots, style, price) - def order_value(self, stock, value, style, price): return self._style_order("/api/trade/order_value", stock, "value", value, style, price) - def order_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_percent", stock, "percent", percent, style, price) - def order_target_value(self, stock, value, style, price): return self._style_order("/api/trade/order_target_value", stock, "tar_value", value, style, price) - def order_target_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_target_percent", stock, "tar_percent", percent, style, price) - def order_shares(self, stock, shares, style, price): return self._style_order("/api/trade/order_shares", stock, "shares", shares, style, price) - - def _future(self, action, stock, amount, style, price): return self._style_order(f"/api/trade/futures/{action}", stock, "amount", amount, style, price) - def futures_buy_open(self, *args): return self._future("buy_open", *args) - def futures_buy_close_tdayfirst(self, *args): return self._future("buy_close_tdayfirst", *args) - def futures_buy_close_ydayfirst(self, *args): return self._future("buy_close_ydayfirst", *args) - def futures_sell_open(self, *args): return self._future("sell_open", *args) - def futures_sell_close_tdayfirst(self, *args): return self._future("sell_close_tdayfirst", *args) - def futures_sell_close_ydayfirst(self, *args): return self._future("sell_close_ydayfirst", *args) - - def _task(self, action, task_id): return self._post_json(f"/api/trade/{action}_task", {"taskId": task_id, "accountType": self.account_type}) - def cancel_task(self, task_id): return self._task("cancel", task_id) - def pause_task(self, task_id): return self._task("pause", task_id) - def resume_task(self, task_id): return self._task("resume", task_id) - def do_order(self): return self._post_json("/api/trade/do_order") - def trade_detail_data(self, datatype): - datatype = str(datatype).strip().lower() - data = self._post_json( - "/api/trade/trade_detail_data", - {"account": self.account_type, "datatype": datatype}, - ).get("data", []) - rows = data if isinstance(data, list) else [data] if isinstance(data, dict) else [] - if datatype == "order": - return [OrderItem.from_trade_detail(row) for row in rows] - if datatype == "position": - return [PositionItem.from_trade_detail(row) for row in rows] - if datatype == "account": - return [Assets.from_dict(row) for row in rows] - return data - def value_by_order_id(self, order_id, datatype): return self._post_json("/api/trade/value_by_order_id", {"orderId": order_id, "accountType": self.account_type, "datatype": datatype}).get("data") - def last_order_id(self, datatype): return self._post_json("/api/trade/last_order_id", {"account": self.account_type, "datatype": datatype}).get("last_order_id") - def can_cancel_order(self, order_id): return self._post_json("/api/trade/can_cancel_order", {"orderId": order_id, "accountType": self.account_type}).get("can_cancel") - def cancel_by_id(self, order_id): return self._post_json("/api/order/cancel_by_id", {"order_id": order_id, "account_type": self.account_type}) - def debt_contract(self): return self._contract("debt_contract") - def assure_contract(self): return self._contract("assure_contract") - def enable_short_contract(self): return self._contract("enable_short_contract") - def _contract(self, name): return self._post_json(f"/api/trade/{name}").get("data", []) - def ipo_data(self, typ): return self._post_field("/api/trade/ipo_data", {"type": typ}, "data") - def new_purchase_limit(self): return self._post_field("/api/trade/new_purchase_limit", None, "data") + def cancel_by_id(self, order_id: str) -> dict[str, Any]: + return self._post_json("/api/trade/cancel_by_id", {"order_id": order_id}) diff --git a/py-client/sdk/v2.py b/py-client/sdk/v2.py deleted file mode 100644 index 20d9160..0000000 --- a/py-client/sdk/v2.py +++ /dev/null @@ -1,86 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any -from urllib.parse import urlencode - -from .client import Client as HTTPClient -from .models import Assets, OrderItem, PositionItem - - -@dataclass(slots=True) -class Portfolio: - assets: Assets - positions: list[PositionItem] - orders: list[OrderItem] - - -class Client_V2(HTTPClient): - """QMT ``/api/v2`` synchronous client.""" - - def portfolio(self) -> Portfolio: - data = self._get_json("/api/v2/portfolio", ) or {} - positions = data.get("positions", {}) - orders = data.get("orders", {}) - return Portfolio( - assets=Assets.from_dict(data.get("assets", {})), - positions=[PositionItem.from_trade_detail(row) for row in positions], - orders=[OrderItem.from_trade_detail(value) for value in orders], - ) - - def positions(self) -> list[PositionItem]: - data = self._post("/api/v2/positions", {"account": self.account_type}) or {} - return [ - PositionItem.from_dict(value, code) - for code, value in data.get("data", {}).items() - ] - - def assets(self) -> dict[str, Any]: - return self._post("/api/v2/assets", {"account": self.account_type}) or {} - - def context_info(self) -> dict[str, Any]: - return self._get("/api/v2/context/info") or {} - - def stock_name(self, stock_code: str) -> Any: - return self._get_ref("stock_name", stock_code) - - def open_date(self, stock_code: str) -> Any: - return self._get_ref("open_date", stock_code) - - def last_volume(self, stock_code: str) -> Any: - return self._get_ref("last_volume", stock_code) - - def total_share(self, stock_code: str) -> Any: - return self._get_ref("total_share", stock_code) - - def svol(self, stock_code: str) -> Any: - return self._get_ref("svol", stock_code) - - def bvol(self, stock_code: str) -> Any: - return self._get_ref("bvol", stock_code) - - def divid_factors(self, stock_code: str) -> Any: - return self._get_ref("divid_factors", stock_code) - - def etf_info(self, stock_code: str) -> Any: - return self._get_ref("etf_info", stock_code) - - def etf_iopv(self, stock_code: str) -> Any: - return self._get_ref("etf_iopv", stock_code) - - def instrument_detail(self, stock_code: str) -> Any: - return self._get_ref("instrumentdetail", stock_code) - - def his_st_data(self, stock_code: str) -> Any: - return self._get_ref("his_st_data", stock_code) - - def _get_ref(self, endpoint: str, stock_code: str) -> Any: - query = urlencode({"stock_code": stock_code}) - payload = self._get(f"/api/v2/get/{endpoint}?{query}") or {} - return payload.get("ref") - - -Client = Client_V2 - - -__all__ = ["Client", "Client_V2", "Portfolio"] diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc index ff2af62..8eac14b 100644 Binary files a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc index 4eb4679..98ed56f 100644 Binary files a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/order.cpython-311.pyc b/py-client/strategy/trend/__pycache__/order.cpython-311.pyc index fa69a5b..221c357 100644 Binary files a/py-client/strategy/trend/__pycache__/order.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/order.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc b/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc index bb63591..1399d24 100644 Binary files a/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc differ diff --git a/py-client/strategy/trend/boot.py b/py-client/strategy/trend/boot.py index 7f276b9..86111a5 100644 --- a/py-client/strategy/trend/boot.py +++ b/py-client/strategy/trend/boot.py @@ -51,10 +51,11 @@ def StartTrend() -> None: config.global_config.qmt_token, config.HTTP_TIMEOUT, ) - assets = client.assets() - _, positions = client.positions() + portfolio = client.portfolio() + assets = portfolio.assets + positions = list(portfolio.positions.values()) order_book = OrderBook() - order_book.refresh(client) + order_book.refresh(client, portfolio.orders) storeState = State.for_strategy( config.global_config.qmt_data_dir, @@ -114,20 +115,18 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: started_at = time.monotonic() - # 1. 刷新订单数据,清理过期订单。 + # 1. 一次获取资产、持仓和订单,并清理过期订单。 try: - run.orders.refresh(run.client) + portfolio = run.client.portfolio() + assets = portfolio.assets + position_codes = list(portfolio.positions) + positions = list(portfolio.positions.values()) + run.orders.refresh(run.client, portfolio.orders) except Exception: - log.exception("[Order] 刷新订单失败") + log.exception("[Portfolio] 刷新账户快照失败") return - # 2. 验证可用资金;低于资金安全线时禁止开新仓。 - try: - assets = run.client.assets() - except Exception: - log.exception("[资金] 获取资产失败") - return allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio if not allow_open_by_cash: log.info("[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total) @@ -135,14 +134,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: # 3. 获取大盘状态,只有大盘信号允许时才执行开仓。 market_ok = market_allow_open() - # 4. 获取当前持仓及持仓证券代码。 - try: - position_codes, positions = run.client.positions() - except Exception: - log.exception("[Position] 获取持仓失败") - return - - # 5. 验证有效开仓信号:排除已有持仓和未决订单。 + # 4. 验证有效开仓信号:排除已有持仓和未决订单。 allow_open: list[SignalItem] = [] allow_codes: list[str] = [] for signal in signals: @@ -153,7 +145,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: if allow_open and not market_ok: log.info("[开仓] 禁止开仓:大盘信号不允许,候选=%d", len(allow_open)) - # 6. 获取持仓和待开仓证券的实时行情 tick。 + # 5. 获取持仓和待开仓证券的实时行情 tick。 all_codes = list(dict.fromkeys(position_codes + allow_codes)) try: ticks = run.client.full_tick(all_codes) @@ -161,7 +153,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: log.exception("[行情] 获取行情失败,代码数量=%d", len(all_codes)) return - # 7. 更新状态机 + # 6. 更新状态机 try: run.state.reconcile(positions, run.orders.data) except Exception: @@ -171,7 +163,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: log.info("[RunOnce] 本轮就绪,持仓=%d,候选=%d,大盘允许=%s,资金允许=%s", len(positions), len(allow_open), market_ok, allow_open_by_cash) # 启动线程,开始计算 - # 9. 持仓计算。 + # 7. 持仓计算。 futures: list[tuple[str, Future]] = [ ( "持仓计算", @@ -186,11 +178,11 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None: ) ] - # 10. 开仓计算:必须同时存在有效信号且大盘允许开仓。 + # 8. 开仓计算:必须同时存在有效信号且大盘允许开仓。 if allow_open and market_ok and allow_open_by_cash: futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open))) - # 11. 开始执行 + # 9. 开始执行 for name, future in futures: _wait_worker(name, future) log.info("[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000)) diff --git a/py-client/strategy/trend/open.py b/py-client/strategy/trend/open.py index d5bf07a..bfbdddb 100644 --- a/py-client/strategy/trend/open.py +++ b/py-client/strategy/trend/open.py @@ -71,7 +71,6 @@ def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None: run.client, OP_BUY, code, - -1, volume, order_id, signal_key, diff --git a/py-client/strategy/trend/order.py b/py-client/strategy/trend/order.py index 51785f4..093e7e3 100644 --- a/py-client/strategy/trend/order.py +++ b/py-client/strategy/trend/order.py @@ -25,7 +25,6 @@ class PlaceOrderRequest: client: Any op: int code: str - price: float volume: int order_id: str strategy_name: str @@ -52,9 +51,8 @@ class OrderBook: key = f"{side}-{code}" return key in self.lock - def refresh(self, client: Client) -> None: - """从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托。""" - orders = client.trade_detail_data("order") + def refresh(self, client: Client, orders: list[OrderItem]) -> None: + """用账户快照刷新委托,并撤销超时的活动委托。""" current = datetime.now() now_timestamp = current.timestamp() data: list[OrderItem] = [] diff --git a/py-client/strategy/trend/positions.py b/py-client/strategy/trend/positions.py index 6bee0a3..d187e6a 100644 --- a/py-client/strategy/trend/positions.py +++ b/py-client/strategy/trend/positions.py @@ -47,7 +47,7 @@ def manage_positions( code = position.stock_code tick = ticks.get(code) if code in runtime.account_cfg.excluded_codes: - log.info("[Position] %s %s 止盈=跳过,补仓=跳过,原因=已配置为排除股票", code, position.stock_name) + log.info("[Position] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=已配置为排除股票", code, position.stock_name) continue if ( not code @@ -56,7 +56,7 @@ def manage_positions( or tick is None or tick.last_price <= 0 ): - log.warning("[Position] %s %s 止盈=跳过,补仓=跳过,原因=持仓或行情数据无效", code or "未知", position.stock_name) + log.warning("[Position] 代码=%s,名称=%s,止盈=跳过,补仓=跳过,原因=持仓或行情数据无效", code or "未知", position.stock_name) continue pnl_rate = round( @@ -87,7 +87,7 @@ def manage_positions( loss_add_action = "大盘信号不允许" log.info( - "[Position] %s %s 盈亏=%.2f%%,止盈=%s,补仓=%s", + "[Position] 代码=%s,名称=%s,盈亏=%.2f%%,止盈=%s,补仓=%s", code, position.stock_name, pnl_rate, profit_action, loss_add_action, ) diff --git a/py-client/test.py b/py-client/test.py index 521f0dd..378686e 100644 --- a/py-client/test.py +++ b/py-client/test.py @@ -1,16 +1,42 @@ -from sdk.v2 import Client_V2 +from datetime import datetime + +from sdk import Client BASE_URL = "http://127.0.0.1:10086" TOKEN = "QMTbyYanweidong" +STOCK_CODE = "000021.SZ" +VOLUME = 100 -def main(): - client = Client_V2(BASE_URL, TOKEN) - portfolio = client.portfolio() - print(portfolio) - - +def main() -> None: + order = { + "opType": 23, + "orderType": 1101, + "stockCode": STOCK_CODE, + "prType": 5, + "price": -1, + "volume": VOLUME, + "quickTrade": 2, + "strategyName": "manual-test", + "orderId": f"test-{datetime.now():%H%M%S}", + } + + with Client(BASE_URL, TOKEN) as client: + #client._post_json("/api/sys/shutdown") + print("真实委托:", order) + result = client.passorder( + order["opType"], + order["stockCode"], + order["volume"], + order_type=order["orderType"], + pr_type=order["prType"], + price=order["price"], + quick_trade=order["quickTrade"], + strategy_name=order["strategyName"], + order_id=order["orderId"], + ) + print("下单结果:", result) if __name__ == "__main__": diff --git a/py-client/tests/test_trend.py b/py-client/tests/test_trend.py index 2629ade..b73c9c5 100644 --- a/py-client/tests/test_trend.py +++ b/py-client/tests/test_trend.py @@ -8,7 +8,7 @@ from types import SimpleNamespace from unittest.mock import patch from libs.grid_take_profit import GridState, GridTrailingTracker -from sdk import APIError, Assets, OrderItem, PositionItem, Tick +from sdk import APIError, Assets, OrderItem, Portfolio, PositionItem, Tick from strategy.trend.order import OrderBook, PlaceOrderRequest from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.boot import RunOnce @@ -80,7 +80,7 @@ class TrendTests(unittest.TestCase): client = FakeOrderClient(orders) book = OrderBook(cancel_timeout_sec=10) - book.refresh(client) + book.refresh(client, orders) self.assertEqual({item.id for item in book.data}, {"completed"}) self.assertEqual(client.canceled, ["active"]) @@ -196,10 +196,11 @@ class TrendTests(unittest.TestCase): def test_low_cash_still_runs_position_management(self): client = SimpleNamespace( - assets=lambda: Assets(total=10000, available=10), - positions=lambda: (["A"], [PositionItem(stock_code="A", volume=100, open_price=10)]), - trade_detail_data=lambda _datatype: [], - deals=lambda: [], + portfolio=lambda: Portfolio( + assets=Assets(total=10000, available=10), + positions={"A": PositionItem(stock_code="A", volume=100, open_price=10)}, + orders=[], + ), full_tick=lambda _codes: {"A": Tick(last_price=11)}, ) with ThreadPoolExecutor(max_workers=2) as executor: @@ -207,7 +208,7 @@ class TrendTests(unittest.TestCase): client=client, account_cfg=SimpleNamespace(min_cash_ratio=0.1), global_cfg=SimpleNamespace(api_host="http://example"), - orders=SimpleNamespace(refresh=lambda _client: None, data=[]), + orders=SimpleNamespace(refresh=lambda _client, _orders: None, data=[]), state=SimpleNamespace( codes=["A"], reconcile=lambda *_args: None, @@ -235,10 +236,11 @@ class TrendTests(unittest.TestCase): )) state.save() client = SimpleNamespace( - assets=lambda: Assets(total=10000, available=5000), - positions=lambda: ([], []), - trade_detail_data=lambda _datatype: [], - deals=lambda: [], + portfolio=lambda: Portfolio( + assets=Assets(total=10000, available=5000), + positions={}, + orders=[], + ), full_tick=lambda _codes: {"A": Tick(last_price=10)}, ) signal = SimpleNamespace(code="A", signal_key="morning")