diff --git a/api/qmt_rest_new.py b/api/qmt_rest_new.py index ac2f529..4f2d316 100644 --- a/api/qmt_rest_new.py +++ b/api/qmt_rest_new.py @@ -123,6 +123,24 @@ class StockGetHandler(BaseHandler): "ref": result }, separators=(',', ':'), ensure_ascii=False, default=str)) + +# Aggregate assets, positions, and orders in one request. +class PortfolioHandler(BaseHandler): + def get(self): + account_data = safe_call(get_trade_detail_data, self.acc(), 'stock', 'account') + positions = safe_call(get_trade_detail_data, self.acc(), 'stock', 'position') or [] + orders = safe_call(get_trade_detail_data, self.acc(), 'stock', 'order') or [] + + result = { + "assets": format_assets(account_data), + "positions": format_holding(positions), + "orders": [fixed_fields(order) for order in orders], + } + self.write(json.dumps(result, separators=(',', ':'), ensure_ascii=False)) + + +# 以下未处理 + # ContextInfo.get_bar_timetag() - Get the bar timestamp class BarTimetagHandler(BaseHandler): def post(self): @@ -1015,33 +1033,48 @@ class GetFactorRankHandler(BaseHandler): # ============= 9. Legacy handlers (compatibility) ============= +def format_holding(positions): + holding = {} + for position in positions: + stock = position.m_strInstrumentID + '.' + position.m_strExchangeID + holding[stock] = { + 'StockCode': stock, + 'StockName': position.m_strInstrumentName, + 'Direction': position.m_nDirection, + 'Volume': position.m_nVolume, + 'OpenPrice': position.m_dOpenPrice, + 'FloatProfit': position.m_dFloatProfit, + 'MarketValue': position.m_dMarketValue, + 'StockHolder': position.m_strStockHolder, + 'FrozenVolume': position.m_nFrozenVolume, + 'CanUseVolume': position.m_nCanUseVolume, + 'OnRoadVolume': position.m_nOnRoadVolume, + 'YesterdayVolume': position.m_nYesterdayVolume, + 'LastPrice': position.m_dLastPrice, + 'ProfitRate': position.m_dProfitRate, + 'FutureTradeType': position.m_eFutureTradeType, + 'ExpireDate': position.m_strExpireDate + } + return holding + + +def format_assets(account_data): + info = account_data[0] if account_data else None + if not info: + raise HTTPError(500, "Failed to get account data") + return { + "total": round(info.m_dBalance, 2), + "available": round(info.m_dAvailable, 2), + } + + # get_trade_detail_data('position') - Query positions in the wrapped format class HoldingHandler(BaseHandler): def post(self): data = json.loads(self.request.body) account = data.get('account', 'stock') positions = safe_call(get_trade_detail_data, self.acc(), account, 'position') or [] - holding = {} - for position in positions: - stock = position.m_strInstrumentID + '.' + position.m_strExchangeID - holding[stock] = { - 'StockCode': stock, - 'StockName': position.m_strInstrumentName, - 'Direction': position.m_nDirection, - 'Volume': position.m_nVolume, - 'OpenPrice': position.m_dOpenPrice, - 'FloatProfit': position.m_dFloatProfit, - 'MarketValue': position.m_dMarketValue, - 'StockHolder': position.m_strStockHolder, - 'FrozenVolume': position.m_nFrozenVolume, - 'CanUseVolume': position.m_nCanUseVolume, - 'OnRoadVolume': position.m_nOnRoadVolume, - 'YesterdayVolume': position.m_nYesterdayVolume, - 'LastPrice': position.m_dLastPrice, - 'ProfitRate': position.m_dProfitRate, - 'FutureTradeType': position.m_eFutureTradeType, - 'ExpireDate': position.m_strExpireDate - } + holding = format_holding(positions) self.write(json.dumps({"data": holding}, separators=(',', ':'), ensure_ascii=False)) # get_trade_detail_data('account') - Query account assets @@ -1050,10 +1083,9 @@ class AssetsHandler(BaseHandler): data = json.loads(self.request.body) account = data.get('account', 'stock') _data = safe_call(get_trade_detail_data, self.acc(), account, 'account') - info = _data[0] if _data else None - if not info: - raise HTTPError(500, "Failed to get account data") - self.write(json.dumps({"total": round(info.m_dBalance, 2),"available": round(info.m_dAvailable, 2)}, separators=(',', ':'), ensure_ascii=False)) + self.write(json.dumps(format_assets(_data), separators=(',', ':'), ensure_ascii=False)) + + # passorder(23) - Simplified buy order wrapper @@ -1210,6 +1242,7 @@ class DealHandler(BaseHandler): def make_app(): return Application([ # V2 + (r"/api/v2/portfolio", PortfolioHandler), (r"/api/v2/positions", HoldingHandler), (r"/api/v2/assets", AssetsHandler), # ContextInfo properties diff --git a/api/qmt_rest_rele.py b/api/qmt_rest_rele.py index bedf133..ac2f529 100644 --- a/api/qmt_rest_rele.py +++ b/api/qmt_rest_rele.py @@ -1,4 +1,4 @@ -# -*- coding: gbk -*- +# -*- coding: utf-8 -*- import json import locale import os @@ -26,10 +26,9 @@ def safe_call(func, *args, **kwargs): except HTTPError: raise except Exception as e: - logger.exception("%s call failed", func.__name__) raise HTTPError( - 502, - reason="QMT upstream call failed: %s" % func.__name__, + 500, + reason="QMT: %s call failed." % func.__name__, ) from e @@ -48,16 +47,13 @@ class BaseHandler(RequestHandler): if self.__class__ not in AUTH_EXEMPT: token = self.request.headers.get('X-Token') if token != TOKEN: - raise HTTPError(401, "Authentication failed: invalid or missing token") + raise HTTPError(500, "Authentication failed: invalid or missing token") def set_default_headers(self): self.set_header("Content-Type", "application/json; charset=utf-8") - def write_error(self, status_code, **kwargs): - self.finish(json.dumps({ - "error": self._reason, - "status_code": status_code - }, separators=(',', ':'), ensure_ascii=False)) + def write_error(self, **kwargs): + self.finish(self._reason) def ctx(self): return self.application.ContextInfo @@ -67,6 +63,7 @@ class BaseHandler(RequestHandler): # ============= 1. ContextInfo properties ============= +# "/api/v2/context/info" class ContextInfoHandler(BaseHandler): def get(self): ctx = self.ctx() @@ -80,38 +77,51 @@ class ContextInfoHandler(BaseHandler): "do_back_test": ctx.do_back_test, "benchmark": ctx.benchmark, "capital": ctx.capital, + "timetag":ctx.timetag, "universe": ctx.get_universe(), } self.write(data, separators=(',', ':'), ensure_ascii=False) - - # ============= 2. Data queries (ContextInfo get_*) ============= -# ContextInfo.get_stock_name() - Get a stock name by symbol -class StockNameHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_stock_name, stockcode) - self.write(json.dumps({"stockcode": stockcode, "name": ret}, separators=(',', ':'), ensure_ascii=False)) - -# get_open_date() - Get the listing date by symbol -class OpenDateHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(get_open_date, stockcode) - self.write(json.dumps({"stockcode": stockcode, "open_date": ret}, separators=(',', ':'), ensure_ascii=False)) - -# ContextInfo.get_last_volume() - Get the latest outstanding shares -class LastVolumeHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_last_volume, stockcode) - if ret is None: - raise HTTPError(500, "Failed to get outstanding shares") - self.write(json.dumps({"stockcode": stockcode, "last_volume": ret}, separators=(',', ':'), ensure_ascii=False)) +STOCK_HANDLER = { + # handler_type: (method_name, use_context) + "stock_name": ("get_stock_name", True), + "open_date": ("get_open_date", True), + "last_volume": ("get_last_volume", True), + "total_share": ("get_total_share", True), + "svol": ("get_svol", True), + "bvol": ("get_bvol", True), + "divid_factors": ("get_divid_factors", True), + "etf_info": ("get_etf_info", False), + "etf_iopv": ("get_etf_iopv", False), + "instrumentdetail": ("get_instrumentdetail", True), + "his_st_data": ("get_his_st_data", True), +} +# "/api/v2/get/*" Stock-related single-symbol queries +class StockGetHandler(BaseHandler): + def get(self, handler_type): + # 快速路径:配置查找 + cfg = STOCK_HANDLER.get(handler_type) + if not cfg: + raise HTTPError(500, "Unknown API") + + # 参数验证 + + query_vals = self.get_query_argument("stock_code", "").strip() + if not query_vals: + raise HTTPError(500, "stock_code required") + + # 方法调用 + method_name, use_context = cfg + method = getattr(self.ctx(), method_name) if use_context else globals()[method_name] + result = safe_call(method, query_vals) + + + # 响应 + self.write(json.dumps({ + "stock_code": query_vals, + "ref": result + }, separators=(',', ':'), ensure_ascii=False, default=str)) # ContextInfo.get_bar_timetag() - Get the bar timestamp class BarTimetagHandler(BaseHandler): @@ -121,12 +131,6 @@ class BarTimetagHandler(BaseHandler): ret = safe_call(self.ctx().get_bar_timetag, index) self.write(json.dumps({"index": index, "timetag": ret}, separators=(',', ':'), ensure_ascii=False)) -# ContextInfo.get_tick_timetag() - Get the latest tick timestamp -class TickTimetagHandler(BaseHandler): - def get(self): - ret = safe_call(self.ctx().get_tick_timetag) - self.write(json.dumps({"timetag": ret}, separators=(',', ':'), ensure_ascii=False)) - # ContextInfo.get_sector() - Get index constituents class SectorHandler(BaseHandler): def post(self): @@ -260,14 +264,6 @@ class FullTickHandler(BaseHandler): raise HTTPError(500, "Failed to get tick data") self.write(json.dumps(ret, separators=(',', ':'), ensure_ascii=False, default=str)) -# ContextInfo.get_divid_factors() - Get dividend and adjustment factors -class DividFactorsHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_divid_factors, stockcode) - self.write(json.dumps({"stockcode": stockcode, "factors": ret or {}}, separators=(',', ':'), ensure_ascii=False)) - # ContextInfo.get_main_contract() - Get the main futures contract class MainContractHandler(BaseHandler): def post(self): @@ -285,14 +281,6 @@ class TimetagToDatetimeHandler(BaseHandler): ret = safe_call(timetag_to_datetime, timetag, fmt) self.write(json.dumps({"timetag": timetag, "datetime": ret}, separators=(',', ':'), ensure_ascii=False)) -# ContextInfo.get_total_share() - Get total shares -class TotalShareHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_total_share, stockcode) - self.write(json.dumps({"stockcode": stockcode, "total_share": ret}, separators=(',', ':'), ensure_ascii=False)) - # ContextInfo.get_trading_dates() - Get the trading-day list class TradingDatesHandler(BaseHandler): def post(self): @@ -306,22 +294,6 @@ class TradingDatesHandler(BaseHandler): ret = safe_call(self.ctx().get_trading_dates, stockcode, start_date, end_date, count_int, period) self.write(json.dumps({"dates": ret or []}, separators=(',', ':'), ensure_ascii=False)) -# ContextInfo.get_svol() - Get sell-side volume -class SvolHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_svol, stockcode) - self.write(json.dumps({"stockcode": stockcode, "svol": ret}, separators=(',', ':'), ensure_ascii=False)) - -# ContextInfo.get_bvol() - Get buy-side volume -class BvolHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_bvol, stockcode) - self.write(json.dumps({"stockcode": stockcode, "bvol": ret}, separators=(',', ':'), ensure_ascii=False)) - # ContextInfo.get_longhubang() - Get Dragon-Tiger List data class LonghubangHandler(BaseHandler): def post(self): @@ -370,30 +342,6 @@ class TurnoverRateHandler(BaseHandler): ret = ret.to_dict() self.write(json.dumps({"data": ret} if ret else {"error": "Failed to get turnover rate"}, separators=(',', ':'), ensure_ascii=False, default=str)) -# get_etf_info() - Get ETF creation/redemption and constituent data -class EtfInfoHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(get_etf_info, stockcode) - self.write(json.dumps({"stockcode": stockcode, "info": ret or {}}, separators=(',', ':'), ensure_ascii=False, default=str)) - -# get_etf_iopv() - Get the ETF indicative optimized portfolio value -class EtfIopvHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(get_etf_iopv, stockcode) - self.write(json.dumps({"stockcode": stockcode, "iopv": ret}, separators=(',', ':'), ensure_ascii=False)) - -# ContextInfo.get_instrumentdetail() - Get instrument details -class InstrumentDetailHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockcode = data.get('stockcode', '') - ret = safe_call(self.ctx().get_instrumentdetail, stockcode) - self.write(json.dumps({"stockcode": stockcode, "detail": ret or {}}, separators=(',', ':'), ensure_ascii=False, default=str)) - # ContextInfo.get_contract_expire_date() - Get the futures contract expiration date class ContractExpireDateHandler(BaseHandler): def post(self): @@ -454,14 +402,6 @@ class FactorDataHandler(BaseHandler): ret = ret.to_dict() self.write(json.dumps({"data": ret} if ret is not None else {"error": "Failed to get factor data"}, separators=(',', ':'), ensure_ascii=False, default=str)) -# ContextInfo.get_his_st_data() - Get historical ST data -class HisStDataHandler(BaseHandler): - def post(self): - data = json.loads(self.request.body) - stockCode = data.get('stockCode', '') - ret = safe_call(self.ctx().get_his_st_data, stockCode) - self.write(json.dumps({"stockCode": stockCode, "data": ret or {}}, separators=(',', ':'), ensure_ascii=False)) - # ContextInfo.get_his_index_data() - Get historical index data class HisIndexDataHandler(BaseHandler): def post(self): @@ -1272,6 +1212,9 @@ def make_app(): # V2 (r"/api/v2/positions", HoldingHandler), (r"/api/v2/assets", AssetsHandler), + # ContextInfo properties + (r"/api/v2/context/info", ContextInfoHandler), + (r"/api/v2/get/(stock_name|open_date|last_volume|total_share|svol|bvol|divid_factors|etf_info|etf_iopv|instrumentdetail|his_st_data)", StockGetHandler), # Legacy compatibility routes (r"/api/holding", HoldingHandler), @@ -1283,15 +1226,8 @@ def make_app(): (r"/api/order/cancel_by_id", CancelByIdHandler), (r"/api/order/deal", DealHandler), - # ContextInfo properties - (r"/api/context/info", ContextInfoHandler), - # Data queries - (r"/api/data/stock_name", StockNameHandler), - (r"/api/data/open_date", OpenDateHandler), - (r"/api/data/last_volume", LastVolumeHandler), (r"/api/data/bar_timetag", BarTimetagHandler), - (r"/api/data/tick_timetag", TickTimetagHandler), (r"/api/data/sector", SectorHandler), (r"/api/data/industry", IndustryHandler), (r"/api/data/stock_list_in_sector", StockListInSectorHandler), @@ -1303,25 +1239,17 @@ def make_app(): (r"/api/data/market_data", MarketDataHandler), (r"/api/data/market_data_ex", MarketDataExHandler), (r"/api/data/full_tick", FullTickHandler), - (r"/api/data/divid_factors", DividFactorsHandler), (r"/api/data/main_contract", MainContractHandler), (r"/api/data/timetag_to_datetime", TimetagToDatetimeHandler), - (r"/api/data/total_share", TotalShareHandler), (r"/api/data/trading_dates", TradingDatesHandler), - (r"/api/data/svol", SvolHandler), - (r"/api/data/bvol", BvolHandler), (r"/api/data/longhubang", LonghubangHandler), (r"/api/data/top10_share_holder", Top10ShareHolderHandler), (r"/api/data/option_detail", OptionDetailHandler), (r"/api/data/turnover_rate", TurnoverRateHandler), - (r"/api/data/etf_info", EtfInfoHandler), - (r"/api/data/etf_iopv", EtfIopvHandler), - (r"/api/data/instrumentdetail", InstrumentDetailHandler), (r"/api/data/contract_expire_date", ContractExpireDateHandler), (r"/api/data/option_undl_data", OptionUndlDataHandler), (r"/api/data/financial_data", FinancialDataHandler), (r"/api/data/factor_data", FactorDataHandler), - (r"/api/data/his_st_data", HisStDataHandler), (r"/api/data/his_index_data", HisIndexDataHandler), (r"/api/data/all_subscription", AllSubscriptionHandler), (r"/api/data/option_list", OptionListHandler), diff --git a/py-client/sdk/__init__.py b/py-client/sdk/__init__.py index f442d47..6fdb18c 100644 --- a/py-client/sdk/__init__.py +++ b/py-client/sdk/__init__.py @@ -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"] diff --git a/py-client/sdk/__pycache__/__init__.cpython-311.pyc b/py-client/sdk/__pycache__/__init__.cpython-311.pyc index e79a499..2bba3f5 100644 Binary files a/py-client/sdk/__pycache__/__init__.cpython-311.pyc and b/py-client/sdk/__pycache__/__init__.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/account.cpython-311.pyc b/py-client/sdk/__pycache__/account.cpython-311.pyc index a32c7d0..b29005b 100644 Binary files a/py-client/sdk/__pycache__/account.cpython-311.pyc and b/py-client/sdk/__pycache__/account.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/client.cpython-311.pyc b/py-client/sdk/__pycache__/client.cpython-311.pyc index 96d9136..a3022fa 100644 Binary files a/py-client/sdk/__pycache__/client.cpython-311.pyc and b/py-client/sdk/__pycache__/client.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/data.cpython-311.pyc b/py-client/sdk/__pycache__/data.cpython-311.pyc index 4c6b982..0068014 100644 Binary files a/py-client/sdk/__pycache__/data.cpython-311.pyc and b/py-client/sdk/__pycache__/data.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/misc.cpython-311.pyc b/py-client/sdk/__pycache__/misc.cpython-311.pyc index 9afa90a..32b8591 100644 Binary files a/py-client/sdk/__pycache__/misc.cpython-311.pyc and b/py-client/sdk/__pycache__/misc.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/trade.cpython-311.pyc b/py-client/sdk/__pycache__/trade.cpython-311.pyc index 035a006..b990680 100644 Binary files a/py-client/sdk/__pycache__/trade.cpython-311.pyc and b/py-client/sdk/__pycache__/trade.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/v2.cpython-311.pyc b/py-client/sdk/__pycache__/v2.cpython-311.pyc new file mode 100644 index 0000000..59e8118 Binary files /dev/null and b/py-client/sdk/__pycache__/v2.cpython-311.pyc differ diff --git a/py-client/sdk/account.py b/py-client/sdk/account.py index 1fccfd1..046fee0 100644 --- a/py-client/sdk/account.py +++ b/py-client/sdk/account.py @@ -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", []) diff --git a/py-client/sdk/client.py b/py-client/sdk/client.py index 72188d9..e8a65e2 100644 --- a/py-client/sdk/client.py +++ b/py-client/sdk/client.py @@ -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 diff --git a/py-client/sdk/data.py b/py-client/sdk/data.py index e454300..bcd7164 100644 --- a/py-client/sdk/data.py +++ b/py-client/sdk/data.py @@ -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 diff --git a/py-client/sdk/misc.py b/py-client/sdk/misc.py index e3c4969..4ef8f6f 100644 --- a/py-client/sdk/misc.py +++ b/py-client/sdk/misc.py @@ -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", {}) diff --git a/py-client/sdk/trade.py b/py-client/sdk/trade.py index 7eec68e..310ba6b 100644 --- a/py-client/sdk/trade.py +++ b/py-client/sdk/trade.py @@ -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") diff --git a/py-client/sdk/v2.py b/py-client/sdk/v2.py new file mode 100644 index 0000000..20d9160 --- /dev/null +++ b/py-client/sdk/v2.py @@ -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"] diff --git a/py-client/test.py b/py-client/test.py index 82c3ed7..7023bdc 100644 --- a/py-client/test.py +++ b/py-client/test.py @@ -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()