This commit is contained in:
2026-09-03 11:33:43 +08:00
parent d5303cc22b
commit e6a5096353
38 changed files with 1911 additions and 2742 deletions

File diff suppressed because it is too large Load Diff

1452
api/qmt_rest_old.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,27 @@
from .account import AccountMixin from .client import Client
from .client import Client as _HTTPClient
from .data import DataMixin
from .errors import APIError, BusinessError from .errors import APIError, BusinessError
from .misc import MiscMixin from .models import Assets, OrderItem, Portfolio, PositionItem, Tick
from .models import * from .trade import (
from .trade import * OP_BUY,
from .v2 import Client as V2Client, Portfolio OP_SELL,
ORDER_SIDE_BY_OFFSET,
ORDER_TYPE_VOLUME,
class Client(AccountMixin, DataMixin, TradeMixin, MiscMixin, _HTTPClient): PR_TYPE_LATEST,
"""big-qmt 同步 HTTP 客户端。""" QUICK_TRADE_NOW,
)
__all__ = [
__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"] "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",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,38 +0,0 @@
from typing import Any
from .models import Assets, PositionItem
class AccountMixin:
account_type: str
def _positions(self, path: str) -> tuple[list[str], list[PositionItem]]:
payload = self._post_json(path, {"account": self.account_type}) or {}
raw = payload.get("data", payload) if isinstance(payload, dict) else payload
if isinstance(raw, list):
positions = [PositionItem.from_trade_detail(item) for item in raw]
return [item.stock_code for item in positions], positions
return list(raw), [PositionItem.from_dict(value, code) for code, value in raw.items()]
def positions(self): return self._positions("/api/v2/positions")
def holding(self): return self._positions("/api/holding")
def assets(self) -> Assets:
payload = self._post_json("/api/v2/assets", {"account": self.account_type}) or {}
data = payload.get("data", payload) if isinstance(payload, dict) else {}
return Assets.from_dict(data)
def total_money(self) -> float: return float(self._post_json("/api/money/total", {"account": self.account_type}).get("total_money", 0))
def available_money(self) -> float: return float(self._post_json("/api/money/available", {"account": self.account_type}).get("available_money", 0))
def buy(self, stock: str, price: float, volume: int, pr_type: int = 0): return self._order("/api/order/buy", stock, price, volume, pr_type)
def sell(self, stock: str, price: float, volume: int, pr_type: int = 0): return self._order("/api/order/sell", stock, price, volume, pr_type)
def _order(self, path, stock, price, volume, pr_type):
body = {"stock": stock, "price": price, "volume": volume}
if pr_type: body["prType"] = pr_type
return self._post_json(path, body)
def order_status_list(self): return self._post_json("/api/order/status", {"account": self.account_type}).get("orders", [])
def cancel_all(self): return self._post_json("/api/order/cancel_all", {"account": self.account_type})
def cancel_by_rule(self, stock: str, volume: int): return self._post_json("/api/order/cancel_order", {"stock": stock, "volume": volume, "account": self.account_type})
def deals(self): return self._post_json("/api/order/deal", {"account": self.account_type}).get("deals", [])

View File

@@ -6,14 +6,16 @@ from typing import Any
import httpx 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: class HTTPClient:
return ",".join(item.strip() for item in items if item.strip())
class Client:
"""复用连接池的同步 QMT HTTP 客户端。""" """复用连接池的同步 QMT HTTP 客户端。"""
def __init__(self, base_url: str, token: str, timeout: float = 15.0) -> None: def __init__(self, base_url: str, token: str, timeout: float = 15.0) -> None:
@@ -31,13 +33,13 @@ class Client:
def close(self) -> None: def close(self) -> None:
self.http.close() self.http.close()
def __enter__(self) -> "Client": def __enter__(self) -> "HTTPClient":
return self return self
def __exit__(self, *_args: object) -> None: def __exit__(self, *_args: object) -> None:
self.close() self.close()
def set_account_type(self, account_type: str) -> "Client": def set_account_type(self, account_type: str) -> "HTTPClient":
if account_type.strip(): if account_type.strip():
self.account_type = account_type self.account_type = account_type
return self return self
@@ -91,28 +93,19 @@ class Client:
f"invalid JSON from {path}: {content[:512]!r}" f"invalid JSON from {path}: {content[:512]!r}"
) from exc ) 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: def _is_idempotent(method: str, path: str) -> bool:
if method == "GET": if method == "GET":
return True return True
prefixes = ( return path == "/api/data/full_tick"
"/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)

8
py-client/sdk/context.py Normal file
View File

@@ -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 {}

View File

@@ -1,83 +1,9 @@
from dataclasses import asdict from __future__ import annotations
from typing import Any
from .client import csv_join from .models import Tick
from .models import *
class DataMixin: class DataMixin:
def _one(self, endpoint, arg, value, key): return self._post_field(f"/api/data/{endpoint}", {arg: value}, key) def full_tick(self, stocks: list[str]) -> dict[str, Tick]:
data = self._post_json("/api/data/full_tick", {"stocks": stocks}) or {}
def stock_name(self, code): return self._one("stock_name", "stockcode", code, "name") return {code: Tick.from_dict(value) for code, value in data.items()}
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"]}

44
py-client/sdk/get.py Normal file
View File

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

View File

@@ -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", {})

View File

@@ -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: def _trade_datetime(data: dict[str, Any]) -> datetime | None:
date = str(data.get("m_strInsertDate") or "") date = str(data.get("m_strInsertDate") or "")
clock = str(data.get("m_strInsertTime") or "").replace(":", "").zfill(6) clock = str(data.get("m_strInsertTime") or "").replace(":", "").zfill(6)
@@ -148,6 +155,16 @@ class Tick:
last_close: float = 0.0 last_close: float = 0.0
raw: dict[str, Any] = field(default_factory=dict) 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) @dataclass(slots=True)
class HistoryDataRequest: class HistoryDataRequest:

