feat client.

This commit is contained in:
2026-08-31 13:00:22 +08:00
parent a946c5b53d
commit 1b6f5a9f03
33 changed files with 544 additions and 1284 deletions

View File

@@ -0,0 +1,5 @@
"""日内做 T 策略。"""
from .boot import StartZT
__all__ = ["StartZT"]

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,75 @@
"""日内做 T 策略启动器。"""
from __future__ import annotations
import logging
import time
from datetime import datetime, time as clock_time
import config
from libs.calc import trading_time
from libs.grid_take_profit import GridTrailingTracker
from libs.market import market_allow_open
from libs.signal import init_signals
from sdk import Client
from strategy.trend.order import OrderBook
from strategy.trend.watch import DipWatch
from .open import open_base
from .positions import manage_positions
from .runtime import Runtime
from .state import TState
def StartZT() -> None:
client = Client(config.global_config.qmt_base_url, config.global_config.qmt_token, config.HTTP_TIMEOUT)
orders = OrderBook()
orders.refresh(client)
_, positions = client.positions()
state = TState.for_strategy(config.global_config.qmt_data_dir, config.account_config.strategy, config.account_config.account_id)
state.reconcile(positions, orders.data, datetime.now().date().isoformat())
run = Runtime(client, config.global_config, config.account_config, state, orders, DipWatch(), GridTrailingTracker(config.account_config.grid_step_pct))
while True:
started = time.monotonic()
try:
RunOnce(run)
except Exception:
logging.exception("ZT 策略本轮失败")
time.sleep(max(0.0, 30.0 - (time.monotonic() - started)))
def RunOnce(run: Runtime) -> None:
if not trading_time(datetime.now()):
return
try:
run.orders.refresh(run.client)
assets = run.client.assets()
position_codes, positions = run.client.positions()
except Exception:
logging.exception("[ZT] 刷新账户或订单失败")
return
today = datetime.now().date().isoformat()
try:
run.state.reconcile(positions, run.orders.data, today)
except Exception:
logging.exception("[ZT] 状态对账失败")
return
signals = init_signals(run.global_cfg, run.account_cfg.signal_allow)
candidate_codes = [item.code for item in signals if item.code not in position_codes]
codes = list(dict.fromkeys(position_codes + candidate_codes))
try:
ticks = run.client.full_tick(codes)
except Exception:
logging.exception("[ZT] 获取行情失败")
return
market_ok = market_allow_open(run.global_cfg.api_host)
if market_ok and assets.available >= assets.total * run.account_cfg.min_cash_ratio:
open_base(run, ticks, signals)
manage_positions(
run,
ticks,
positions,
assets.available,
today,
force_buy_back=datetime.now().time() >= clock_time(14, 50),
)

View File

@@ -0,0 +1,27 @@
"""使用 dcm 信号建立做 T 底仓。"""
from __future__ import annotations
import logging
from libs.calc import calc_buy_volume
from sdk import OP_BUY
from strategy.trend.order import PlaceOrderRequest
def open_base(run, ticks, signals) -> None:
"""仅处理 dcm 信号,使用趋势策略同款反弹确认建立底仓。"""
for signal in signals:
if signal.signal_key != "dcm" or run.orders.busy(signal.code, "BUY"):
continue
tick = ticks.get(signal.code)
price = tick.last_price if tick else 0.0
if price <= 0 or price > run.account_cfg.zt_max_price:
continue
volume = calc_buy_volume(price, run.account_cfg.buy_value)
if volume <= 0 or not run.buy_watch.triggered("ZT 建仓", signal.code, price):
continue
request = PlaceOrderRequest(run.client, OP_BUY, signal.code, volume, run.orders.new_order_id("base"), run.account_cfg.strategy)
if run.orders.place(request):
run.buy_watch.forget(signal.code)
logging.info("[ZT 建仓] %s 买入 %d", signal.code, volume)

View File

