From e6a50963533990dace1b651805f74c67003a11a1 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Thu, 3 Sep 2026 11:33:43 +0800 Subject: [PATCH] fix bug --- api/qmt_rest_new.py | 1289 ++------------- api/qmt_rest_old.py | 1452 +++++++++++++++++ api/qmt_rest_rele.py | 1214 +------------- py-client/sdk/__init__.py | 40 +- .../sdk/__pycache__/__init__.cpython-311.pyc | Bin 1101 -> 793 bytes .../sdk/__pycache__/account.cpython-311.pyc | Bin 6074 -> 0 bytes .../sdk/__pycache__/client.cpython-311.pyc | Bin 7890 -> 6556 bytes .../sdk/__pycache__/context.cpython-311.pyc | Bin 0 -> 777 bytes .../sdk/__pycache__/data.cpython-311.pyc | Bin 17217 -> 1192 bytes .../sdk/__pycache__/errors.cpython-311.pyc | Bin 1374 -> 1404 bytes py-client/sdk/__pycache__/get.cpython-311.pyc | Bin 0 -> 3231 bytes .../sdk/__pycache__/misc.cpython-311.pyc | Bin 6708 -> 0 bytes .../sdk/__pycache__/models.cpython-311.pyc | Bin 16197 -> 17421 bytes .../sdk/__pycache__/portfolio.cpython-311.pyc | Bin 0 -> 5005 bytes py-client/sdk/__pycache__/sys.cpython-311.pyc | Bin 0 -> 775 bytes .../sdk/__pycache__/trade.cpython-311.pyc | Bin 11958 -> 2394 bytes py-client/sdk/__pycache__/v2.cpython-311.pyc | Bin 6401 -> 0 bytes py-client/sdk/account.py | 38 - py-client/sdk/client.py | 49 +- py-client/sdk/context.py | 8 + py-client/sdk/data.py | 84 +- py-client/sdk/get.py | 44 + py-client/sdk/misc.py | 31 - py-client/sdk/models.py | 17 + py-client/sdk/portfolio.py | 52 + py-client/sdk/sys.py | 8 + py-client/sdk/trade.py | 122 +- py-client/sdk/v2.py | 86 - .../trend/__pycache__/boot.cpython-311.pyc | Bin 10362 -> 10144 bytes .../trend/__pycache__/open.cpython-311.pyc | Bin 6917 -> 6903 bytes .../trend/__pycache__/order.cpython-311.pyc | Bin 8250 -> 8150 bytes .../__pycache__/positions.cpython-311.pyc | Bin 9357 -> 9427 bytes py-client/strategy/trend/boot.py | 42 +- py-client/strategy/trend/open.py | 1 - py-client/strategy/trend/order.py | 6 +- py-client/strategy/trend/positions.py | 6 +- py-client/test.py | 40 +- py-client/tests/test_trend.py | 24 +- 38 files changed, 1911 insertions(+), 2742 deletions(-) create mode 100644 api/qmt_rest_old.py delete mode 100644 py-client/sdk/__pycache__/account.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/context.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/get.cpython-311.pyc delete mode 100644 py-client/sdk/__pycache__/misc.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/portfolio.cpython-311.pyc create mode 100644 py-client/sdk/__pycache__/sys.cpython-311.pyc delete mode 100644 py-client/sdk/__pycache__/v2.cpython-311.pyc delete mode 100644 py-client/sdk/account.py create mode 100644 py-client/sdk/context.py create mode 100644 py-client/sdk/get.py delete mode 100644 py-client/sdk/misc.py create mode 100644 py-client/sdk/portfolio.py create mode 100644 py-client/sdk/sys.py delete mode 100644 py-client/sdk/v2.py 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 2bba3f581f0d69e00ad7766681297aec21e61be3..a33517f56b1b924de8ee5080e02717074075feb5 100644 GIT binary patch literal 793 zcmb`E&1%~~5XWa_d*v_rqb-#70Scm|y_Zn1V+BmzNVVh^FM`F5l%QHGCA%^ux4cDq z?xpl4`V2mrz4lOgGvt<28M&bpa_FJE!;kszz<+j_FP7B=05E=h`tZa6_>SWC7{8P2 z69Bk^2qKb#AdC>qjnohoRuNTJEp;WW3CgH2nJMb5E*h*MnygvYs%cBKSzB1l5*^kN zUDg#n))Rf!CvXZ;?K5HbBI<)}{v-fk6HO1&B+d%k(Di{gcBIUuwuUQ}WU*4Ev}>9U zl!^o6iJ?yg=&x-FKw!+>+#`iu1(kDzB8HV2kvy_ zOu0WEIea+h?%|>D1iHU_4d$N1-@238Ye#py=}u1uf#V0d`(`#ic*TS1VC3*4_nqC5 zRRE9_QA1P^Ekqk(Avy>X(M8k|4MY!t+c>iCYkFLcUpiejbJc?7bYgL;4 z@j(k+E94@I@9bT+#b(;%e37P{e}!vY_099qi|}JEm*M$pkrmHV%6n|$sw)53*SXj!2TNR{kAcT}QE<_}aY22ckh?9v;<<^edvjcRi5JLwB z#6m|@2@on@8~hm=qgxoN7O@4DsS~<$LaFGxpWeN9?>*i7dY8{<002Hzc2;cxV4o>{ zO811LmjLhz9B_~i0kRNcvfvAWXh~ca{Zt@ZGM6P^2~41V?=aPTEDT><+tGS9+@e>?!?(EO7;#%r3GJP;v4&{6XvcQ)EKs#zlypKuLi01N$ZNV$#8u{3@uEgQ z98G8fkpO_EjgZ6-L*EM<{TvvvZy6np@W!}cAqOnML6+zUHPA#J-5Ljg2j0rmMi7r5 zym)^2@y+-x(=^5pKJ6ZEfBmudd4KEaye3f<By3fP%orO9m8%AxSzM+{)xb5qT&Ba-g`&QI&1FN! zOV#pXMW+KsPcIity=KzEJBy{++t^$v6m>jby-UZEm|Cf*4h)xTH^`;l zrbOQXperJTI&iUl8XeHur_q56?bGPMWcw_f<$12%M+e5*eRN=|eHOYhLX%zipKaH7 euD)LVqJ6vfi&=l4xTui5FLvRCZz=5X2K)htrZTnw 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 b29005b0b4fcc9c7b23bf95201b62c5a46805073..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6074 zcmd5=U2GFq7Cv)l#`z@<`6omW%3sV+LMTv52`x+b2Lo|{;D*dPo=NIt;xRL0ip>Og z@I$sjDy&v&RJE$r2O!I?HmbC0v@gr9R4I?wXe6SMSgo{AeKSj?mHMu?z!FNwEn$QNz^uwI1dD&f0X)rXik7zc zbb&5QrT87Cd*jYNkKZZFnZgm|kQoZ+cg~?-kLVv@9y$sZTyL7>%z`ybm|I-2WDm*x z<^arqkLW0!vvx$+CL~i2$%-T^W+G)ht-G@H3toOd@v_*XDsn!0PX>8@w?ZvEnl|T>)x3YGV6Nmbv^m<*N8d3TCWyFX?4KoxCD~^ETI{c_&Is*ESBQhWKiH=|>>WGd9*eQ#J5~>!G9bON8 z>{R64$v_%u$HL6#zKK!1k!)VY1yKUyk4%q+x~9*%e%+Pc@?_m?uf6X$?KqLCJ89RQ zq;==#_fD0)>^wNv{N0vJ=SjQsB(3S68vS(qlkw?o>Ft@C9d^wQTC-#R>J=KQwom)*q+U$6tEu z9~}Et;(j6rT;JsveIGvW`!LgY-R`?iTZb~OLw4&B^$yW;7t>`2fbsG!=1i;cvRuLm z0q_xja&1>A7U{aVw<{0n45xPhzznBvL7g{n|_S z9Lu!#+U>n`UGMS(fb#xqnX8%1A8D!_iH_!i&hV@#7IX5){)74W%>qe;~CL6V%3h-Cae$#eLXJXnQQ1i)oW8_FT38I+y33YOxG#9 z>lF1~ocj3F+n?N?K9GJV189+ZQ9-Di|$*`yKiLylH5s>1vpL4kXHbE&ypWl{R&{$Ic)Dbhtpfnk^g(78w!vP z-3&$4(6FkwXnXVh0>t&b=+mX}?nijrir_7I7!v^Fsta*^w*On{8;KtOkPeN~$b=n9 z((AV~`z?FF^)G+}N69}~odx&>3E=HAxq{#xxQgH&xQgvP1K8d(fYZJK7ii`aj>ys% zM~vx_u;O@bYHEB!amr#k%iqHVdG(Up?|TDtS)4^I-Vz>(08jHfxDmqT3Qd<4y=yq0 znBg3c#CkKLO9PmY>hEB65UY0)3f!Y0Ec3(hM6$LBz~OiTxx~N+?*R>5eGwRX9vGsM zMsM7twp&emZlM@E=D>L_Ea*3jp1OV4*FuE9Ty8&R`h9`}4+o<;_^&XuP z7OLuK`L6WU2ffoR5B&??`cLnEa(CKH_h-C2Z0`&u z=0{6D8QtLNrc62iFhgeCjh~K(4H+~18`#=e%N9!Rvl8aK$qhw5v}_(fjtnIEd+RC)rqVR>GLht5QgH!`Tcez@dauoyw)fC3*VN1WP|L2e}97hQ}Mf-blMo z&A&^p1nn!M^vp+@O%Z!jgtkUp6$`_0KHPus2Df0mSMc2|lP~xcPD!$Pxth#Ii^qo% zA4{T67vx8b7;&mNKdQ;8IGm95xT+-A7umje5>qVW7{g#r{ZW8B!{?G?cL<2d#HI1U{gUm;lkPmWI^y~@gB?jvH;&`%)GlsJp-M>44^ zLW!;3hT!aB;7s>^b>Z_1bXU)_iurx^*+KgL^-SB4-8S^1ZTNZHFb$7UHJWMD>^6-y zYRqH&K8^)cUyD;NNzu@RB1ulABu!{?T*Y>^B;AOI)Z7ejn7ZSYq);@fnIZfW(U2rZ zG~+Q<(ceO`k+0dO6Z=rocVib3*1NDm&SQBR%O)H?5;b-F`q$A%8DHurv2_Nkb6BC; z>^}=8uKV{yIgVk4FEc*p@Ng{s7U~+QfLBIn%nBY+;--x(;5{9G(m%WTch}~Gxk3Bj z8M^z7z5T2^wL)DB;&F}&y;SI35PB%PvH~w|oAzXZ7q^vGl^~uY)0eY=_Z4aa;s8lE z;tWe@N^1fl3+m!UGF^|WE|N6c?IM{y=Jua$cJC|H1Vk1൤R`rpzg6sOoY{0!g zd)vLQPz$`c6ZOeel_1{bsBni0cNT;QWf#6-oTQl~<7Ade$PQIWy&!0`;Bw7|#`60- zj>k;I@N*S;%JEESvZ5NUdoaIYhQ&XqmW=&l`C-==kLUvcfJ|(R13;GNIBo%&Xz}%T Z=%6cISx-9`7fK6n?W_+iQFM6X(Z-K;;xjvVcjU6lft78W7MDl$OwzrLY*!Kpbpun4Pq7 zgk*#)^+&A=+N$)>V?~8Ha%>Mh^w@*N$y)W)9;&LBpl~Ut>doTB3mj^Xp62Jx_rCY$ z&D(i6@%^i#KPHoL008B$@8;@BfIo52`6c`gS^53;=&=R?8m7@oqN}!=|HD#t` zKUPUsGiFBi1JW6KqjTl^4r3gLttc zMN#Yww8PUUj&g2ugLv98wyKnxwz;@$SIYLP=IOfA>Bnbykuo_db&TUL$x?K?dn?qR=sgWa!HXZCnVEKf4xJ2p zAxCgGspd2#w4E<87#1pqHP%Zik7hxg+sQYlxyfXN6>zoanf>?3DMR4~QVN$Uw!`ibz7+r*f_Quz@_7XSmNH#PQgPV* zHo7TM0Dvd@s0_p}(XkWb0B{fZ&LD&rvn*B}J%D0>BMur+Yt3@Pb>= z&M`zYh$LVrk{92DnwYM6pB*~O7GL13KRJr(y*{v}e;xlK{<;38{+ax9B+oaye{1Gj z(vsqZ{j)$i9fOiux513Vgw=gIOGDEAMl46Rx_4sRN#Vh-F^ix~+qhY-C9I1KH65g7cFf`xC30ygd;!XDteeZfa1utbi#jq~b1#^}LrP1+Z zKN){;H2E7j^j;Tf+#Dbsx2~|x&i?KbSe-uRPNj!Q!L`%Y@iPb&^2zu-5D#x$z7zI@ zHE%l|#DQ+awfVQ*Y-SrNxCb*IpY;RG-$z$;i?WhUnKkFwwSb+e=j_BJLsSfA4)Vs? zDL2-$ixk`gJ&PYcwQ&|BcMe@ppK-y*_peIFk+@G-S_ni&`hnB$x>|OR%yhZGq_{iP zKKFd~r>RMKZz^_?I{Y0!W-?sph8JRsr9jLir=?tXth(ZG#x3@K(=Z6{K_Jbq2bdmy zQII{X4154Z4#hguRG#ne@_omv!uJNXRhiE|n#4d|E(53W31~b`N#6}=6OW!pR#6W5 z*wRh8E>Yxcq-*wn?LySxuH8Xn&naMFr7_>ET+g_luG`Vrjw@KH_-iO$M{xiJUJIFa zeqMZp!wWJC9Z?kJQk98Wo657{(!8t+vhZb47X{W@XNX{V>ipa4zLvUgS&fI|%McH{ z2M7OcfELazqwq0sXW)~CH5CBJ3gXF6*4h^fKcXQ%f~9tsU$FC+ZJrmiEPr_VM4m0q z+YY1oOZEK4i`S~c;>;0NjeW9XzJBrIj#GxpFI_>pXpmDB48=1ZA-)tv^a%5((4g6M! zb|T&C9ebD-(KV#=-e;+A!JK`d})Sscq6pLFXR#~NEOFZ^n}D7c0r z71?rp_IMG7j~7Q4ZZC9&368HbFE-zKzHlAKA*b%}B=*7rpDsLw24pVDa7veg=ki25 zP_^koy21`}ff<6%o+s`<8$W0aw;<5kFw_dRHf(4GTN?&j!PbT?tzdhR&c;@-Ek{l^ z2+1wO$rs8;a{h7m>IEf6CYIrO(N;5URd1`=wwi3KDcMgwO2^2=<2CJp5HXe^Aku4N HdAR=pyWx-n delta 3963 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 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 0000000000000000000000000000000000000000..262300d5cc4232d40d1ba3c5e86ecd9506f8aa4a GIT binary patch literal 777 zcmZ`%F>ljA7=54Zgiw-36j2EVR54+RCZ$t~5K;va6Ir^sMkhDtL~fmPaz2Zus;Um1 zSQuesDneZt`58uHs5~JdA+ZI8DHFOlDXD(l_wj2Ydrcwq~6@rRzUDZAzg5h+gv zk%(=|%uO+9&DQFe5^1zNvGd^&hhy*pH%~0bnVY>vgXe)LXf4JrM24 zrtB)~_=@euk+3PVVn1xhnz}NsJ$>lDjO3o%PduUAouISY?<%)9SZ#%YFO(beJ$EWr z>kYE<6f-S+aBF^hwfWK&0P$@A$2hOvczyKhD82LGb9Mbobv>;%KJKKY#z^m`X&X|n zl_lMFD=RT3yspn!w!m07;z_7+nX!K2g?U9*0DOR4)U>ir#aYrIzBh3_o<2i^3L8WG zC*>lV*0c-SvYn_z`ix0^4p~tRdV%OjeYbL#C50qB;-+W>|p5uM|1 bdU<@sa(a0Tiw12B@pt;E=YIX8FfsoP&?CD5 literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/data.cpython-311.pyc b/py-client/sdk/__pycache__/data.cpython-311.pyc index 006801405e267b7c723ba6c87a83608afd11e244..ccbfb86ded8d1382de0438dc1a63efe7bd8f395b 100644 GIT binary patch literal 1192 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 literal 0 HcmV?d00001 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 32b859143ce366d19b0b0366d29017b4c642c78d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6708 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 diff --git a/py-client/sdk/__pycache__/models.cpython-311.pyc b/py-client/sdk/__pycache__/models.cpython-311.pyc index 805c3d113aa261a978281e9e041cfecd3e798063..e17059cae1f5b2e0e74b30129cf186247ca50b87 100644 GIT binary patch delta 1772 zcma)+e@t6d6vxl)du{21_Emng{AL0ta2`8_bo{$Rp79B3BEGP}N#)O8f!dw-Ax z9VShNiF1XE8F5jzkg;Kx$tXA#r_s2uI1_174K^_$F~8Vmx5_P~f+p&VC9Q{( zbO!-I(szR+C1&XI%WA*h=k?1&01(r;6vd+|XRVUgt@bIZT)wZ%uXt6jf8em{IddXr z7~B{tXGNb^QTvX{l1EO?>HMe_T3v7N9033sF$w@m4FHVn_^$EkBhC@$+s;+qw!(f9 z-nPo;t*~EYQ^@UXmGEDVzX`cWAC*N&KMmT-XuSNHLXSZ*NofIo1^_~MeC*RJNXBk( zh&1~^Yn$xAcs(K&03Pe?Jzn=|yIZsA4FDzp-~h;@VFGXoRI)ucOjHdurDX3{1Pu(s zlZmKK9{0)$zHcdK7pI`V+)zF&F5D<@)#R;x|HPD!6n_$EKdM{ zgO3Sb#XF#=t^v0v#$(fIu=MG3COoH#e%XKKB-%d09!|3!{x0TB7GL8dVo6jii32g@ ztY%n(HDk3CwWFP@V*04^tF>@aof`UJ=tk;X&zQt{E9Tt)3R*R1#`U~eNC0mZ))8^w4FbC~P6xlM`SSa?;Z*rB zZvG`i2ZEj35mf9{bf_+4Q{@SI+EE||Q6eVs3{*OE;ArKV)*RR9wL%;=k>Ei0RG@E{ zHFE4OKl`bbV{B8>nUxA0sw=-mA$LuHhIlPYbI5D&1E{q&25 z0vf14MwVztT`SFM=xF#7JxUOF5nticlja}9m*2@j_LO~%_I$)Qh;OmpqljW#tCD>XOwRIpcS z8_u&!$?gj4FFX3raf0oURkBPkG#3az006$wln3F3=2r3|Xi`UVf|)DC5+Rl-vBWhT r$&bTcB+#~wNN}LBZn>t>KM&ffj6{gTUL?@Aj@S-DuYKY6mVe2AK;PS& delta 941 zcmZXS-AmJ96vxlA{kFFGGRvG}YEY!PNmk3PrS4~GL?vp>y78hSe+EW1`=yt4li;#t zt&=akt{2PHB zh-hJ5-(h!tBGYY}HzkV0ONuo|r=`ix-%NRluB1)#YOYSDDFe18x05vYMRS43glTk% zeqq(tG?Le695jC{GU`g^g^2O?wI4)1zb`7`n$lHP&DKa>JD)C9&GlY8zuw?kdX|WE zD-nHBwASW!y@yv8W;?s#1rkJbBEl|zL)Nqsm{tP%jz32%N?knWpYbp}b-*P6#q?gF zAGI7Pp#uZ_eB*HiN~YG7O^7(pmsMG1S}%tK{?%6zENfkZx(?g`QZZ0TS@kj`aEr$S zqY4zN8XZBeu*PwCg$Fln3*CW~1qY$-0xsYlFa*d8--Nmk3dT zm+=8#0T)&zO~VRbP;>aZ&*^-Ka9|QcJH!>?0=85}6u1`UWpiI;ZD<_9g}@`=F*YDQ z+ScA0J+{A1Z*6YT4fykcC%{v@Lw_be{{&E~9 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 0000000000000000000000000000000000000000..664573f2a6004948f25bf4c675f9ee1394024278 GIT binary patch literal 5005 zcmd5=T}&I<6+U-nY>yq10S^#ZlC_(#DOL&41lS}HHf+--sT6Otv`OU{kS*1lv2RX{xT-{tu|4(o4FUw*6PKRG-YIl3BaqN>)qa zV>mHUem%C*x5%pzX1NM6qCkvOniPIfi1A8L@hl24L6H^Tq7d^a&5D0fh`s(PMgs0aWPXlc3m0e}Y}ug(z!tN^R!8CO%t zBLeWG@??%AVV)@5A{5AK7vCcu;0a&00}~`Vcc3?sOZMi<5qE)&uE-2*7ULuo z;6}$d2QW|OxdPFvfSj(33tWM?f&4vp_s|%u!#YP15ZgaVv+1~!oY2>Ko9pSbg^8@9 z+MZt~Qh8NF`@-SycHj9k6ABBPIgl$<(#E3J>`W{*##l4?dD*OZ&%6}x+K zv)h4TeCI?qog10!b|+NN&cfkTb|R6|MtXKu?*jmB2mltK2z*Omr~7+j$Fi`j7J1m; zbidmQcCEE+2xf4=3Jw%`*UDE2tL@K~_@dd~1S6IUP|N`a=pX}h3IT1M-wVi^9KOfh zJvatb0zf1&Hg*$3Ql|+;jVr32NT${~D&q`Kv$r)&;Bc6>0>E})IF;1i0G!I|AcAw0 zfn&(moNH6Qf5~d@-QdmUeyh2^a-X*0x4l`E5-q~hw*Y{)WA`0Y8?Wt;PpNu*TFYi^ zeoEDCPf}OY8lyS&;<_gEj<*F(O-?GTk?M`LSy@Lx!sbFuAYB<@$-voKii;Y=NYT>j42OU^3YamXp15Bh444= zui}?&#@J8Ib62f%R}J}^DPObXYek>X7j?jKO{X`y z!)#cs!#8f!B^8sz4VF~@f0C#Oi%{Td<*2|-a$^wjMCaPv64YDakjqU>55^s5!(rTc zt1L#}R*LR%cRNZPqK%kuEW1mR!;*`J0bm#TaJE6MNlHLgTxJG`M!jkOoa@ z(2@o>{f0DZh@%erm|w1hL$PMO`UyjSj}+UZOxUdeac}2i5aFV8Ej79CY#I@MM-3UI z>x3RdXmFP;^YQ|T69jDQokkj z8`7Cg4$+Z6o%v+up>H*4N}ZO}X^5RHCbo!mg+7~8ZC=wUMJOh!i~S1o+X%pK;em_$ zwPo4v#|if_;oBS~=E)tHMxwE{1d~J|i{c%aE}anp?o=Hj6z)&LJXhdWQMT$@t&Q`1 zfnNyfSUQ%P!WYON;WN&yNxvZ+6nMAo^Fo0r!cB$`V;@EtMQ_2WV#VQ9BK?t)7@0d!lN8lOa2(@XO<(7k zZhRRQ!~a?UM-k0%pB3)gXtKg57r^1GVEYmi6WM%5x5aWHVtW`PZ2>cH?r1)v<#Skq zs!EuBHtf{i;c{A?3DdLagh;U}usxbilR4Xy%HCF~QxhZYS#qkEgRrK853kc>GpAk#c4zg5=~PJIw3^?gCCA zqnLqC?T80S)}fi&L#EverN1H<@C1tB?%KSb_el}z`bOgUfE1yw_i}7~T`zK;cZ%Td z#0JmVBJ9yQMx(vaNC4fR%@b|@7{a9AMfk+1pvstzuox+;F})I z!YTn7w*kBX10x}Z#34juBQ%DlV-gs62xcCFSw{wNY|QA6#VS46)gOAoCix|g^GJCr zh`6*#Zf^3lKU=Hglt{))Q#&6HVQ>sqVdkObn9R~*d99bqN5PAL=T+gWMDR(21%0(8 zmAJjK$38o06o}7cW=4n#X(D^vK6Q5X~gL> zU&tWhHpws2fv@O}oDMAMhucyMtW0h_xa&TT#I8F^Jg(d)!C-YXRBoKE_QSyE%9U)_ zm8o2d)4VY+ySF^s-r9WO0swgpzyVI0S6=VG+Rv`v`P^)OX|}Uw=i~jX)*0*lbZtZE zwW6jE-O6i}a&PEUnlDf~j93zCyh!ON@xrnq>Ht1KG&F5dr|K+eAa71x4^F?rAeD^* z|4F5YrgiO#w%krsB791PzKOi5(m3D)p(h%&nw literal 0 HcmV?d00001 diff --git a/py-client/sdk/__pycache__/trade.cpython-311.pyc b/py-client/sdk/__pycache__/trade.cpython-311.pyc index b9906802fd610e56bee05cb497da0fdca15b1a0c..d8d8ad7f7f27d97317c42f0f15d971424d7cec2b 100644 GIT binary patch literal 2394 zcma)7&2JM&6o0e3UfUagg*b6Onh-!0a+?rJBcLg*6o{#4^FLE9ya6+n5y;(aSRY*G?KfQhPcHaDc z@4eYyqfre2;P1a4-2Fub_!A#mBj77!M+bnPfdVB3SRpzgs7nQDMb>2kRMr)$=m8qg zRjTSi8q`BHM8o2*Jyi6t1YZDc+XNbUECD=~S^;#8MzfHPZKEMgcw4SmEIB#HDi!TC z;qrK~x}5@m?QQ_DrWAl}JkvIAz~!sAZgVA@nVd{3T)OZvmoAKQY4kFeMnB27BH&Q{ z2eNZTz%W546m-dyO@$IOKqV?Isx(F0HxWQL?buZGAnl>aO+^pUUfQ{-=waHGg>)a+ z=2?!KQ`SAJxV^i(yUT$GOJ&2UmQ5~OMTcv)Q_3$J`4Tm`Y&(pr- z2Fpz)P-a%%dJ1=4Y^?|Jq#btQWnzk z^jdNxSGGpP*+$Aa+ZN|Zhq+oR&m;P@5ztfk@A6=$Jb9nKdlv*;|?ba_EwJ6Epq@E*RkkQ*#>k-+oECeS6W#UO#zTg+J!!&t543V1;q25+C4o5;)=^S5U+#@92Gx27^YHrsrj9G}l*=Xw0*tsB?AGUn&T zCo;zL%r~M*8CC)tMS%_S1O&69WLW~OC}NnBA4N)P&0iw|eM4UAeJ|DH^?l&=pY*zW z8=XA?tpP`fizINl0f&VeL+8U{kObxkI=)-_=fhJ?r`wQ_#fnp5reQF=gt+Qd%T{rT z;Vr|}nOOrr)WMlq1HaX53=w^b0zbz*>64bdF_AH@-Zo~gU(aUdeb`uq`&7IQUGaJU zoLvI|f_(f001cTC;=$?q;rR->>W61T1uarjJuOlTddcoOw0rF*YBBFbM=kCRocZnI dv(D#>_0$wjNH)~-1Q|G5z83a3?^qRw{s+&}IEnxO literal 11958 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 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 ff2af621c90c1b1e7f9afea6b6fc8ef488e3d4f8..8eac14b99a4520489664431bb04d2b459a766511 100644 GIT binary patch delta 2564 zcma)7U2GKB6+UfcLY(D)|F6|a-zsmTCtTX+KLAfxfN0$nnrDuIu9x9jAX|WFReoIQVHHro~mnS zc5PFnj#g*yJ@@?F`<-*Y`}K+1as79ZNDu&^@2h$Hv-JD=v?>TPUQn;4i5=hz{Bs2- z_RSCg7{Sqa4tq5nKR0#B6`dr$9BNfuiAl9EK9@|$z=$2eH&U&-)7Esfqj*Wp5(6)( ziBKb;y=}|91#c!y$(5Om_Y;{Be;u zf--+*Gu&wjh< z(g`&0hBh5{=YkmdEg{|bd;MF3tJ-XZI%g_p%N4pC09K{5<*Gw__)s}Nb@r7~l}7m_ zKuc$6srn;&2mr1|KhvHBfHfCfjs7Hf3IGZKP<<1pjlv7DKL$tVsH@WG+eOTj%Fr z^Rk7yw9k|F-In$(c!nnAM~ra;7LDNeu(+7A#)rhkA$fdQq~GTo;?1?oRB{T zuoWB$o7-GL0|w3Buc>%n&1Xd>+L~|dN>0Kb12EAJ@iy4*%3IaGQ7w%3)ee4L6jxzj zNAREVjIl8d6W1@yrJ1fgI2qp&c(q#zIPFbMvx$q!)&orf2j>F3Do(-$!X$f}@5iKO z%QLX!zTR$}fHvvvZuuI{%+40@v*@uC3WMqR^sW`j+X8P^4X8x5u=aK(JYadqcd)AQl`ia&$q{KA*|s zmCQsBXWQJ?kM8BcpZCQ%{wjPbUd<*&B1f?&`>d_X(Uo}e?TO`h_fovO9^dW7ch6}n zv6L5UpHuEwZS!R>(^t3ld)EFr{Z71f-tw$H%U1uA)nB&;JZqpHAN1mbbJ~g;x)NMg zQ%h=UzW-N`)n4;<9;~ZFo;q}U?T7XDRQgC=9ro1W+v@O&s?RBN$~{?-qYoZDxEDk# zcYNW`xz~lG2La&21EV8P9Du(a$c&ld-xFla6h9tHJ>3HTXxTF^ii-l_GSD^N3yZy( ziKzIAP9~xveVt<`Rvk*&!nOQ1($}aIod$qi!kPtT90}gGxonDehCk~nkP~>T>)$*0%e#&?kMswi`?!MF;A{2Bxk~Ysv!%!B lFBbl~<1`sOOs2>^ct|pGoRBl*9{jJrqJS@TCvmnr_+N$<{$l_D delta 2852 zcmb7GeQZV3pdjF3Y0Wu)G#0Jiiti zu9u)yDxJb;bUQ!-sxUG32cudQI;3tBQoC-@{u5`G!aPNrG>FV!rT?)>)u#KB7sqBy z-PO~1@7{CH@7#0lx#zs8egE3;`lZvU004~V&uZ!B_g#lE{!{=s2L_^JN{n8@uk$cv zqhH`w<$D}Jfh+KammZZPNT!nkzYVS;fNO}Rf&osm)00Y+(;W1q;mcnXrSZV87$`RTk8^jD$X7SGs3=NrRl6HVGT zpcFk|Pq~++h)4vZjUJU7IqTIy$CO}VSwah0IE(IBGh-|L(%u%?Zp{G*BD}#>=bDqI zlt2JMMk-(#jP4bzD4nziIIV@AaeUpmk`beSbwtok`cFrqC0$ieK~EIyHU1{1yPa1M zQt97a-)Pj=1AxAP5$1QNz6~fxFZ$A73mCSl zTbf{fXHac;5oEaM!LVC=fDE4z=yjh;KgWTdj94pv0W#7he$*6Ww3ds&Sk49j4b#gF zL6MVz0Bw_FV+6EiJN1gWWCD!F>Mm5!8xhp_u$(FxTfO!{(~DzpoIeQ15Hnb{H04@C zrThI(8u7cv3jF_N4gYUzgs)x!!fPJO2Lc*96mw{+tglfKHp9yzn-&Bj=~qLtj@xA&Ta= zHch10ke(uVDqt3jdk(Ka82SBaQ!eS(vAgI$rOK`{MyFd8!2tOWq0 zz4|>BVzzmS$n@o4+-t83K$uPj)b@fS*YQ{$t$t!2IgAv&5C~Ccqc6N76ee`DWqK;Y z47+UvG}&6yqBI_C9Q!J9wp23C`tZ~y?Gr-estHkT;y(>F%(^^fCHJZbaLr6MHV|ao z1hf`wOH(oW1#UnXVL|mDDS0#XJk|lG9JB$iWg7t19>f4E+ZzayD00Z)5 zJddNVGFDVq6CKmPG{py;Y3TEt`RXdTjc;1$0K`SyS2=)d%M~8M#FL7d*XXTk*43U# zd%!`sQ3W8gYE%SZ*(I@Jx6Vf|hk7=?x&{DVs@!_Ja(SXJ_TbXH_ddL`aGoxl`^|%M zH$MN}MCImm<<^@E7k^dx{d@Fpp}kvKgL-M7p2_NcvHPcPE}Z>!<-MQZKmD`&SKhDO z_{FPX6YhgI9=ti(7pwgA=KV8gD<8aZ??bvb{6aj!nEQwGL&nf>&SEc2oV@p2x~M*V zIlKe)Cv{e|br$S0pBp}=Gn+a?E{h}jP|hghyrCa5hK^+Q=h&)zWH>i$4CjUhb=D#E zXBgSS$hS>)_V{3SgxNG37d;S(wPMC)9jq)24rj7uX<%d|o6G2}=F%%ed4ryg#3dG3 zeK)-kd8)~?*kWE}0!rQ((2Z=Sf56b!Q-3tk;2K$StDnK;O=!9`x@SGBgt5AzKgD1V zSmYJfA=?FdJsND_ME5+lPv9AER}puYaQ6)ErhkflD=LWYyVyRV%;C^14xR6~*j2=< zOL+B+`OvYJP)BBJM2V&KcZUuP4zPE&C!&SzU8w_nf+IR^3)qyGm+T5ywxt&dcXz6Jc`^Zz*N7>lh#oyVE80|^!R^d}Mx<}$a6VV=t*I5%T^LazpPq3h} z1~RyOa{z#rTfb~$xv-c@SpZnanOdeop2^0RjwQ#$7`aWcI=W%Se+6TFpOFSeSYKEt zErJlXaDrY}Q|LK5ul~c%etZ@WI7dHAhS2Hhh2$XG+=T|vLwJmiqJsz>LJ#4;y^hl# Ht!e!aDh@rp diff --git a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc index 4eb46795f5ee36047758b974d6f06b217e1d9db6..98ed56f31f235043900dada14b3b8c0bbfb3e0e1 100644 GIT binary patch delta 171 zcmZoQ`)q?8xse&a{k?fnhZx$OI5*VoYHUX3%7ryqMo*@(2FOjJcCr z1hho?Zg2?P;1K%A%g)dCg@K))t-<|+z~*BDf0!AAHXjyoVrDGZ%qFpsNrZ>d@B;%X KAvgK7lobH(Yb|&H delta 173 zcmexv+G@tToR^o2fq{XcvU_^g%8k5x`M5Y47#Nrt7#KgFntX*Xo-c)|hN*_7hItt~ z1H)?e$@cu-;>^n!85mYGg0z7^6JrWXFoP!RjIt`yLc;J(4Z zcY{OV28Yl`ehz-NFAN;~Yz^)ogf{OG_`}Q?ym_mL6EkDs<{#o4nM8OQ4L>lT5(<;| HN?8E_el;wx diff --git a/py-client/strategy/trend/__pycache__/order.cpython-311.pyc b/py-client/strategy/trend/__pycache__/order.cpython-311.pyc index fa69a5b7c09a313a469a9cdef875f3d61e566ad6..221c3572a55d811cd187e614b51331062cee8884 100644 GIT binary patch delta 1854 zcmZ`(O>7%g5T55<@2(xYPMTkn+Hpy0%MHyRGyyeAn$#gGG_8XZsya$6m`-pu>)pKz1RvfqRu<`BDypACqE<1eY5s%_006*a4RNOi1<8vwA0f8i@CsUEd)O^nK_ zS8ZJrql((4cC3j}o9d20(8t!ruw1cT%;e+x)<0qsX*>o1^c4gNp^vpmJsiMAc4mYQDExVY&Xv`%8 zfB-=DezyR84myVNbfpQ4+uK>#E)olCYHns;hikPgCZTLiL0M8sSvD*oSiE0W3?*G7 z)fJNf;1Z$avnE4HTTR{N#{^3D3`_o;NjJ89^Kk_aOL*)Pn!=vy|I1e_JeCT*%9Yag<7z^ z=n1yuoF=1e!x|f_B>0{vo{#D*yQoK;ma%#mnIUl(Yi_A^T%}1sp)rNe{?8=Ie z(Q-k1wKY5V4Pk4_i4vt+POms~`43}yDytRsc(zb?PD#Xb2`!_k6)~l2SsKD&z+w0K zd^TTo(EuZ^E(eZ4@n-_?32bkTyY7<4Mpvj;xYH|!`sF)AB-Fpv-gMX6@;3i-v6M(? z#o`#=8eL+0ZFD4z;{M!7RLjQcGDUwngW$oF2Qv9YJQL%S6HBRdlzrLGTG2$i5b*LT zZ^Bs?>PW7h#yuZTUX2KDIon$(;mFc=aBh`qaTf3&64i>Oj9wA6j}mkdU3jTwEOaWD ztX2Vs=xGIfG=Rsfg-kr5t+@CgPv-N>eT9q|PiRY9eeOkJ^(Yec;=iQoL&TpXe0{1? zKS~4DHv}IY+KIrmC~D{Ekn!YnWvxyzfrWq3+Vu(cP1O7bV3kh!oLkd}AA_+x&E0L%%#+W(F?N7kD$(NZq zGjnF<&YY#*?_Lgm9t`>c09t3xWtYDW?vzM^@0B`R4O^kUE&!m4tx&(d9O_<&@Mhma(`dUm^+qt=y?sO%6;BObzhgE0vDkS{kLkX*HqP zDt(#;rj-Q%xPVCrN@%o>R!u7joo=Dk(@MgiF}itLN%&}F2+Vpup&V9O3;-6FlrX76 zjCL%=HmvCEnqW@VH+gGXJS!+eA@GLs88@QKM2(ccp!B z{C3fBjVxv$$PquXOs5DVvtifBVowB3r8n4yJUzfEU~jqddfU@}02i``XEUsT56j-~ zl6fD$?vET>y=r3(1YDger-J}R-HCYa0NH@YQ`I%HK~Hjx>}n#ek@fTLiWsTn*ZrIM zdll8k)=G;2|{YU^Xp!KZT3x4xXI=z3`$`?<3{>_yy#S*tE2pYk-W=0dXC zlci(6v`=8C; z{pQQ%k7t)JUs}F6dvE3={%hdb%95IA)MiY=kz-vv8hn*>^HgwO&8AY=VJT`SsqLgP zxg`~KSp4D0$Q%yuK@OMJd_BAuZN{w1} z&ZZ?LhmC%>dr77sf5iE);rjy4eZ-;byjH5ZSd&(wIGxV#5sW z*+GN`^bFm>UG57mg4Y&iqMJIFOIh~m%0u|)`tteV zwy~U&vg{L!Khz$VCw5?o}IlG6s+^PRP?<0{FcpM&} h?jgbh8KlOWWc!MQR<#x(H7oFBd?54>|L0URBWq!yp6nSoa#vo3=Gi$`Nf$fnfZCK3Qu+~e!ig5R<-#3 zo}Q-@dY>=f0AWw|;B-?@C0eceoR^o2fq{YHe95#di;cW$oT32?3=Gi$`Nf$fnfZCK3aZ5*G&zdXO*9yX zjQZqvoSQ|>u*q&d!L^5x(P(lj&lLIe3z@kW^2@(4FcdI4Gks)W;B{g80wzCz$jLUm f4xG6c@{2Fz*Iy`~EW@iZc`NU8MwiWV`OL%sHz_rd 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")