View File

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

8
py-client/sdk/sys.py Normal file
View File

@@ -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 {}

View File

@@ -1,82 +1,62 @@
from .models import * from __future__ import annotations
from typing import Any from typing import Any
OP_BUY = 23 OP_BUY = 23
OP_SELL = 24 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"} ORDER_SIDE_BY_OFFSET = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
class TradeMixin: class TradeMixin:
account_type: str def passorder(
self,
def passorder(self, op_type, stock, volume, order_type=0, pr_type=0, price=0.0, quick_trade=0, strategy_name=""): op_type: int,
body = {"opType": op_type, "stock": stock, "price": price, "volume": volume} stock_code: str,
for key, value in (("orderType", order_type), ("prType", pr_type), ("quickTrade", quick_trade), ("strategyName", strategy_name)): volume: int,
if value: body[key] = value order_type: int = ORDER_TYPE_VOLUME,
return self._post_json("/api/trade/passorder", body) pr_type: int = PR_TYPE_LATEST,
price: float = -1,
def passorder_latest(self, side, stock, volume): return self.passorder_latest_tagged(side, stock, volume, "", "") quick_trade: int = QUICK_TRADE_NOW,
def passorder_latest_tagged(self, side, stock_code, volume, strategy_name, order_id): strategy_name: str = "",
body = { order_id: str = "",
"opType": side, ) -> dict[str, Any]:
"orderType": ORDER_TYPE_VOLUME, return self._post_json(
"/api/trade/passorder",
{
"opType": op_type,
"orderType": order_type,
"stockCode": stock_code, "stockCode": stock_code,
"prType": PR_TYPE_LATEST, "prType": pr_type,
"price": -1, "price": price,
"volume": volume, "volume": volume,
"quickTrade": QUICK_TRADE_NOW, "quickTrade": quick_trade,
"strategyName": strategy_name, "strategyName": strategy_name,
"orderId": order_id, "orderId": order_id,
} },
return self._post_json("/api/trade/passorder", body) )
def algo_passorder(self, **kwargs): return self._post_json("/api/trade/algo_passorder", kwargs) def passorder_latest(self, side: int, stock_code: str, volume: int) -> dict[str, Any]:
def smart_algo_passorder(self, **kwargs): return self._post_json("/api/trade/smart_algo_passorder", kwargs) return self.passorder(side, stock_code, volume)
def _style_order(self, path, stock, value_key, value, style, price): def passorder_latest_tagged(
return self._post_json(path, {"stock": stock, value_key: value, "style": style, "price": price}) self,
def order_lots(self, stock, lots, style, price): return self._style_order("/api/trade/order_lots", stock, "lots", lots, style, price) side: int,
def order_value(self, stock, value, style, price): return self._style_order("/api/trade/order_value", stock, "value", value, style, price) stock_code: str,
def order_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_percent", stock, "percent", percent, style, price) volume: int,
def order_target_value(self, stock, value, style, price): return self._style_order("/api/trade/order_target_value", stock, "tar_value", value, style, price) strategy_name: str,
def order_target_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_target_percent", stock, "tar_percent", percent, style, price) order_id: str,
def order_shares(self, stock, shares, style, price): return self._style_order("/api/trade/order_shares", stock, "shares", shares, style, price) ) -> dict[str, Any]:
return self.passorder(
side,
stock_code,
volume,
strategy_name=strategy_name,
order_id=order_id,
)
def _future(self, action, stock, amount, style, price): return self._style_order(f"/api/trade/futures/{action}", stock, "amount", amount, style, price) def cancel_by_id(self, order_id: str) -> dict[str, Any]:
def futures_buy_open(self, *args): return self._future("buy_open", *args) return self._post_json("/api/trade/cancel_by_id", {"order_id": order_id})
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")

