fix bug
This commit is contained in:
@@ -123,6 +123,24 @@ class StockGetHandler(BaseHandler):
|
|||||||
"ref": result
|
"ref": result
|
||||||
}, separators=(',', ':'), ensure_ascii=False, default=str))
|
}, 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
|
# ContextInfo.get_bar_timetag() - Get the bar timestamp
|
||||||
class BarTimetagHandler(BaseHandler):
|
class BarTimetagHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -1015,33 +1033,48 @@ class GetFactorRankHandler(BaseHandler):
|
|||||||
|
|
||||||
|
|
||||||
# ============= 9. Legacy handlers (compatibility) =============
|
# ============= 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
|
# get_trade_detail_data('position') - Query positions in the wrapped format
|
||||||
class HoldingHandler(BaseHandler):
|
class HoldingHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
data = json.loads(self.request.body)
|
data = json.loads(self.request.body)
|
||||||
account = data.get('account', 'stock')
|
account = data.get('account', 'stock')
|
||||||
positions = safe_call(get_trade_detail_data, self.acc(), account, 'position') or []
|
positions = safe_call(get_trade_detail_data, self.acc(), account, 'position') or []
|
||||||
holding = {}
|
holding = format_holding(positions)
|
||||||
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))
|
self.write(json.dumps({"data": holding}, separators=(',', ':'), ensure_ascii=False))
|
||||||
|
|
||||||
# get_trade_detail_data('account') - Query account assets
|
# get_trade_detail_data('account') - Query account assets
|
||||||
@@ -1050,10 +1083,9 @@ class AssetsHandler(BaseHandler):
|
|||||||
data = json.loads(self.request.body)
|
data = json.loads(self.request.body)
|
||||||
account = data.get('account', 'stock')
|
account = data.get('account', 'stock')
|
||||||
_data = safe_call(get_trade_detail_data, self.acc(), account, 'account')
|
_data = safe_call(get_trade_detail_data, self.acc(), account, 'account')
|
||||||
info = _data[0] if _data else None
|
self.write(json.dumps(format_assets(_data), separators=(',', ':'), ensure_ascii=False))
|
||||||
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
|
# passorder(23) - Simplified buy order wrapper
|
||||||
@@ -1210,6 +1242,7 @@ class DealHandler(BaseHandler):
|
|||||||
def make_app():
|
def make_app():
|
||||||
return Application([
|
return Application([
|
||||||
# V2
|
# V2
|
||||||
|
(r"/api/v2/portfolio", PortfolioHandler),
|
||||||
(r"/api/v2/positions", HoldingHandler),
|
(r"/api/v2/positions", HoldingHandler),
|
||||||
(r"/api/v2/assets", AssetsHandler),
|
(r"/api/v2/assets", AssetsHandler),
|
||||||
# ContextInfo properties
|
# ContextInfo properties
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# -*- coding: gbk -*-
|
# -*- coding: utf-8 -*-
|
||||||
import json
|
import json
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
@@ -26,10 +26,9 @@ def safe_call(func, *args, **kwargs):
|
|||||||
except HTTPError:
|
except HTTPError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("%s call failed", func.__name__)
|
|
||||||
raise HTTPError(
|
raise HTTPError(
|
||||||
502,
|
500,
|
||||||
reason="QMT upstream call failed: %s" % func.__name__,
|
reason="QMT: %s call failed." % func.__name__,
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
@@ -48,16 +47,13 @@ class BaseHandler(RequestHandler):
|
|||||||
if self.__class__ not in AUTH_EXEMPT:
|
if self.__class__ not in AUTH_EXEMPT:
|
||||||
token = self.request.headers.get('X-Token')
|
token = self.request.headers.get('X-Token')
|
||||||
if token != TOKEN:
|
if token != TOKEN:
|
||||||
raise HTTPError(401, "Authentication failed: invalid or missing token")
|
raise HTTPError(500, "Authentication failed: invalid or missing token")
|
||||||
|
|
||||||
def set_default_headers(self):
|
def set_default_headers(self):
|
||||||
self.set_header("Content-Type", "application/json; charset=utf-8")
|
self.set_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
def write_error(self, status_code, **kwargs):
|
def write_error(self, **kwargs):
|
||||||
self.finish(json.dumps({
|
self.finish(self._reason)
|
||||||
"error": self._reason,
|
|
||||||
"status_code": status_code
|
|
||||||
}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
def ctx(self):
|
def ctx(self):
|
||||||
return self.application.ContextInfo
|
return self.application.ContextInfo
|
||||||
@@ -67,6 +63,7 @@ class BaseHandler(RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
# ============= 1. ContextInfo properties =============
|
# ============= 1. ContextInfo properties =============
|
||||||
|
# "/api/v2/context/info"
|
||||||
class ContextInfoHandler(BaseHandler):
|
class ContextInfoHandler(BaseHandler):
|
||||||
def get(self):
|
def get(self):
|
||||||
ctx = self.ctx()
|
ctx = self.ctx()
|
||||||
@@ -80,38 +77,51 @@ class ContextInfoHandler(BaseHandler):
|
|||||||
"do_back_test": ctx.do_back_test,
|
"do_back_test": ctx.do_back_test,
|
||||||
"benchmark": ctx.benchmark,
|
"benchmark": ctx.benchmark,
|
||||||
"capital": ctx.capital,
|
"capital": ctx.capital,
|
||||||
|
"timetag":ctx.timetag,
|
||||||
"universe": ctx.get_universe(),
|
"universe": ctx.get_universe(),
|
||||||
}
|
}
|
||||||
self.write(data, separators=(',', ':'), ensure_ascii=False)
|
self.write(data, separators=(',', ':'), ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============= 2. Data queries (ContextInfo get_*) =============
|
# ============= 2. Data queries (ContextInfo get_*) =============
|
||||||
# ContextInfo.get_stock_name() - Get a stock name by symbol
|
STOCK_HANDLER = {
|
||||||
class StockNameHandler(BaseHandler):
|
# handler_type: (method_name, use_context)
|
||||||
def post(self):
|
"stock_name": ("get_stock_name", True),
|
||||||
data = json.loads(self.request.body)
|
"open_date": ("get_open_date", True),
|
||||||
stockcode = data.get('stockcode', '')
|
"last_volume": ("get_last_volume", True),
|
||||||
ret = safe_call(self.ctx().get_stock_name, stockcode)
|
"total_share": ("get_total_share", True),
|
||||||
self.write(json.dumps({"stockcode": stockcode, "name": ret}, separators=(',', ':'), ensure_ascii=False))
|
"svol": ("get_svol", True),
|
||||||
|
"bvol": ("get_bvol", True),
|
||||||
# get_open_date() - Get the listing date by symbol
|
"divid_factors": ("get_divid_factors", True),
|
||||||
class OpenDateHandler(BaseHandler):
|
"etf_info": ("get_etf_info", False),
|
||||||
def post(self):
|
"etf_iopv": ("get_etf_iopv", False),
|
||||||
data = json.loads(self.request.body)
|
"instrumentdetail": ("get_instrumentdetail", True),
|
||||||
stockcode = data.get('stockcode', '')
|
"his_st_data": ("get_his_st_data", True),
|
||||||
ret = safe_call(get_open_date, stockcode)
|
}
|
||||||
self.write(json.dumps({"stockcode": stockcode, "open_date": ret}, separators=(',', ':'), ensure_ascii=False))
|
# "/api/v2/get/*" Stock-related single-symbol queries
|
||||||
|
class StockGetHandler(BaseHandler):
|
||||||
# ContextInfo.get_last_volume() - Get the latest outstanding shares
|
def get(self, handler_type):
|
||||||
class LastVolumeHandler(BaseHandler):
|
# 快速路径:配置查找
|
||||||
def post(self):
|
cfg = STOCK_HANDLER.get(handler_type)
|
||||||
data = json.loads(self.request.body)
|
if not cfg:
|
||||||
stockcode = data.get('stockcode', '')
|
raise HTTPError(500, "Unknown API")
|
||||||
ret = safe_call(self.ctx().get_last_volume, stockcode)
|
|
||||||
if ret is None:
|
# 参数验证
|
||||||
raise HTTPError(500, "Failed to get outstanding shares")
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "last_volume": ret}, separators=(',', ':'), ensure_ascii=False))
|
query_vals = self.get_query_argument("stock_code", "").strip()
|
||||||
|
if not query_vals:
|
||||||
|
raise HTTPError(500, "stock_code required")
|
||||||
|
|
||||||
|
# 方法调用
|
||||||
|
method_name, use_context = cfg
|
||||||
|
method = getattr(self.ctx(), method_name) if use_context else globals()[method_name]
|
||||||
|
result = safe_call(method, query_vals)
|
||||||
|
|
||||||
|
|
||||||
|
# 响应
|
||||||
|
self.write(json.dumps({
|
||||||
|
"stock_code": query_vals,
|
||||||
|
"ref": result
|
||||||
|
}, separators=(',', ':'), ensure_ascii=False, default=str))
|
||||||
|
|
||||||
# ContextInfo.get_bar_timetag() - Get the bar timestamp
|
# ContextInfo.get_bar_timetag() - Get the bar timestamp
|
||||||
class BarTimetagHandler(BaseHandler):
|
class BarTimetagHandler(BaseHandler):
|
||||||
@@ -121,12 +131,6 @@ class BarTimetagHandler(BaseHandler):
|
|||||||
ret = safe_call(self.ctx().get_bar_timetag, index)
|
ret = safe_call(self.ctx().get_bar_timetag, index)
|
||||||
self.write(json.dumps({"index": index, "timetag": ret}, separators=(',', ':'), ensure_ascii=False))
|
self.write(json.dumps({"index": index, "timetag": ret}, separators=(',', ':'), ensure_ascii=False))
|
||||||
|
|
||||||
# ContextInfo.get_tick_timetag() - Get the latest tick timestamp
|
|
||||||
class TickTimetagHandler(BaseHandler):
|
|
||||||
def get(self):
|
|
||||||
ret = safe_call(self.ctx().get_tick_timetag)
|
|
||||||
self.write(json.dumps({"timetag": ret}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_sector() - Get index constituents
|
# ContextInfo.get_sector() - Get index constituents
|
||||||
class SectorHandler(BaseHandler):
|
class SectorHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -260,14 +264,6 @@ class FullTickHandler(BaseHandler):
|
|||||||
raise HTTPError(500, "Failed to get tick data")
|
raise HTTPError(500, "Failed to get tick data")
|
||||||
self.write(json.dumps(ret, separators=(',', ':'), ensure_ascii=False, default=str))
|
self.write(json.dumps(ret, separators=(',', ':'), ensure_ascii=False, default=str))
|
||||||
|
|
||||||
# ContextInfo.get_divid_factors() - Get dividend and adjustment factors
|
|
||||||
class DividFactorsHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(self.ctx().get_divid_factors, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "factors": ret or {}}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_main_contract() - Get the main futures contract
|
# ContextInfo.get_main_contract() - Get the main futures contract
|
||||||
class MainContractHandler(BaseHandler):
|
class MainContractHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -285,14 +281,6 @@ class TimetagToDatetimeHandler(BaseHandler):
|
|||||||
ret = safe_call(timetag_to_datetime, timetag, fmt)
|
ret = safe_call(timetag_to_datetime, timetag, fmt)
|
||||||
self.write(json.dumps({"timetag": timetag, "datetime": ret}, separators=(',', ':'), ensure_ascii=False))
|
self.write(json.dumps({"timetag": timetag, "datetime": ret}, separators=(',', ':'), ensure_ascii=False))
|
||||||
|
|
||||||
# ContextInfo.get_total_share() - Get total shares
|
|
||||||
class TotalShareHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(self.ctx().get_total_share, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "total_share": ret}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_trading_dates() - Get the trading-day list
|
# ContextInfo.get_trading_dates() - Get the trading-day list
|
||||||
class TradingDatesHandler(BaseHandler):
|
class TradingDatesHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -306,22 +294,6 @@ class TradingDatesHandler(BaseHandler):
|
|||||||
ret = safe_call(self.ctx().get_trading_dates, stockcode, start_date, end_date, count_int, period)
|
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))
|
self.write(json.dumps({"dates": ret or []}, separators=(',', ':'), ensure_ascii=False))
|
||||||
|
|
||||||
# ContextInfo.get_svol() - Get sell-side volume
|
|
||||||
class SvolHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(self.ctx().get_svol, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "svol": ret}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_bvol() - Get buy-side volume
|
|
||||||
class BvolHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(self.ctx().get_bvol, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "bvol": ret}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_longhubang() - Get Dragon-Tiger List data
|
# ContextInfo.get_longhubang() - Get Dragon-Tiger List data
|
||||||
class LonghubangHandler(BaseHandler):
|
class LonghubangHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -370,30 +342,6 @@ class TurnoverRateHandler(BaseHandler):
|
|||||||
ret = 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))
|
self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get turnover rate"}, separators=(',', ':'), ensure_ascii=False, default=str))
|
||||||
|
|
||||||
# get_etf_info() - Get ETF creation/redemption and constituent data
|
|
||||||
class EtfInfoHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(get_etf_info, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "info": ret or {}}, separators=(',', ':'), ensure_ascii=False, default=str))
|
|
||||||
|
|
||||||
# get_etf_iopv() - Get the ETF indicative optimized portfolio value
|
|
||||||
class EtfIopvHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(get_etf_iopv, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "iopv": ret}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_instrumentdetail() - Get instrument details
|
|
||||||
class InstrumentDetailHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockcode = data.get('stockcode', '')
|
|
||||||
ret = safe_call(self.ctx().get_instrumentdetail, stockcode)
|
|
||||||
self.write(json.dumps({"stockcode": stockcode, "detail": ret or {}}, separators=(',', ':'), ensure_ascii=False, default=str))
|
|
||||||
|
|
||||||
# ContextInfo.get_contract_expire_date() - Get the futures contract expiration date
|
# ContextInfo.get_contract_expire_date() - Get the futures contract expiration date
|
||||||
class ContractExpireDateHandler(BaseHandler):
|
class ContractExpireDateHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -454,14 +402,6 @@ class FactorDataHandler(BaseHandler):
|
|||||||
ret = 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))
|
self.write(json.dumps({"data": ret} if ret is not None else {"error": "Failed to get factor data"}, separators=(',', ':'), ensure_ascii=False, default=str))
|
||||||
|
|
||||||
# ContextInfo.get_his_st_data() - Get historical ST data
|
|
||||||
class HisStDataHandler(BaseHandler):
|
|
||||||
def post(self):
|
|
||||||
data = json.loads(self.request.body)
|
|
||||||
stockCode = data.get('stockCode', '')
|
|
||||||
ret = safe_call(self.ctx().get_his_st_data, stockCode)
|
|
||||||
self.write(json.dumps({"stockCode": stockCode, "data": ret or {}}, separators=(',', ':'), ensure_ascii=False))
|
|
||||||
|
|
||||||
# ContextInfo.get_his_index_data() - Get historical index data
|
# ContextInfo.get_his_index_data() - Get historical index data
|
||||||
class HisIndexDataHandler(BaseHandler):
|
class HisIndexDataHandler(BaseHandler):
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -1272,6 +1212,9 @@ def make_app():
|
|||||||
# V2
|
# V2
|
||||||
(r"/api/v2/positions", HoldingHandler),
|
(r"/api/v2/positions", HoldingHandler),
|
||||||
(r"/api/v2/assets", AssetsHandler),
|
(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
|
# Legacy compatibility routes
|
||||||
(r"/api/holding", HoldingHandler),
|
(r"/api/holding", HoldingHandler),
|
||||||
@@ -1283,15 +1226,8 @@ def make_app():
|
|||||||
(r"/api/order/cancel_by_id", CancelByIdHandler),
|
(r"/api/order/cancel_by_id", CancelByIdHandler),
|
||||||
(r"/api/order/deal", DealHandler),
|
(r"/api/order/deal", DealHandler),
|
||||||
|
|
||||||
# ContextInfo properties
|
|
||||||
(r"/api/context/info", ContextInfoHandler),
|
|
||||||
|
|
||||||
# Data queries
|
# Data queries
|
||||||
(r"/api/data/stock_name", StockNameHandler),
|
|
||||||
(r"/api/data/open_date", OpenDateHandler),
|
|
||||||
(r"/api/data/last_volume", LastVolumeHandler),
|
|
||||||
(r"/api/data/bar_timetag", BarTimetagHandler),
|
(r"/api/data/bar_timetag", BarTimetagHandler),
|
||||||
(r"/api/data/tick_timetag", TickTimetagHandler),
|
|
||||||
(r"/api/data/sector", SectorHandler),
|
(r"/api/data/sector", SectorHandler),
|
||||||
(r"/api/data/industry", IndustryHandler),
|
(r"/api/data/industry", IndustryHandler),
|
||||||
(r"/api/data/stock_list_in_sector", StockListInSectorHandler),
|
(r"/api/data/stock_list_in_sector", StockListInSectorHandler),
|
||||||
@@ -1303,25 +1239,17 @@ def make_app():
|
|||||||
(r"/api/data/market_data", MarketDataHandler),
|
(r"/api/data/market_data", MarketDataHandler),
|
||||||
(r"/api/data/market_data_ex", MarketDataExHandler),
|
(r"/api/data/market_data_ex", MarketDataExHandler),
|
||||||
(r"/api/data/full_tick", FullTickHandler),
|
(r"/api/data/full_tick", FullTickHandler),
|
||||||
(r"/api/data/divid_factors", DividFactorsHandler),
|
|
||||||
(r"/api/data/main_contract", MainContractHandler),
|
(r"/api/data/main_contract", MainContractHandler),
|
||||||
(r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler),
|
(r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler),
|
||||||
(r"/api/data/total_share", TotalShareHandler),
|
|
||||||
(r"/api/data/trading_dates", TradingDatesHandler),
|
(r"/api/data/trading_dates", TradingDatesHandler),
|
||||||
(r"/api/data/svol", SvolHandler),
|
|
||||||
(r"/api/data/bvol", BvolHandler),
|
|
||||||
(r"/api/data/longhubang", LonghubangHandler),
|
(r"/api/data/longhubang", LonghubangHandler),
|
||||||
(r"/api/data/top10_share_holder", Top10ShareHolderHandler),
|
(r"/api/data/top10_share_holder", Top10ShareHolderHandler),
|
||||||
(r"/api/data/option_detail", OptionDetailHandler),
|
(r"/api/data/option_detail", OptionDetailHandler),
|
||||||
(r"/api/data/turnover_rate", TurnoverRateHandler),
|
(r"/api/data/turnover_rate", TurnoverRateHandler),
|
||||||
(r"/api/data/etf_info", EtfInfoHandler),
|
|
||||||
(r"/api/data/etf_iopv", EtfIopvHandler),
|
|
||||||
(r"/api/data/instrumentdetail", InstrumentDetailHandler),
|
|
||||||
(r"/api/data/contract_expire_date", ContractExpireDateHandler),
|
(r"/api/data/contract_expire_date", ContractExpireDateHandler),
|
||||||
(r"/api/data/option_undl_data", OptionUndlDataHandler),
|
(r"/api/data/option_undl_data", OptionUndlDataHandler),
|
||||||
(r"/api/data/financial_data", FinancialDataHandler),
|
(r"/api/data/financial_data", FinancialDataHandler),
|
||||||
(r"/api/data/factor_data", FactorDataHandler),
|
(r"/api/data/factor_data", FactorDataHandler),
|
||||||
(r"/api/data/his_st_data", HisStDataHandler),
|
|
||||||
(r"/api/data/his_index_data", HisIndexDataHandler),
|
(r"/api/data/his_index_data", HisIndexDataHandler),
|
||||||
(r"/api/data/all_subscription", AllSubscriptionHandler),
|
(r"/api/data/all_subscription", AllSubscriptionHandler),
|
||||||
(r"/api/data/option_list", OptionListHandler),
|
(r"/api/data/option_list", OptionListHandler),
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ from .errors import APIError, BusinessError
|
|||||||
from .misc import MiscMixin
|
from .misc import MiscMixin
|
||||||
from .models import *
|
from .models import *
|
||||||
from .trade import *
|
from .trade import *
|
||||||
|
from .v2 import Client as V2Client, Portfolio
|
||||||
|
|
||||||
|
|
||||||
class Client(AccountMixin, DataMixin, TradeMixin, MiscMixin, _HTTPClient):
|
class Client(AccountMixin, DataMixin, TradeMixin, MiscMixin, _HTTPClient):
|
||||||
"""big-qmt 同步 HTTP 客户端。"""
|
"""big-qmt 同步 HTTP 客户端。"""
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Client", "APIError", "BusinessError", "OP_BUY", "OP_SELL", "ORDER_TYPE_VOLUME", "PR_TYPE_LATEST", "QUICK_TRADE_NOW", "ORDER_SIDE_BY_OFFSET", "OrderItem", "PositionItem", "parse_order"]
|
__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"]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
py-client/sdk/__pycache__/v2.cpython-311.pyc
Normal file
BIN
py-client/sdk/__pycache__/v2.cpython-311.pyc
Normal file
Binary file not shown.
@@ -7,7 +7,7 @@ class AccountMixin:
|
|||||||
account_type: str
|
account_type: str
|
||||||
|
|
||||||
def _positions(self, path: str) -> tuple[list[str], list[PositionItem]]:
|
def _positions(self, path: str) -> tuple[list[str], list[PositionItem]]:
|
||||||
payload = self._post(path, {"account": self.account_type}) or {}
|
payload = self._post_json(path, {"account": self.account_type}) or {}
|
||||||
raw = payload.get("data", payload) if isinstance(payload, dict) else payload
|
raw = payload.get("data", payload) if isinstance(payload, dict) else payload
|
||||||
if isinstance(raw, list):
|
if isinstance(raw, list):
|
||||||
positions = [PositionItem.from_trade_detail(item) for item in raw]
|
positions = [PositionItem.from_trade_detail(item) for item in raw]
|
||||||
@@ -18,21 +18,21 @@ class AccountMixin:
|
|||||||
def holding(self): return self._positions("/api/holding")
|
def holding(self): return self._positions("/api/holding")
|
||||||
|
|
||||||
def assets(self) -> Assets:
|
def assets(self) -> Assets:
|
||||||
payload = self._post("/api/v2/assets", {"account": self.account_type}) or {}
|
payload = self._post_json("/api/v2/assets", {"account": self.account_type}) or {}
|
||||||
data = payload.get("data", payload) if isinstance(payload, dict) else {}
|
data = payload.get("data", payload) if isinstance(payload, dict) else {}
|
||||||
return Assets.from_dict(data)
|
return Assets.from_dict(data)
|
||||||
|
|
||||||
def total_money(self) -> float: return float(self._post("/api/money/total", {"account": self.account_type}).get("total_money", 0))
|
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("/api/money/available", {"account": self.account_type}).get("available_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 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 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):
|
def _order(self, path, stock, price, volume, pr_type):
|
||||||
body = {"stock": stock, "price": price, "volume": volume}
|
body = {"stock": stock, "price": price, "volume": volume}
|
||||||
if pr_type: body["prType"] = pr_type
|
if pr_type: body["prType"] = pr_type
|
||||||
return self._post(path, body)
|
return self._post_json(path, body)
|
||||||
|
|
||||||
def order_status_list(self): return self._post("/api/order/status", {"account": self.account_type}).get("orders", [])
|
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("/api/order/cancel_all", {"account": self.account_type})
|
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("/api/order/cancel_order", {"stock": stock, "volume": volume, "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("/api/order/deal", {"account": self.account_type}).get("deals", [])
|
def deals(self): return self._post_json("/api/order/deal", {"account": self.account_type}).get("deals", [])
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from dataclasses import asdict, is_dataclass
|
from dataclasses import asdict, is_dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ class Client:
|
|||||||
self.account_type = account_type
|
self.account_type = account_type
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _request(self, method: str, path: str, body: Any = None) -> Any:
|
def _request_bytes(self, method: str, path: str, body: Any = None) -> bytes:
|
||||||
if is_dataclass(body):
|
if is_dataclass(body):
|
||||||
body = asdict(body)
|
body = asdict(body)
|
||||||
attempts = 2 if _is_idempotent(method, path) else 1
|
attempts = 2 if _is_idempotent(method, path) else 1
|
||||||
@@ -56,30 +57,45 @@ class Client:
|
|||||||
assert response is not None
|
assert response is not None
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
try:
|
try:
|
||||||
message = response.json().get("error", response.text)
|
message = response.text
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
message = response.text.strip()
|
message = response.text.strip()
|
||||||
raise APIError(response.status_code, str(message))
|
raise APIError(response.status_code, str(message))
|
||||||
if not response.content:
|
if not response.content:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
def _get_bytes(self, path: str) -> bytes:
|
||||||
|
return self._request_bytes("GET", path)
|
||||||
|
|
||||||
|
def _post_bytes(self, path: str, body: Any = None) -> bytes:
|
||||||
|
return self._request_bytes("POST", path, {} if body is None else body)
|
||||||
|
|
||||||
|
def _get_json(self, path: str) -> Any:
|
||||||
|
content = self._get_bytes(path)
|
||||||
|
return self._decode_json(path, content)
|
||||||
|
|
||||||
|
def _post_json(self, path: str, body: Any = None) -> Any:
|
||||||
|
content = self._post_bytes(path, body)
|
||||||
|
return self._decode_json(path, content)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _decode_json(path: str, content: bytes) -> Any:
|
||||||
|
if not content:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return response.json()
|
return json.loads(content)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"invalid JSON from {path}: {response.content[:512]!r}"
|
f"invalid JSON from {path}: {content[:512]!r}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
def _get(self, path: str) -> Any:
|
|
||||||
return self._request("GET", path)
|
|
||||||
|
|
||||||
def _post(self, path: str, body: Any = None) -> Any:
|
|
||||||
return self._request("POST", path, {} if body is None else body)
|
|
||||||
|
|
||||||
def _get_field(self, path: str, key: str) -> Any:
|
def _get_field(self, path: str, key: str) -> Any:
|
||||||
return self._get(path).get(key)
|
return self._get_json(path).get(key)
|
||||||
|
|
||||||
def _post_field(self, path: str, body: Any, key: str) -> Any:
|
def _post_field(self, path: str, body: Any, key: str) -> Any:
|
||||||
result = self._post(path, body)
|
result = self._post_json(path, body)
|
||||||
if isinstance(result, dict) and result.get("error"):
|
if isinstance(result, dict) and result.get("error"):
|
||||||
raise BusinessError(result["error"])
|
raise BusinessError(result["error"])
|
||||||
return result.get(key, result) if key and isinstance(result, dict) else result
|
return result.get(key, result) if key and isinstance(result, dict) else result
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ class DataMixin:
|
|||||||
def last_volume(self, code): return self._one("last_volume", "stockcode", code, "last_volume")
|
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 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 tick_timetag(self): return self._get_field("/api/data/tick_timetag", "timetag")
|
||||||
def sector(self, sector, realtime): return self._post("/api/data/sector", {"sector": sector, "realtime": realtime}).get("stocks", [])
|
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("/api/data/industry", {"industry": industry}).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("/api/data/stock_list_in_sector", {"sectorname": name}).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 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 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 risk_free_rate(self, index): return self._one("risk_free_rate", "index", index, "risk_free_rate")
|
||||||
@@ -28,7 +28,7 @@ class DataMixin:
|
|||||||
def market_data_ex(self, req): return self._post_field("/api/data/market_data_ex", 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):
|
def full_tick(self, stocks):
|
||||||
raw = self._post("/api/data/full_tick", {"stocks": stocks}) or {}
|
raw = self._post_json("/api/data/full_tick", {"stocks": stocks}) or {}
|
||||||
def number(data, *names):
|
def number(data, *names):
|
||||||
for name in names:
|
for name in names:
|
||||||
try: return float(data[name])
|
try: return float(data[name])
|
||||||
@@ -46,7 +46,7 @@ class DataMixin:
|
|||||||
def trading_dates(self, stockcode, start_date, end_date, period, count=0):
|
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)
|
body = locals_body(stockcode=stockcode, start_date=start_date, end_date=end_date, period=period)
|
||||||
if count: body["count"] = count
|
if count: body["count"] = count
|
||||||
return self._post("/api/data/trading_dates", body).get("dates", [])
|
return self._post_json("/api/data/trading_dates", body).get("dates", [])
|
||||||
def svol(self, code): return self._one("svol", "stockcode", code, "svol")
|
def svol(self, code): return self._one("svol", "stockcode", code, "svol")
|
||||||
def bvol(self, code): return self._one("bvol", "stockcode", code, "bvol")
|
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 longhubang(self, stocks, start, end): return self._post_field("/api/data/longhubang", {"stock_list": csv_join(stocks), "startTime": start, "endTime": end}, "data")
|
||||||
@@ -73,8 +73,8 @@ class DataMixin:
|
|||||||
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")
|
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 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 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("/api/data/subscribe_quote", {"stock_code": code, "period": period, "dividend_type": dividend_type})
|
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("/api/data/unsubscribe_quote", {"sub_id": sub_id})
|
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 locals_body(**kwargs): return kwargs
|
||||||
|
|||||||
@@ -27,5 +27,5 @@ class MiscMixin:
|
|||||||
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 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_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 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("/api/sys/python_version")
|
def python_version(self): return self._get_json("/api/sys/python_version")
|
||||||
def shutdown(self): return self._post("/api/sys/shutdown", {})
|
def shutdown(self): return self._post_json("/api/sys/shutdown", {})
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TradeMixin:
|
|||||||
body = {"opType": op_type, "stock": stock, "price": price, "volume": volume}
|
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)):
|
for key, value in (("orderType", order_type), ("prType", pr_type), ("quickTrade", quick_trade), ("strategyName", strategy_name)):
|
||||||
if value: body[key] = value
|
if value: body[key] = value
|
||||||
return self._post("/api/trade/passorder", body)
|
return self._post_json("/api/trade/passorder", body)
|
||||||
|
|
||||||
def passorder_latest(self, side, stock, volume): return self.passorder_latest_tagged(side, stock, 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):
|
def passorder_latest_tagged(self, side, stock_code, volume, strategy_name, order_id):
|
||||||
@@ -29,13 +29,13 @@ class TradeMixin:
|
|||||||
"strategyName": strategy_name,
|
"strategyName": strategy_name,
|
||||||
"orderId": order_id,
|
"orderId": order_id,
|
||||||
}
|
}
|
||||||
return self._post("/api/trade/passorder", body)
|
return self._post_json("/api/trade/passorder", body)
|
||||||
|
|
||||||
def algo_passorder(self, **kwargs): return self._post("/api/trade/algo_passorder", kwargs)
|
def algo_passorder(self, **kwargs): return self._post_json("/api/trade/algo_passorder", kwargs)
|
||||||
def smart_algo_passorder(self, **kwargs): return self._post("/api/trade/smart_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):
|
def _style_order(self, path, stock, value_key, value, style, price):
|
||||||
return self._post(path, {"stock": stock, value_key: value, "style": style, "price": 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_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_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_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_percent", stock, "percent", percent, style, price)
|
||||||
@@ -51,14 +51,14 @@ class TradeMixin:
|
|||||||
def futures_sell_close_tdayfirst(self, *args): return self._future("sell_close_tdayfirst", *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 futures_sell_close_ydayfirst(self, *args): return self._future("sell_close_ydayfirst", *args)
|
||||||
|
|
||||||
def _task(self, action, task_id): return self._post(f"/api/trade/{action}_task", {"taskId": task_id, "accountType": self.account_type})
|
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 cancel_task(self, task_id): return self._task("cancel", task_id)
|
||||||
def pause_task(self, task_id): return self._task("pause", 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 resume_task(self, task_id): return self._task("resume", task_id)
|
||||||
def do_order(self): return self._post("/api/trade/do_order")
|
def do_order(self): return self._post_json("/api/trade/do_order")
|
||||||
def trade_detail_data(self, datatype):
|
def trade_detail_data(self, datatype):
|
||||||
datatype = str(datatype).strip().lower()
|
datatype = str(datatype).strip().lower()
|
||||||
data = self._post(
|
data = self._post_json(
|
||||||
"/api/trade/trade_detail_data",
|
"/api/trade/trade_detail_data",
|
||||||
{"account": self.account_type, "datatype": datatype},
|
{"account": self.account_type, "datatype": datatype},
|
||||||
).get("data", [])
|
).get("data", [])
|
||||||
@@ -70,13 +70,13 @@ class TradeMixin:
|
|||||||
if datatype == "account":
|
if datatype == "account":
|
||||||
return [Assets.from_dict(row) for row in rows]
|
return [Assets.from_dict(row) for row in rows]
|
||||||
return data
|
return data
|
||||||
def value_by_order_id(self, order_id, datatype): return self._post("/api/trade/value_by_order_id", {"orderId": order_id, "accountType": self.account_type, "datatype": datatype}).get("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("/api/trade/last_order_id", {"account": self.account_type, "datatype": datatype}).get("last_order_id")
|
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("/api/trade/can_cancel_order", {"orderId": order_id, "accountType": self.account_type}).get("can_cancel")
|
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("/api/order/cancel_by_id", {"order_id": order_id, "account_type": self.account_type})
|
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 debt_contract(self): return self._contract("debt_contract")
|
||||||
def assure_contract(self): return self._contract("assure_contract")
|
def assure_contract(self): return self._contract("assure_contract")
|
||||||
def enable_short_contract(self): return self._contract("enable_short_contract")
|
def enable_short_contract(self): return self._contract("enable_short_contract")
|
||||||
def _contract(self, name): return self._post(f"/api/trade/{name}").get("data", [])
|
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 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 new_purchase_limit(self): return self._post_field("/api/trade/new_purchase_limit", None, "data")
|
||||||
|
|||||||
86
py-client/sdk/v2.py
Normal file
86
py-client/sdk/v2.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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"]
|
||||||
@@ -1,25 +1,15 @@
|
|||||||
import json
|
from sdk.v2 import Client_V2
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from sdk import Client
|
|
||||||
|
|
||||||
BASE_URL = "http://127.0.0.1:10086"
|
BASE_URL = "http://127.0.0.1:10086"
|
||||||
TOKEN = "QMTbyYanweidong"
|
TOKEN = "QMTbyYanweidong"
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
client = Client(BASE_URL, TOKEN).set_account_type("stock")
|
with Client_V2(BASE_URL, TOKEN) as client:
|
||||||
assets = client.assets();
|
portfolio = client.portfolio()
|
||||||
_, positions = client.positions()
|
print(portfolio)
|
||||||
print(f"总资产:{assets.total:.2f}元,可用资金:{assets.available:.2f}元")
|
|
||||||
for p in sorted(positions, key=lambda item: item.stock_code):
|
|
||||||
if p.volume > 0: print(f"{p.stock_code} {p.stock_name} 持仓={p.volume} 可用={p.can_use_volume} 成本={p.open_price:.2f} 现价={p.last_price:.2f}")
|
|
||||||
data_dir = os.environ.get("QMT_DATA_DIR", "").strip()
|
|
||||||
if not data_dir: raise SystemExit("环境变量 QMT_DATA_DIR 为空")
|
|
||||||
codes = json.loads((Path(data_dir) / "pass_codes.json").read_text(encoding="utf-8"))
|
|
||||||
for code, tick in sorted(client.full_tick(codes).items()):
|
|
||||||
print(f"{code} last={tick.last_price:.2f} close={tick.last_close:.2f}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": main()
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user