feat client.
This commit is contained in:
@@ -36,6 +36,8 @@ def AutoBuyIpo() -> int:
|
||||
|
||||
result = client.ipo_data("STOCK")
|
||||
for stock in result:
|
||||
if ".BJ" in stock:
|
||||
continue
|
||||
lp = Path(config.global_config.qmt_data_dir)/f"{stock}.lock"
|
||||
if not is_lock(lp):
|
||||
ipo_price = result[stock]['issuePrice'] # 发行价
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,7 +10,9 @@ import time
|
||||
from datetime import datetime
|
||||
|
||||
import config
|
||||
from libs import init_signals, market_allow_open, trading_time
|
||||
from libs.calc import trading_time
|
||||
from libs.market import market_allow_open
|
||||
from libs.signal import init_signals, SignalItem
|
||||
from sdk import Client
|
||||
from libs.grid_take_profit import GridTrailingTracker
|
||||
from .state import State
|
||||
@@ -113,16 +115,18 @@ def StartTrend() -> None:
|
||||
time.sleep(max(0.0, 30.0 - elapsed))
|
||||
|
||||
|
||||
def RunOnce(run: Runtime, signals) -> None:
|
||||
def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
"""按固定步骤执行一轮趋势策略, ``RunOnce``。"""
|
||||
if not trading_time(datetime.now()):
|
||||
return
|
||||
|
||||
# 1. 取消超过有效期仍未完成的委托订单。
|
||||
# 1. 刷新订单数据,清理过期订单。
|
||||
try:
|
||||
run.orders.refresh(run.client)
|
||||
except Exception:
|
||||
logging.exception("取消过期订单失败")
|
||||
return
|
||||
|
||||
|
||||
# 2. 验证可用资金;低于资金安全线时禁止开新仓。
|
||||
try:
|
||||
@@ -144,37 +148,25 @@ def RunOnce(run: Runtime, signals) -> None:
|
||||
logging.exception("获取持仓失败")
|
||||
return
|
||||
|
||||
# 每轮使用最新委托和成交恢复状态。查询或落盘失败时禁止继续开仓,
|
||||
# 避免在订单结果不明确的情况下提交重复买单。
|
||||
previous_state_codes = set(run.state.codes)
|
||||
# 5. 更新状态机
|
||||
try:
|
||||
broker_orders = run.client.trade_detail_data("order")
|
||||
run.state.reconcile(positions, broker_orders)
|
||||
run.state.reconcile(positions, run.orders.data)
|
||||
except Exception:
|
||||
logging.exception("订单状态对账失败,本轮禁止自动交易")
|
||||
return
|
||||
|
||||
removed_codes = previous_state_codes - set(run.state.codes)
|
||||
for code in removed_codes:
|
||||
run.open_watch.forget(code)
|
||||
run.add_watch.forget(code)
|
||||
|
||||
# 5. 验证有效开仓信号:排除已有持仓和未决订单。
|
||||
position_code_set = set(position_codes)
|
||||
allow_open = []
|
||||
seen_codes = position_code_set | set(run.state.unresolved_codes)
|
||||
# 6. 验证有效开仓信号:排除已有持仓和未决订单。
|
||||
allow_open: list[SignalItem] = []
|
||||
allow_codes: list[str] = []
|
||||
for signal in signals:
|
||||
if signal.code not in seen_codes:
|
||||
if signal.code not in position_codes:
|
||||
allow_open.append(signal)
|
||||
seen_codes.add(signal.code)
|
||||
allow_codes.append(signal.code)
|
||||
|
||||
# 6. 获取持仓和待开仓证券的实时行情 tick。
|
||||
all_codes = list(position_codes)
|
||||
all_codes.extend(
|
||||
signal.code for signal in allow_open if signal.code not in position_code_set
|
||||
)
|
||||
all_codes = allow_codes + position_codes
|
||||
try:
|
||||
ticks = run.client.full_tick(list(dict.fromkeys(all_codes)))
|
||||
ticks = run.client.full_tick(all_codes)
|
||||
except Exception:
|
||||
logging.exception("获取行情失败")
|
||||
return
|
||||
|
||||
@@ -7,18 +7,14 @@ from datetime import datetime
|
||||
|
||||
from libs import calc_buy_volume
|
||||
from sdk import OP_BUY
|
||||
|
||||
from .runtime import Runtime
|
||||
from .order import PlaceOrderRequest
|
||||
from .state import STATUS_ING, StateItem
|
||||
|
||||
|
||||
def open_signal(run, ticks, open_signals) -> None:
|
||||
def open_signal(run:Runtime, ticks, open_signals) -> None:
|
||||
"""逐个验证开仓信号并提交买入委托。"""
|
||||
for item in open_signals:
|
||||
# 候选生成后状态仍可能发生变化,提交前再次阻止未决订单重复开仓。
|
||||
if run.state.has_unresolved_order(item.code):
|
||||
continue
|
||||
|
||||
# 1. 验证信号配置允许开仓的时间区间。
|
||||
signal_config = run.global_cfg.signals.get(item.signal_key)
|
||||
if signal_config is None or not check_timezone(signal_config.timezone):
|
||||
@@ -34,44 +30,44 @@ def open_signal(run, ticks, open_signals) -> None:
|
||||
if price <= 0:
|
||||
continue
|
||||
|
||||
# 4. 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。
|
||||
if not run.open_watch.triggered("开仓", item.code, price):
|
||||
continue
|
||||
|
||||
# 5. 根据单笔买入金额计算整手开仓数量。
|
||||
volume = calc_buy_volume(price, run.account_cfg.buy_value)
|
||||
if volume <= 0:
|
||||
continue
|
||||
|
||||
# 6. 生成本地订单号并按最新价提交开仓委托。
|
||||
order_id = run.orders.new_order_id("base")
|
||||
request = PlaceOrderRequest(
|
||||
run.client,
|
||||
OP_BUY,
|
||||
item.code,
|
||||
volume,
|
||||
order_id,
|
||||
item.signal_key,
|
||||
)
|
||||
if not run.orders.place(request):
|
||||
# 当前价高于昨收价可开仓
|
||||
if signal_config.gt_last_price_is_open and item.last_close>0 and price>item.last_close:
|
||||
try:
|
||||
do_open(run,item.code,volume,item.signal_key)
|
||||
logging.info("[开仓] %s 买入 %d 股", item.code, volume)
|
||||
except Exception as e:
|
||||
logging.exception("[开仓] %s 失败: %s", item.code,e)
|
||||
continue
|
||||
|
||||
# 4. 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。
|
||||
if not run.open_watch.triggered("开仓", item.code, price):
|
||||
continue
|
||||
|
||||
# 7. 保存底仓订单、数量、成本和处理中状态。
|
||||
run.state.set(
|
||||
StateItem(
|
||||
code=item.code,
|
||||
base_order_id=order_id,
|
||||
base_qty=volume,
|
||||
base_cost=price,
|
||||
base_status=STATUS_ING,
|
||||
)
|
||||
)
|
||||
try:
|
||||
run.state.save()
|
||||
except OSError:
|
||||
logging.exception("[状态] %s 开仓状态保存失败", item.code)
|
||||
run.open_watch.forget(item.code)
|
||||
logging.info("[ZT][开仓] %s 买入 %d 股", item.code, volume)
|
||||
do_open(run,item.code,volume,item.signal_key)
|
||||
logging.info("[开仓] %s 买入 %d 股", item.code, volume)
|
||||
except Exception as e:
|
||||
logging.exception("[开仓] %s 失败: %s", item.code,e)
|
||||
|
||||
|
||||
def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None:
|
||||
"""生成本地订单号并按最新价提交开仓委托。"""
|
||||
order_id = run.orders.new_order_id("base")
|
||||
request = PlaceOrderRequest(
|
||||
run.client,
|
||||
OP_BUY,
|
||||
code,
|
||||
volume,
|
||||
order_id,
|
||||
signal_key,
|
||||
)
|
||||
if not run.orders.place(request):
|
||||
raise RuntimeError("订单提交失败")
|
||||
|
||||
|
||||
def check_timezone(timezone: str, now: datetime | None = None) -> bool:
|
||||
|
||||
5
py-client/strategy/zt/__init__.py
Normal file
5
py-client/strategy/zt/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""日内做 T 策略。"""
|
||||
|
||||
from .boot import StartZT
|
||||
|
||||
__all__ = ["StartZT"]
|
||||
BIN
py-client/strategy/zt/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
py-client/strategy/zt/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/strategy/zt/__pycache__/boot.cpython-311.pyc
Normal file
BIN
py-client/strategy/zt/__pycache__/boot.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/strategy/zt/__pycache__/open.cpython-311.pyc
Normal file
BIN
py-client/strategy/zt/__pycache__/open.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/strategy/zt/__pycache__/positions.cpython-311.pyc
Normal file
BIN
py-client/strategy/zt/__pycache__/positions.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/strategy/zt/__pycache__/runtime.cpython-311.pyc
Normal file
BIN
py-client/strategy/zt/__pycache__/runtime.cpython-311.pyc
Normal file
Binary file not shown.
BIN
py-client/strategy/zt/__pycache__/state.cpython-311.pyc
Normal file
BIN
py-client/strategy/zt/__pycache__/state.cpython-311.pyc
Normal file
Binary file not shown.
75
py-client/strategy/zt/boot.py
Normal file
75
py-client/strategy/zt/boot.py
Normal 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),
|
||||
)
|
||||
27
py-client/strategy/zt/open.py
Normal file
27
py-client/strategy/zt/open.py
Normal 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)
|
||||
71
py-client/strategy/zt/positions.py
Normal file
71
py-client/strategy/zt/positions.py
Normal 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)
|
||||
22
py-client/strategy/zt/runtime.py
Normal file
22
py-client/strategy/zt/runtime.py
Normal 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
|
||||
103
py-client/strategy/zt/state.py
Normal file
103
py-client/strategy/zt/state.py
Normal 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)
|
||||
Reference in New Issue
Block a user