fix bug
This commit is contained in:
1293
api/qmt_rest_new.py
1293
api/qmt_rest_new.py
File diff suppressed because it is too large
Load Diff
1452
api/qmt_rest_old.py
Normal file
1452
api/qmt_rest_old.py
Normal file
File diff suppressed because it is too large
Load Diff
1210
api/qmt_rest_rele.py
1210
api/qmt_rest_rele.py
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,27 @@
|
||||
from .account import AccountMixin
|
||||
from .client import Client as _HTTPClient
|
||||
from .data import DataMixin
|
||||
from .client import Client
|
||||
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", "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"]
|
||||
from .models import Assets, OrderItem, Portfolio, PositionItem, Tick
|
||||
from .trade import (
|
||||
OP_BUY,
|
||||
OP_SELL,
|
||||
ORDER_SIDE_BY_OFFSET,
|
||||
ORDER_TYPE_VOLUME,
|
||||
PR_TYPE_LATEST,
|
||||
QUICK_TRADE_NOW,
|
||||
)
|
||||
__all__ = [
|
||||
"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.
BIN
py-client/sdk/__pycache__/context.cpython-311.pyc
Normal file
BIN
py-client/sdk/__pycache__/context.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
py-client/sdk/__pycache__/get.cpython-311.pyc
Normal file
BIN
py-client/sdk/__pycache__/get.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
py-client/sdk/__pycache__/portfolio.cpython-311.pyc
Normal file
BIN
py-client/sdk/__pycache__/portfolio.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/sdk/__pycache__/sys.cpython-311.pyc
Normal file
BIN
py-client/sdk/__pycache__/sys.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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", [])
|
||||
@@ -6,14 +6,16 @@ from typing import Any
|
||||
|
||||
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:
|
||||
return ",".join(item.strip() for item in items if item.strip())
|
||||
|
||||
|
||||
class Client:
|
||||
class HTTPClient:
|
||||
"""复用连接池的同步 QMT HTTP 客户端。"""
|
||||
|
||||
def __init__(self, base_url: str, token: str, timeout: float = 15.0) -> None:
|
||||
@@ -31,13 +33,13 @@ class Client:
|
||||
def close(self) -> None:
|
||||
self.http.close()
|
||||
|
||||
def __enter__(self) -> "Client":
|
||||
def __enter__(self) -> "HTTPClient":
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args: object) -> None:
|
||||
self.close()
|
||||
|
||||
def set_account_type(self, account_type: str) -> "Client":
|
||||
def set_account_type(self, account_type: str) -> "HTTPClient":
|
||||
if account_type.strip():
|
||||
self.account_type = account_type
|
||||
return self
|
||||
@@ -91,28 +93,19 @@ class Client:
|
||||
f"invalid JSON from {path}: {content[:512]!r}"
|
||||
) 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:
|
||||
if method == "GET":
|
||||
return True
|
||||
prefixes = (
|
||||
"/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)
|
||||
return path == "/api/data/full_tick"
|
||||
|
||||
8
py-client/sdk/context.py
Normal file
8
py-client/sdk/context.py
Normal 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 {}
|
||||
@@ -1,83 +1,9 @@
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from .client import csv_join
|
||||
from .models import *
|
||||
from .models import Tick
|
||||
|
||||
|
||||
class DataMixin:
|
||||
def _one(self, endpoint, arg, value, key): return self._post_field(f"/api/data/{endpoint}", {arg: value}, key)
|
||||
|
||||
def stock_name(self, code): return self._one("stock_name", "stockcode", code, "name")
|
||||
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"]}
|
||||
def full_tick(self, stocks: list[str]) -> dict[str, Tick]:
|
||||
data = self._post_json("/api/data/full_tick", {"stocks": stocks}) or {}
|
||||
return {code: Tick.from_dict(value) for code, value in data.items()}
|
||||
|
||||
44
py-client/sdk/get.py
Normal file
44
py-client/sdk/get.py
Normal 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")
|
||||
@@ -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", {})
|
||||
@@ -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:
|
||||
date = str(data.get("m_strInsertDate") or "")
|
||||
clock = str(data.get("m_strInsertTime") or "").replace(":", "").zfill(6)
|
||||
@@ -148,6 +155,16 @@ class Tick:
|
||||
last_close: float = 0.0
|
||||
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)
|
||||
class HistoryDataRequest:
|
||||
|
||||
52
py-client/sdk/portfolio.py
Normal file
52
py-client/sdk/portfolio.py
Normal 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
8
py-client/sdk/sys.py
Normal 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 {}
|
||||
@@ -1,82 +1,62 @@
|
||||
from .models import *
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
OP_BUY = 23
|
||||
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"}
|
||||
|
||||
|
||||
class TradeMixin:
|
||||
account_type: str
|
||||
|
||||
def passorder(self, op_type, stock, volume, order_type=0, pr_type=0, price=0.0, quick_trade=0, strategy_name=""):
|
||||
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_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):
|
||||
body = {
|
||||
"opType": side,
|
||||
"orderType": ORDER_TYPE_VOLUME,
|
||||
def passorder(
|
||||
self,
|
||||
op_type: int,
|
||||
stock_code: str,
|
||||
volume: int,
|
||||
order_type: int = ORDER_TYPE_VOLUME,
|
||||
pr_type: int = PR_TYPE_LATEST,
|
||||
price: float = -1,
|
||||
quick_trade: int = QUICK_TRADE_NOW,
|
||||
strategy_name: str = "",
|
||||
order_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
return self._post_json(
|
||||
"/api/trade/passorder",
|
||||
{
|
||||
"opType": op_type,
|
||||
"orderType": order_type,
|
||||
"stockCode": stock_code,
|
||||
"prType": PR_TYPE_LATEST,
|
||||
"price": -1,
|
||||
"prType": pr_type,
|
||||
"price": price,
|
||||
"volume": volume,
|
||||
"quickTrade": QUICK_TRADE_NOW,
|
||||
"quickTrade": quick_trade,
|
||||
"strategyName": strategy_name,
|
||||
"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 smart_algo_passorder(self, **kwargs): return self._post_json("/api/trade/smart_algo_passorder", kwargs)
|
||||
def passorder_latest(self, side: int, stock_code: str, volume: int) -> dict[str, Any]:
|
||||
return self.passorder(side, stock_code, volume)
|
||||
|
||||
def _style_order(self, path, stock, value_key, value, style, 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)
|
||||
def order_target_value(self, stock, value, style, price): return self._style_order("/api/trade/order_target_value", stock, "tar_value", value, style, price)
|
||||
def order_target_percent(self, stock, percent, style, price): return self._style_order("/api/trade/order_target_percent", stock, "tar_percent", percent, style, price)
|
||||
def order_shares(self, stock, shares, style, price): return self._style_order("/api/trade/order_shares", stock, "shares", shares, style, price)
|
||||
def passorder_latest_tagged(
|
||||
self,
|
||||
side: int,
|
||||
stock_code: str,
|
||||
volume: int,
|
||||
strategy_name: str,
|
||||
order_id: str,
|
||||
) -> 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 futures_buy_open(self, *args): return self._future("buy_open", *args)
|
||||
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")
|
||||
def cancel_by_id(self, order_id: str) -> dict[str, Any]:
|
||||
return self._post_json("/api/trade/cancel_by_id", {"order_id": order_id})
|
||||
|
||||
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -51,10 +51,11 @@ def StartTrend() -> None:
|
||||
config.global_config.qmt_token,
|
||||
config.HTTP_TIMEOUT,
|
||||
)
|
||||
assets = client.assets()
|
||||
_, positions = client.positions()
|
||||
portfolio = client.portfolio()
|
||||
assets = portfolio.assets
|
||||
positions = list(portfolio.positions.values())
|
||||
order_book = OrderBook()
|
||||
order_book.refresh(client)
|
||||
order_book.refresh(client, portfolio.orders)
|
||||
|
||||
storeState = State.for_strategy(
|
||||
config.global_config.qmt_data_dir,
|
||||
@@ -114,20 +115,18 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
|
||||
started_at = time.monotonic()
|
||||
|
||||
# 1. 刷新订单数据,清理过期订单。
|
||||
# 1. 一次获取资产、持仓和订单,并清理过期订单。
|
||||
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:
|
||||
log.exception("[Order] 刷新订单失败")
|
||||
log.exception("[Portfolio] 刷新账户快照失败")
|
||||
return
|
||||
|
||||
|
||||
# 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
|
||||
if not allow_open_by_cash:
|
||||
log.info("[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f", assets.available, assets.total)
|
||||
@@ -135,14 +134,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
|
||||
market_ok = market_allow_open()
|
||||
|
||||
# 4. 获取当前持仓及持仓证券代码。
|
||||
try:
|
||||
position_codes, positions = run.client.positions()
|
||||
except Exception:
|
||||
log.exception("[Position] 获取持仓失败")
|
||||
return
|
||||
|
||||
# 5. 验证有效开仓信号:排除已有持仓和未决订单。
|
||||
# 4. 验证有效开仓信号:排除已有持仓和未决订单。
|
||||
allow_open: list[SignalItem] = []
|
||||
allow_codes: list[str] = []
|
||||
for signal in signals:
|
||||
@@ -153,7 +145,7 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
if allow_open and not market_ok:
|
||||
log.info("[开仓] 禁止开仓:大盘信号不允许,候选=%d", len(allow_open))
|
||||
|
||||
# 6. 获取持仓和待开仓证券的实时行情 tick。
|
||||
# 5. 获取持仓和待开仓证券的实时行情 tick。
|
||||
all_codes = list(dict.fromkeys(position_codes + allow_codes))
|
||||
try:
|
||||
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))
|
||||
return
|
||||
|
||||
# 7. 更新状态机
|
||||
# 6. 更新状态机
|
||||
try:
|
||||
run.state.reconcile(positions, run.orders.data)
|
||||
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)
|
||||
|
||||
# 启动线程,开始计算
|
||||
# 9. 持仓计算。
|
||||
# 7. 持仓计算。
|
||||
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:
|
||||
futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open)))
|
||||
|
||||
# 11. 开始执行
|
||||
# 9. 开始执行
|
||||
for name, future in futures:
|
||||
_wait_worker(name, future)
|
||||
log.info("[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000))
|
||||
|
||||
@@ -71,7 +71,6 @@ def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None:
|
||||
run.client,
|
||||
OP_BUY,
|
||||
code,
|
||||
-1,
|
||||
volume,
|
||||
order_id,
|
||||
signal_key,
|
||||
|
||||
@@ -25,7 +25,6 @@ class PlaceOrderRequest:
|
||||
client: Any
|
||||
op: int
|
||||
code: str
|
||||
price: float
|
||||
volume: int
|
||||
order_id: str
|
||||
strategy_name: str
|
||||
@@ -52,9 +51,8 @@ class OrderBook:
|
||||
key = f"{side}-{code}"
|
||||
return key in self.lock
|
||||
|
||||
def refresh(self, client: Client) -> None:
|
||||
"""从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托。"""
|
||||
orders = client.trade_detail_data("order")
|
||||
def refresh(self, client: Client, orders: list[OrderItem]) -> None:
|
||||
"""用账户快照刷新委托,并撤销超时的活动委托。"""
|
||||
current = datetime.now()
|
||||
now_timestamp = current.timestamp()
|
||||
data: list[OrderItem] = []
|
||||
|
||||
@@ -47,7 +47,7 @@ def manage_positions(
|
||||
code = position.stock_code
|
||||
tick = ticks.get(code)
|
||||
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
|
||||
if (
|
||||
not code
|
||||
@@ -56,7 +56,7 @@ def manage_positions(
|
||||
or tick is None
|
||||
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
|
||||
|
||||
pnl_rate = round(
|
||||
@@ -87,7 +87,7 @@ def manage_positions(
|
||||
loss_add_action = "大盘信号不允许"
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
TOKEN = "QMTbyYanweidong"
|
||||
STOCK_CODE = "000021.SZ"
|
||||
VOLUME = 100
|
||||
|
||||
|
||||
def main():
|
||||
client = Client_V2(BASE_URL, TOKEN)
|
||||
portfolio = client.portfolio()
|
||||
print(portfolio)
|
||||
|
||||
def main() -> None:
|
||||
order = {
|
||||
"opType": 23,
|
||||
"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__":
|
||||
|
||||
@@ -8,7 +8,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
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.positions import LOSS_TIERS, handle_loss, manage_positions
|
||||
from strategy.trend.boot import RunOnce
|
||||
@@ -80,7 +80,7 @@ class TrendTests(unittest.TestCase):
|
||||
client = FakeOrderClient(orders)
|
||||
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(client.canceled, ["active"])
|
||||
@@ -196,10 +196,11 @@ class TrendTests(unittest.TestCase):
|
||||
|
||||
def test_low_cash_still_runs_position_management(self):
|
||||
client = SimpleNamespace(
|
||||
assets=lambda: Assets(total=10000, available=10),
|
||||
positions=lambda: (["A"], [PositionItem(stock_code="A", volume=100, open_price=10)]),
|
||||
trade_detail_data=lambda _datatype: [],
|
||||
deals=lambda: [],
|
||||
portfolio=lambda: Portfolio(
|
||||
assets=Assets(total=10000, available=10),
|
||||
positions={"A": PositionItem(stock_code="A", volume=100, open_price=10)},
|
||||
orders=[],
|
||||
),
|
||||
full_tick=lambda _codes: {"A": Tick(last_price=11)},
|
||||
)
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
@@ -207,7 +208,7 @@ class TrendTests(unittest.TestCase):
|
||||
client=client,
|
||||
account_cfg=SimpleNamespace(min_cash_ratio=0.1),
|
||||
global_cfg=SimpleNamespace(api_host="http://example"),
|
||||
orders=SimpleNamespace(refresh=lambda _client: None, data=[]),
|
||||
orders=SimpleNamespace(refresh=lambda _client, _orders: None, data=[]),
|
||||
state=SimpleNamespace(
|
||||
codes=["A"],
|
||||
reconcile=lambda *_args: None,
|
||||
@@ -235,10 +236,11 @@ class TrendTests(unittest.TestCase):
|
||||
))
|
||||
state.save()
|
||||
client = SimpleNamespace(
|
||||
assets=lambda: Assets(total=10000, available=5000),
|
||||
positions=lambda: ([], []),
|
||||
trade_detail_data=lambda _datatype: [],
|
||||
deals=lambda: [],
|
||||
portfolio=lambda: Portfolio(
|
||||
assets=Assets(total=10000, available=5000),
|
||||
positions={},
|
||||
orders=[],
|
||||
),
|
||||
full_tick=lambda _codes: {"A": Tick(last_price=10)},
|
||||
)
|
||||
signal = SimpleNamespace(code="A", signal_key="morning")
|
||||
|
||||
Reference in New Issue
Block a user