View File

@@ -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"]

View File

@@ -51,10 +51,11 @@ def StartTrend() -> None:
config.global_config.qmt_token, config.global_config.qmt_token,
config.HTTP_TIMEOUT, config.HTTP_TIMEOUT,
) )
assets = client.assets() portfolio = client.portfolio()
_, positions = client.positions() assets = portfolio.assets
positions = list(portfolio.positions.values())
order_book = OrderBook() order_book = OrderBook()
order_book.refresh(client) order_book.refresh(client, portfolio.orders)
storeState = State.for_strategy( storeState = State.for_strategy(
config.global_config.qmt_data_dir, config.global_config.qmt_data_dir,
@@ -114,20 +115,18 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
started_at = time.monotonic() started_at = time.monotonic()
# 1. 刷新订单数据,清理过期订单。 # 1. 一次获取资产、持仓和订单,并清理过期订单。
try: 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: except Exception:
log.exception("[Order] 刷新订单失败") log.exception("[Portfolio] 刷新账户快照失败")
return return
# 2. 验证可用资金;低于资金安全线时禁止开新仓。 # 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 allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio
if not allow_open_by_cash: if not allow_open_by_cash:
log.info("[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total) log.info("[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total)
@@ -135,14 +134,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。 # 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
market_ok = market_allow_open() market_ok = market_allow_open()
# 4. 获取当前持仓及持仓证券代码 # 4. 验证有效开仓信号:排除已有持仓和未决订单
try:
position_codes, positions = run.client.positions()
except Exception:
log.exception("[Position] 获取持仓失败")
return
# 5. 验证有效开仓信号:排除已有持仓和未决订单。
allow_open: list[SignalItem] = [] allow_open: list[SignalItem] = []
allow_codes: list[str] = [] allow_codes: list[str] = []
for signal in signals: for signal in signals:
@@ -153,7 +145,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
if allow_open and not market_ok: if allow_open and not market_ok:
log.info("[开仓] 禁止开仓:大盘信号不允许,候选=%d", len(allow_open)) log.info("[开仓] 禁止开仓:大盘信号不允许,候选=%d", len(allow_open))
# 6. 获取持仓和待开仓证券的实时行情 tick。 # 5. 获取持仓和待开仓证券的实时行情 tick。
all_codes = list(dict.fromkeys(position_codes + allow_codes)) all_codes = list(dict.fromkeys(position_codes + allow_codes))
try: try:
ticks = run.client.full_tick(all_codes) 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)) log.exception("[行情] 获取行情失败,代码数量=%d", len(all_codes))
return return
# 7. 更新状态机 # 6. 更新状态机
try: try:
run.state.reconcile(positions, run.orders.data) run.state.reconcile(positions, run.orders.data)
except Exception: 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) 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]] = [ 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: if allow_open and market_ok and allow_open_by_cash:
futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open))) futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open)))
# 11. 开始执行 # 9. 开始执行
for name, future in futures: for name, future in futures:
_wait_worker(name, future) _wait_worker(name, future)
log.info("[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000)) log.info("[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000))

View File

@@ -71,7 +71,6 @@ def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None:
run.client, run.client,
OP_BUY, OP_BUY,
code, code,
-1,
volume, volume,
order_id, order_id,
signal_key, signal_key,

View File

@@ -25,7 +25,6 @@ class PlaceOrderRequest:
client: Any client: Any
op: int op: int
code: str code: str
price: float
volume: int volume: int
order_id: str order_id: str
strategy_name: str strategy_name: str
@@ -52,9 +51,8 @@ class OrderBook:
key = f"{side}-{code}" key = f"{side}-{code}"
return key in self.lock return key in self.lock
def refresh(self, client: Client) -> None: def refresh(self, client: Client, orders: list[OrderItem]) -> None:
"""从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托。""" """用账户快照刷新委托,并撤销超时的活动委托。"""
orders = client.trade_detail_data("order")
current = datetime.now() current = datetime.now()
now_timestamp = current.timestamp() now_timestamp = current.timestamp()
data: list[OrderItem] = [] data: list[OrderItem] = []