@@ -0,0 +1,71 @@
"""日内先卖后买的做 T 规则。"""
from __future__ import annotations
import logging
from libs.grid_take_profit import GridState
from sdk import OP_BUY, OP_SELL, PositionItem
from strategy.trend.order import PlaceOrderRequest
from .state import BUYING, READY, SELLING, SOLD
def manage_positions(run, ticks, positions: list[PositionItem], available: float, today: str, force_buy_back: bool = False) -> None:
for position in positions:
code = position.stock_code
tick = ticks.get(code)
if not code or code in run.account_cfg.excluded_codes or tick is None:
continue
price = tick.last_price
if price <= 0 or price > run.account_cfg.zt_max_price:
continue
try:
state = run.state.get(code)
except KeyError:
continue
if state.phase == READY and not force_buy_back:
_try_sell(run, state, position, price, today)
elif state.phase == SOLD:
_try_buy_back(run, state, price, available, today, force_buy_back)
def _try_sell(run, state, position: PositionItem, price: float, today: str) -> None:
if state.base_cost <= 0 or run.orders.busy(state.code, "SELL"):
return
pnl_rate = (price - state.base_cost) / state.base_cost * 100
observation = run.sell_tracker.observe(f"{run.account_cfg.account_id}:{state.code}", pnl_rate)
if observation.state != GridState.RETREAT:
return
volume = min(position.can_use_volume, int(state.base_qty * run.account_cfg.zt_sell_ratio) // 100 * 100)
if volume <= 0:
return
order_id = run.orders.new_order_id("t-sell")
request = PlaceOrderRequest(run.client, OP_SELL, state.code, volume, order_id, run.account_cfg.strategy)
if not run.orders.place(request):
return
state.trade_date, state.phase = today, SELLING
state.sell_order_id, state.sell_qty, state.sell_price = order_id, volume, price
run.state.set(state)
run.state.save()
logging.info("[ZT 卖出] %s %d 股,网格回撤触发", state.code, volume)
def _try_buy_back(run, state, price: float, available: float, today: str, force: bool) -> None:
target = state.sell_price * (1 - run.account_cfg.zt_buy_fall_pct / 100)
if (not force and price > target) or run.orders.busy(state.code, "BUY"):
return
if state.sell_qty <= 0 or price * state.sell_qty > available:
return
if not force and not run.buy_watch.triggered("ZT 买回", state.code, price):
return
order_id = run.orders.new_order_id("t-buy")
request = PlaceOrderRequest(run.client, OP_BUY, state.code, state.sell_qty, order_id, run.account_cfg.strategy)
if not run.orders.place(request):
return
state.trade_date, state.phase, state.buy_order_id = today, BUYING, order_id
run.state.set(state)
run.state.save()
run.buy_watch.forget(state.code)
reason = "尾盘强制买回" if force else f"回撤 {run.account_cfg.zt_buy_fall_pct:.2f}% 后反弹确认"
logging.info("[ZT 买回] %s %d 股,%s", state.code, state.sell_qty, reason)

View File

@@ -0,0 +1,22 @@
"""做 T 策略的运行期依赖。"""
from dataclasses import dataclass
from config import AccountConfig, GlobalConfig
from libs.grid_take_profit import GridTrailingTracker
from sdk import Client
from strategy.trend.order import OrderBook
from strategy.trend.watch import DipWatch
from .state import TState
@dataclass(slots=True)
class Runtime:
client: Client
global_cfg: GlobalConfig
account_cfg: AccountConfig
state: TState
orders: OrderBook
buy_watch: DipWatch
sell_tracker: GridTrailingTracker

View File

@@ -0,0 +1,103 @@
"""做 T 策略的底仓与日内轮次状态。"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from pathlib import Path
from threading import Lock
from typing import Iterable
from sdk import OrderItem, PositionItem
READY = "READY"
SELLING = "SELLING"
SOLD = "SOLD"
BUYING = "BUYING"
DONE = "DONE"
@dataclass(slots=True)
class TStateItem:
code: str
base_qty: int = 0
base_cost: float = 0.0
trade_date: str = ""
phase: str = READY
sell_order_id: str = ""
sell_qty: int = 0
sell_price: float = 0.0
buy_order_id: str = ""
class TState:
"""持久化 dcm 底仓和每只证券每日一次的做 T 进度。"""
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self.lock = Lock()
self.items = self._load()
@classmethod
def for_strategy(cls, data_dir: str | Path, strategy: str, account_id: str) -> "TState":
return cls(Path(data_dir) / f"{strategy}_{account_id}_state.json")
def get(self, code: str) -> TStateItem:
with self.lock:
return self.items[code]
def set(self, item: TStateItem) -> None:
with self.lock:
self.items[item.code] = item
def reconcile(self, positions: Iterable[PositionItem], orders: list[OrderItem], today: str) -> None:
position_list = [item for item in positions if item.stock_code and item.volume > 0]
position_codes = {item.stock_code for item in position_list}
by_local_id: dict[str, list[OrderItem]] = {}
for order in orders:
if order.local_order_id:
by_local_id.setdefault(order.local_order_id, []).append(order)
for position in position_list:
if position.stock_code not in self.items and position.open_price > 0:
self.set(TStateItem(position.stock_code, position.volume, position.open_price))
for code in list(self.items):
item = self.get(code)
if code not in position_codes:
with self.lock:
self.items.pop(code, None)
continue
if item.trade_date and item.trade_date != today and item.phase in {DONE, READY}:
item.trade_date, item.phase = "", READY
item.sell_order_id = item.buy_order_id = ""
item.sell_qty = 0
item.sell_price = 0.0
if item.phase == SELLING and _completed(by_local_id.get(item.sell_order_id)):
item.phase = SOLD
elif item.phase == BUYING and _completed(by_local_id.get(item.buy_order_id)):
item.phase = DONE
self.set(item)
self.save()
def save(self) -> None:
with self.lock:
self.path.parent.mkdir(parents=True, exist_ok=True)
temporary = self.path.with_suffix(self.path.suffix + ".tmp")
temporary.write_text(json.dumps({key: asdict(value) for key, value in self.items.items()}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
temporary.replace(self.path)
def _load(self) -> dict[str, TStateItem]:
try:
raw = json.loads(self.path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
except (OSError, json.JSONDecodeError) as exc:
raise ValueError(f"[ZT 状态] 读取失败: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("[ZT 状态] 根节点必须是对象")
return {code: TStateItem(**value) for code, value in raw.items()}
def _completed(orders: list[OrderItem] | None) -> bool:
return bool(orders) and all(order.status == "56" for order in orders)