fix bug
This commit is contained in:
@@ -186,6 +186,24 @@ class HoldingHandler(BaseHandler):
|
||||
holding = format_holding(positions)
|
||||
self.write_json({"data": holding})
|
||||
|
||||
class OrgHandler(BaseHandler):
|
||||
def get(self, handler_type):
|
||||
result = safe_call(
|
||||
get_trade_detail_data, self.acc(), 'stock', handler_type
|
||||
) or []
|
||||
|
||||
data = []
|
||||
for item in result:
|
||||
fields = {}
|
||||
for name in dir(item):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
value = getattr(item, name)
|
||||
if not callable(value):
|
||||
fields[name] = value
|
||||
data.append(fields)
|
||||
self.write_json({"data": data})
|
||||
|
||||
# get_trade_detail_data('account') - Query account assets
|
||||
class AssetsHandler(BaseHandler):
|
||||
def get(self):
|
||||
@@ -318,6 +336,7 @@ def format_holding(positions):
|
||||
'Direction': position.m_nDirection,
|
||||
'Volume': position.m_nVolume,
|
||||
'OpenPrice': position.m_dOpenPrice,
|
||||
'OpenCost':position.m_dOpenCost,
|
||||
'FloatProfit': position.m_dFloatProfit,
|
||||
'MarketValue': position.m_dMarketValue,
|
||||
'StockHolder': position.m_strStockHolder,
|
||||
@@ -372,6 +391,7 @@ def make_app():
|
||||
# Portfolio
|
||||
(r"/api/portfolio", PortfolioHandler),
|
||||
(r"/api/portfolio/positions", HoldingHandler),
|
||||
(r"/api/portfolio/org/(account|order|deal|position)",OrgHandler),
|
||||
(r"/api/portfolio/assets", AssetsHandler),
|
||||
(r"/api/portfolio/order", OrderHandler),
|
||||
(r"/api/portfolio/deal", DealHandler),
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from urllib.request import Request, urlopen
|
||||
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')
|
||||
DATA_DIR = os.environ.get('QMT_DATA_DIR', r'D:\qmt_strategy_data')
|
||||
TOKEN="QMTbyYanweidong"
|
||||
PORT = 10086
|
||||
PASS_CODES_URL = "http://139.224.247.176:13499/a/pass_codes"
|
||||
|
||||
# ===================================
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -32,6 +33,36 @@ def safe_call(func, *args, **kwargs):
|
||||
) from e
|
||||
|
||||
|
||||
def get_pass_codes(account_id):
|
||||
request = Request(
|
||||
PASS_CODES_URL,
|
||||
headers={"Accept": "application/json", "User-Agent": "big-qmt/1"},
|
||||
)
|
||||
with urlopen(request, timeout=10) as response:
|
||||
payload = json.load(response)
|
||||
|
||||
remote_codes = payload.get("data")
|
||||
if not isinstance(remote_codes, list):
|
||||
raise ValueError("pass_codes response data must be an array")
|
||||
|
||||
positions = safe_call(
|
||||
get_trade_detail_data, account_id, 'stock', 'position'
|
||||
) or []
|
||||
position_codes = [
|
||||
position.m_strInstrumentID + '.' + position.m_strExchangeID
|
||||
for position in positions
|
||||
]
|
||||
|
||||
codes = []
|
||||
seen = set()
|
||||
for code in remote_codes + position_codes:
|
||||
code = str(code).strip()
|
||||
if code and code not in seen:
|
||||
seen.add(code)
|
||||
codes.append(code)
|
||||
return codes
|
||||
|
||||
|
||||
# ============= BaseHandler =============
|
||||
|
||||
AUTH_EXEMPT = set()
|
||||
@@ -61,6 +92,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 +119,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,25 +157,26 @@ 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
|
||||
@@ -144,13 +184,31 @@ 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))
|
||||
self.write_json({"data": holding})
|
||||
|
||||
class OrgHandler(BaseHandler):
|
||||
def get(self, handler_type):
|
||||
result = safe_call(
|
||||
get_trade_detail_data, self.acc(), 'stock', handler_type
|
||||
) or []
|
||||
|
||||
data = []
|
||||
for item in result:
|
||||
fields = {}
|
||||
for name in dir(item):
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
value = getattr(item, name)
|
||||
if not callable(value):
|
||||
fields[name] = value
|
||||
data.append(fields)
|
||||
self.write_json({"data": data})
|
||||
|
||||
# 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))
|
||||
self.write_json(format_assets(_data))
|
||||
|
||||
class OrderHandler(BaseHandler):
|
||||
def get(self):
|
||||
@@ -158,13 +216,13 @@ class OrderHandler(BaseHandler):
|
||||
if ret is None:
|
||||
ret = []
|
||||
result = [fixed_fields(obj) for obj in ret]
|
||||
self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False))
|
||||
self.write_json(result)
|
||||
|
||||
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))
|
||||
self.write_json({"deals": rets})
|
||||
|
||||
# ContextInfo.get_full_tick() - Get full tick data
|
||||
class FullTickHandler(BaseHandler):
|
||||
@@ -176,7 +234,7 @@ 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)
|
||||
|
||||
# passorder() - Submit a general trading order
|
||||
class PassorderHandler(BaseHandler):
|
||||
@@ -204,14 +262,14 @@ class PassorderHandler(BaseHandler):
|
||||
logger.exception("passorder failed")
|
||||
raise HTTPError(502, reason="QMT order submission failed") from e
|
||||
|
||||
self.write(json.dumps({
|
||||
self.write_json({
|
||||
"status": "success",
|
||||
"opType": opType,
|
||||
"stockCode": stockCode,
|
||||
"strategy_name": strategy_name,
|
||||
"local_order_id": order_id,
|
||||
"order_ref": str(order_ref)
|
||||
}, separators=(',', ':'), ensure_ascii=False))
|
||||
})
|
||||
|
||||
|
||||
class CancelByIdHandler(BaseHandler):
|
||||
@@ -223,21 +281,28 @@ class CancelByIdHandler(BaseHandler):
|
||||
raise HTTPError(400, "order_id cannot be empty")
|
||||
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(), 'stock', self.ctx())
|
||||
self.write(json.dumps({
|
||||
self.write_json({
|
||||
"status": "success" if result is not False else "failed",
|
||||
"order_id": order_id,
|
||||
}, 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', 'STOCK')
|
||||
ret = safe_call(get_ipo_data, typ)
|
||||
self.write_json(ret)
|
||||
|
||||
# sys: Python version information
|
||||
class PythonVersionHandler(BaseHandler):
|
||||
def get(self):
|
||||
import sys
|
||||
version_info = {
|
||||
"python_version": sys.version,
|
||||
"python_version_info": {
|
||||
@@ -248,7 +313,73 @@ class PythonVersionHandler(BaseHandler):
|
||||
"serial": sys.version_info.serial,
|
||||
}
|
||||
}
|
||||
self.write(json.dumps(version_info, separators=(',', ':'), ensure_ascii=False))
|
||||
self.write_json(version_info)
|
||||
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
def format_holding(positions):
|
||||
holding = {}
|
||||
for position in positions:
|
||||
stock = position.m_strInstrumentID + '.' + position.m_strExchangeID
|
||||
holding[stock] = {
|
||||
'StockCode': stock,
|
||||
'TradeID':position.m_strTradeID,
|
||||
'StockName': position.m_strInstrumentName,
|
||||
'Direction': position.m_nDirection,
|
||||
'Volume': position.m_nVolume,
|
||||
'OpenPrice': position.m_dOpenPrice,
|
||||
'OpenCost':position.m_dOpenCost,
|
||||
'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
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def fixed_fields(obj, fields=TRADE_DETAIL_FIELDS):
|
||||
result = {}
|
||||
for field in fields:
|
||||
try:
|
||||
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)
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
# ============= Route registration =============
|
||||
def make_app():
|
||||
@@ -256,15 +387,17 @@ def make_app():
|
||||
# ContextInfo properties
|
||||
(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
|
||||
|
||||
# Portfolio
|
||||
(r"/api/portfolio", PortfolioHandler),
|
||||
(r"/api/portfolio/positions", HoldingHandler),
|
||||
(r"/api/portfolio/org/(account|order|deal|position)",OrgHandler),
|
||||
(r"/api/portfolio/assets", AssetsHandler),
|
||||
(r"/api/portfolio/order", OrderHandler),
|
||||
(r"/api/portfolio/deal", DealHandler),
|
||||
|
||||
|
||||
(r"/api/data/full_tick", FullTickHandler),
|
||||
(r"/api/trade/ipo_data", IpoDataHandler),
|
||||
(r"/api/trade/cancel_by_id", CancelByIdHandler),
|
||||
(r"/api/trade/passorder", PassorderHandler),
|
||||
|
||||
@@ -286,11 +419,9 @@ def init(ContextInfo):
|
||||
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))
|
||||
|
||||
codes = get_pass_codes(ContextInfo.accountID)
|
||||
ContextInfo.set_universe(list(codes))
|
||||
|
||||
# Api App
|
||||
app = make_app()
|
||||
|
||||
Reference in New Issue
Block a user