View File

@@ -47,7 +47,7 @@ def manage_positions(
code = position.stock_code code = position.stock_code
tick = ticks.get(code) tick = ticks.get(code)
if code in runtime.account_cfg.excluded_codes: 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 continue
if ( if (
not code not code
@@ -56,7 +56,7 @@ def manage_positions(
or tick is None or tick is None
or tick.last_price <= 0 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 continue
pnl_rate = round( pnl_rate = round(
@@ -87,7 +87,7 @@ def manage_positions(
loss_add_action = "大盘信号不允许" loss_add_action = "大盘信号不允许"
log.info( 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, code, position.stock_name, pnl_rate, profit_action, loss_add_action,
) )

View File

@@ -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" BASE_URL = "http://127.0.0.1:10086"
TOKEN = "QMTbyYanweidong" TOKEN = "QMTbyYanweidong"
STOCK_CODE = "000021.SZ"
VOLUME = 100
def main(): def main() -> None:
client = Client_V2(BASE_URL, TOKEN) order = {
portfolio = client.portfolio() "opType": 23,
print(portfolio) "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__": if __name__ == "__main__":

View File

@@ -8,7 +8,7 @@ from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
from libs.grid_take_profit import GridState, GridTrailingTracker 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.order import OrderBook, PlaceOrderRequest
from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions
from strategy.trend.boot import RunOnce from strategy.trend.boot import RunOnce
@@ -80,7 +80,7 @@ class TrendTests(unittest.TestCase):
client = FakeOrderClient(orders) client = FakeOrderClient(orders)
book = OrderBook(cancel_timeout_sec=10) 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({item.id for item in book.data}, {"completed"})
self.assertEqual(client.canceled, ["active"]) self.assertEqual(client.canceled, ["active"])
@@ -196,10 +196,11 @@ class TrendTests(unittest.TestCase):
def test_low_cash_still_runs_position_management(self): def test_low_cash_still_runs_position_management(self):
client = SimpleNamespace( client = SimpleNamespace(
assets=lambda: Assets(total=10000, available=10), portfolio=lambda: Portfolio(
positions=lambda: (["A"], [PositionItem(stock_code="A", volume=100, open_price=10)]), assets=Assets(total=10000, available=10),
trade_detail_data=lambda _datatype: [], positions={"A": PositionItem(stock_code="A", volume=100, open_price=10)},
deals=lambda: [], orders=[],
),
full_tick=lambda _codes: {"A": Tick(last_price=11)}, full_tick=lambda _codes: {"A": Tick(last_price=11)},
) )
with ThreadPoolExecutor(max_workers=2) as executor: with ThreadPoolExecutor(max_workers=2) as executor:
@@ -207,7 +208,7 @@ class TrendTests(unittest.TestCase):
client=client, client=client,
account_cfg=SimpleNamespace(min_cash_ratio=0.1), account_cfg=SimpleNamespace(min_cash_ratio=0.1),
global_cfg=SimpleNamespace(api_host="http://example"), global_cfg=SimpleNamespace(api_host="http://example"),
orders=SimpleNamespace(refresh=lambda _client: None, data=[]), orders=SimpleNamespace(refresh=lambda _client, _orders: None, data=[]),
state=SimpleNamespace( state=SimpleNamespace(
codes=["A"], codes=["A"],
reconcile=lambda *_args: None, reconcile=lambda *_args: None,
@@ -235,10 +236,11 @@ class TrendTests(unittest.TestCase):
)) ))
state.save() state.save()
client = SimpleNamespace( client = SimpleNamespace(
assets=lambda: Assets(total=10000, available=5000), portfolio=lambda: Portfolio(
positions=lambda: ([], []), assets=Assets(total=10000, available=5000),
trade_detail_data=lambda _datatype: [], positions={},
deals=lambda: [], orders=[],
),
full_tick=lambda _codes: {"A": Tick(last_price=10)}, full_tick=lambda _codes: {"A": Tick(last_price=10)},
) )
signal = SimpleNamespace(code="A", signal_key="morning") signal = SimpleNamespace(code="A", signal_key="morning")