This commit is contained in:
2026-09-03 00:54:57 +08:00
parent 07e74d054e
commit e2800fc193
17 changed files with 262 additions and 208 deletions

View File

@@ -5,10 +5,11 @@ from .errors import APIError, BusinessError
from .misc import MiscMixin
from .models import *
from .trade import *
from .v2 import Client as V2Client, Portfolio
class Client(AccountMixin, DataMixin, TradeMixin, MiscMixin, _HTTPClient):
"""big-qmt 同步 HTTP 客户端。"""
__all__ = ["Client", "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.

View File

@@ -7,7 +7,7 @@ class AccountMixin:
account_type: str
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
if isinstance(raw, list):
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 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 {}
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 available_money(self) -> float: return float(self._post("/api/money/available", {"account": self.account_type}).get("available_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_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(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 cancel_all(self): return self._post("/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 deals(self): return self._post("/api/order/deal", {"account": self.account_type}).get("deals", [])
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

@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from dataclasses import asdict, is_dataclass
from typing import Any
@@ -41,7 +42,7 @@ class Client:
self.account_type = account_type
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):
body = asdict(body)
attempts = 2 if _is_idempotent(method, path) else 1
@@ -56,30 +57,45 @@ class Client:
assert response is not None
if response.status_code >= 400:
try:
message = response.json().get("error", response.text)
message = response.text
except (ValueError, AttributeError):
message = response.text.strip()
raise APIError(response.status_code, str(message))
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
try:
return response.json()
return json.loads(content)
except ValueError as exc:
raise ValueError(
f"invalid JSON from {path}: {response.content[:512]!r}"
f"invalid JSON from {path}: {content[:512]!r}"
) 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:
return self._get(path).get(key)
return self._get_json(path).get(key)
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"):
raise BusinessError(result["error"])
return result.get(key, result) if key and isinstance(result, dict) else result

View File

@@ -13,9 +13,9 @@ class DataMixin:
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("/api/data/sector", {"sector": sector, "realtime": realtime}).get("stocks", [])
def industry(self, industry): return self._post("/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 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")
@@ -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 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):
for name in names:
try: return float(data[name])
@@ -46,7 +46,7 @@ class DataMixin:
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("/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 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")
@@ -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")
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("/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 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

View File

@@ -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 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("/api/sys/python_version")
def shutdown(self): return self._post("/api/sys/shutdown", {})
def python_version(self): return self._get_json("/api/sys/python_version")
def shutdown(self): return self._post_json("/api/sys/shutdown", {})

View File

@@ -14,7 +14,7 @@ class TradeMixin:
body = {"opType": op_type, "stock": stock, "price": price, "volume": volume}
for key, value in (("orderType", order_type), ("prType", pr_type), ("quickTrade", quick_trade), ("strategyName", strategy_name)):
if value: body[key] = value
return self._post("/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_tagged(self, side, stock_code, volume, strategy_name, order_id):
@@ -29,13 +29,13 @@ class TradeMixin:
"strategyName": strategy_name,
"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 smart_algo_passorder(self, **kwargs): return self._post("/api/trade/smart_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_json("/api/trade/smart_algo_passorder", kwargs)
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_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)
@@ -51,14 +51,14 @@ class TradeMixin:
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(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 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("/api/trade/do_order")
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(
data = self._post_json(
"/api/trade/trade_detail_data",
{"account": self.account_type, "datatype": datatype},
).get("data", [])
@@ -70,13 +70,13 @@ class TradeMixin:
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("/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 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 cancel_by_id(self, order_id): return self._post("/api/order/cancel_by_id", {"order_id": order_id, "account_type": self.account_type})
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(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 new_purchase_limit(self): return self._post_field("/api/trade/new_purchase_limit", None, "data")

86
py-client/sdk/v2.py Normal file
View 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"]

View File

@@ -1,25 +1,15 @@
import json
import os
from pathlib import Path
from sdk.v2 import Client_V2
from sdk import Client
BASE_URL = "http://127.0.0.1:10086"
TOKEN = "QMTbyYanweidong"
def main():
client = Client(BASE_URL, TOKEN).set_account_type("stock")
assets = client.assets();
_, positions = client.positions()
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}")
def main() -> None:
with Client_V2(BASE_URL, TOKEN) as client:
portfolio = client.portfolio()
print(portfolio)
if __name__ == "__main__": main()
if __name__ == "__main__":
main()