This commit is contained in:
2026-09-06 11:34:23 +08:00
parent b38ff12f2a
commit ac9e9193ad
32 changed files with 235 additions and 61 deletions

View File

@@ -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),

View File

@@ -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():
@@ -257,14 +388,16 @@ def make_app():
(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,10 +419,8 @@ 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)
codes = get_pass_codes(ContextInfo.accountID)
ContextInfo.set_universe(list(codes))
# Api App

View File

@@ -15,7 +15,7 @@ from sdk import OrderItem, PositionItem
PENDING_TIME_OUT = 3600
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class StateItem:
code: str
base_order_id: str = ""
@@ -27,7 +27,7 @@ class StateItem:
added_cost: float = 0.0
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class PendingOrder:
order_id: str
code: str

View File

@@ -7,7 +7,7 @@ from pathlib import Path
import yaml
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class SignalConfig:
"""单个交易信号的数据源及开仓限制配置。"""
@@ -21,7 +21,7 @@ class SignalConfig:
gt_last_price_is_open: bool = False
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class GlobalConfig:
"""所有主机共享的系统配置。"""
@@ -37,7 +37,7 @@ class GlobalConfig:
signals: dict[str, SignalConfig] = field(default_factory=dict)
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class AccountConfig:
"""当前主机所使用的账户及交易策略参数。"""

View File

@@ -20,7 +20,7 @@ class GridState(str, Enum):
STEADY = "steady" # 仍处于当前峰值网格,继续持有
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class GridObservation:
"""一次网格观察的不可变结果。"""

View File

@@ -4,12 +4,12 @@ import secrets
from .http import get_json
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class SignalItem:
signal_key: str = ""; code: str = ""; name: str = ""; desc: str = ""; last_close: float = 0
tech_indicator: dict[str, float] = field(default_factory=dict)
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class SignalResult:
code: str = ""; total: int = 0; updated: str = ""; data: dict[str, SignalItem] = field(default_factory=dict); message: str = ""

View File

@@ -38,7 +38,7 @@ from strategy.trend.boot import StartTrend
from strategy.zt.boot import StartZT
from strategy.ipo import AutoBuyIpo
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class StrategyDefinition:
mutex_scope: str
start_strategy: object

View File

@@ -11,7 +11,7 @@ def _number(value: Any, kind: type = float) -> Any:
return kind()
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class OrderItem:
"""由 QMT 委托明细解析得到的标准订单记录。"""
id: str
@@ -64,13 +64,14 @@ class OrderItem:
)
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class PositionItem:
stock_code: str = ""
stock_name: str = ""
trade_id:str = ""
direction: Any = None
volume: int = 0
open_cost: float = 0.0
open_price: float = 0.0
float_profit: float = 0.0
market_value: float = 0.0
@@ -88,7 +89,7 @@ class PositionItem:
def from_dict(cls, data: dict[str, Any], code: str = "") -> "PositionItem":
return cls(
stock_code=str(data.get("StockCode") or code), stock_name=str(data.get("StockName") or ""),
trade_id=str(data.get("TradeID") or ""),
trade_id=str(data.get("TradeID") or ""),open_cost=_number(data.get("OpenCost")),
direction=data.get("Direction"), volume=_number(data.get("Volume"), int),
open_price=_number(data.get("OpenPrice")), float_profit=_number(data.get("FloatProfit")),
market_value=_number(data.get("MarketValue")), stock_holder=str(data.get("StockHolder") or ""),
@@ -107,6 +108,7 @@ class PositionItem:
trade_id=str(data.get("TradeID") or ""),
direction=data.get("Direction"),
volume=_number(data.get("Volume"), int),
open_cost=_number(data.get("OpenCost")),
open_price=_number(data.get("OpenPrice")),
float_profit=_number(data.get("FloatProfit")),
market_value=_number(data.get("MarketValue")),
@@ -122,7 +124,7 @@ class PositionItem:
)
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class Assets:
total: float = 0.0
available: float = 0.0
@@ -136,7 +138,7 @@ class Assets:
)
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class Portfolio:
assets: Assets
positions: dict[str, PositionItem]
@@ -152,7 +154,7 @@ def _trade_datetime(data: dict[str, Any]) -> datetime | None:
return None
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class Tick:
last_price: float = 0.0
last_close: float = 0.0
@@ -169,7 +171,7 @@ class Tick:
)
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class HistoryDataRequest:
length: int = 10
period: str = ""
@@ -178,7 +180,7 @@ class HistoryDataRequest:
skip_paused: bool = True
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class MarketDataRequest:
fields: list[str] = field(default_factory=list)
stocks: list[str] = field(default_factory=list)
@@ -189,7 +191,7 @@ class MarketDataRequest:
count: int = 0
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class FinancialDataRequest:
tabname: str = ""; colname: str = ""; market: str = ""; code: str = ""
report_type: str = ""; barpos: int = 0
@@ -197,22 +199,22 @@ class FinancialDataRequest:
start_date: str = ""; end_date: str = ""
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class FactorDataRequest:
field_list: list[str] = field(default_factory=list); stock_list: list[str] = field(default_factory=list)
stock_code: str = ""; start_date: str = ""; end_date: str = ""
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class BSMPriceRequest:
option_type: str; object_prices: Any; strike_price: float; risk_free: float; sigma: float; days: int; dividend: float
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class BSMIVRequest:
option_type: str; object_prices: float; strike_price: float; option_price: float; risk_free: float; days: int; dividend: float
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class LocalDataRequest:
stock_code: str; start_time: str = ""; end_time: str = ""; period: str = ""; divid_type: str = ""; count: int = 0

View File

@@ -6,6 +6,14 @@ from .models import Assets, OrderItem, Portfolio, PositionItem
class PortfolioMixin:
def org(self, datatype: str) -> list[dict[str, Any]]:
"""查询 account、order、deal 或 position返回原始字段字典列表。"""
datatype = str(datatype).strip().lower()
if datatype not in {"account", "order", "deal", "position"}:
raise ValueError(f"unsupported org datatype: {datatype}")
response = self._get_json(f"/api/portfolio/org/{datatype}")
return response["data"]
def portfolio(self) -> Portfolio:
data = self._get_json("/api/portfolio") or {}
positions = {

View File

@@ -39,7 +39,7 @@ def Overview(assets, positions, account_cfg=None) -> None:
for position in positions:
if position.volume <= 0:
continue
log.info("[启动] %s %s,持仓=%d,可用=%d,成本=%.2f,现价=%.2f,盈亏=%.2f%%", position.stock_code, position.stock_name, position.volume, position.can_use_volume, position.open_price, position.last_price, position.profit_rate * 100)
log.info("[启动] %s %s %s,持仓=%d,可用=%d,成本=%.2f(%.2f),现价=%.2f,盈亏=%.2f%%",position.trade_id, position.stock_code, position.stock_name, position.volume, position.can_use_volume, position.open_price,position.open_cost, position.last_price, position.profit_rate * 100)

View File

@@ -19,7 +19,7 @@ TRACKED_STATUSES = BUSY_STATUSES | COMPLETED_STATUSES
CANCELABLE_STATUSES = {"49", "50", "51", "52"}
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class PlaceOrderRequest:
"""``OrderBook.place`` 提交委托所需的全部参数。"""
op: int

View File

@@ -15,7 +15,7 @@ import logging as log
LOSS_TIERS = [-50.0]
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class TradeDecision:
"""一次止盈或补仓判断的统一结果。"""

View File

@@ -13,7 +13,7 @@ from .order import OrderBook
from .watch import DipWatch
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class Runtime:
"""集中保存趋势策略运行期间共享的依赖和状态。

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from threading import Lock
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class _Entry:
last_close: float
expires_at: datetime

View File

@@ -11,7 +11,7 @@ from strategy.trend.watch import DipWatch
from .state import TState
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class Runtime:
client: Client
global_cfg: GlobalConfig

View File

@@ -17,7 +17,7 @@ BUYING = "BUYING"
DONE = "DONE"
@dataclass(frozen=True, slots=True)
@dataclass(slots=True)
class TStateItem:
code: str
base_qty: int = 0

View File

@@ -1,4 +1,5 @@
from datetime import datetime
import json
from sdk import Client
@@ -8,8 +9,20 @@ TOKEN = "QMTbyYanweidong"
STOCK_CODE = "000021.SZ"
VOLUME = 100
def main() -> None:
"""只读查询四类原始数据,输出 JSON 便于核对字段。"""
with Client(BASE_URL, TOKEN) as client:
for datatype in ("account", "order", "deal", "position"):
print(f"\n=== {datatype} ===")
try:
result = client.org(datatype)
print(f"记录数:{len(result)}")
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
except Exception as exc:
print(f"查询失败:{exc}")
def main1() -> None:
order = {
"opType": 23,
"orderType": 1101,