fix trend,zt
This commit is contained in:
@@ -13,36 +13,18 @@ from datetime import datetime
|
||||
import config
|
||||
from libs.calc import trading_time
|
||||
from libs.market import market_allow_open
|
||||
from libs.overview import Overview
|
||||
from libs.signal import init_signals, SignalItem
|
||||
from libs.collector import collector_push
|
||||
from sdk import Client
|
||||
from libs.grid_take_profit import GridTrailingTracker
|
||||
from .order import OrderBook
|
||||
from .watch import DipWatch
|
||||
from .runtime import Runtime
|
||||
from libs.order import OrderBook
|
||||
from libs.watch import DipWatch
|
||||
from libs.runtime import Runtime
|
||||
from .open import open_signal
|
||||
from .positions import manage_positions
|
||||
|
||||
|
||||
def Overview(assets, positions, account_cfg=None) -> None:
|
||||
"""记录策略启动时的账户、资金和持仓概览。"""
|
||||
account_cfg = account_cfg or config.account_config
|
||||
|
||||
if account_cfg is not None:
|
||||
log.info("[启动] 账户=%s,主机=%s,单笔金额=%.2f", account_cfg.account_id, account_cfg.host_key, account_cfg.buy_value)
|
||||
|
||||
if assets is not None:
|
||||
log.info("[启动] 总资产=%.2f,可用资金=%.2f", assets.total, assets.available)
|
||||
else:
|
||||
log.warning("[启动] 获取资金概览失败")
|
||||
|
||||
for position in positions:
|
||||
if position.volume <= 0:
|
||||
continue
|
||||
log.info("[启动] %s %s %s,持仓=%d,可用=%d,成本=%.2f(%.2f),现价=%.2f,盈亏=%.2f%%",position.trade_id, position.stock_code, position.stock_name, position.volume, position.can_use_volume, position.open_price,position.open_cost, position.last_price, position.profit_rate * 100)
|
||||
|
||||
|
||||
|
||||
def StartTrend() -> None:
|
||||
"""初始化趋势策略,并以 30 秒间隔持续执行。"""
|
||||
client = Client(
|
||||
@@ -55,7 +37,7 @@ def StartTrend() -> None:
|
||||
portfolio = client.portfolio()
|
||||
assets = portfolio.assets
|
||||
positions = list(portfolio.positions.values())
|
||||
order_book = OrderBook()
|
||||
order_book = OrderBook("trend")
|
||||
order_book.refresh(client, portfolio.orders)
|
||||
|
||||
# 获取本策略的信号开仓数据
|
||||
@@ -63,7 +45,12 @@ def StartTrend() -> None:
|
||||
config.global_config,
|
||||
config.account_config.signal_allow,
|
||||
)
|
||||
log.info("[启动] 趋势策略已启动,账户=%s,信号=%d,持仓=%d", config.account_config.account_id, len(signals), len(positions))
|
||||
log.info(
|
||||
"[启动] 趋势策略已启动,账户=%s,信号=%d,持仓=%d",
|
||||
config.account_config.account_id,
|
||||
len(signals),
|
||||
len(positions),
|
||||
)
|
||||
executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="trend")
|
||||
run = Runtime(
|
||||
client=client,
|
||||
@@ -101,7 +88,9 @@ def StartTrend() -> None:
|
||||
try:
|
||||
RunOnce(run, signals)
|
||||
except Exception as e:
|
||||
log.error(f"[Trend] 本 tick 执行失败,下一 tick 继续: {e}", exc_info=True)
|
||||
log.error(
|
||||
f"[Trend] 本 tick 执行失败,下一 tick 继续: {e}", exc_info=True
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
if executor is not None:
|
||||
@@ -110,13 +99,15 @@ def StartTrend() -> None:
|
||||
client.close()
|
||||
|
||||
|
||||
def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
def RunOnce(run: Runtime, signals: list[SignalItem]) -> None:
|
||||
"""按固定步骤执行一轮趋势策略, ``RunOnce``。"""
|
||||
if not trading_time(datetime.now()):
|
||||
return
|
||||
|
||||
print("=" * 40 + f" Ticker {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} " +"=" * 40)
|
||||
|
||||
print(
|
||||
"=" * 40 + f" Ticker {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} " + "=" * 40
|
||||
)
|
||||
|
||||
started_at = time.monotonic()
|
||||
|
||||
# 1. 一次获取资产、持仓和订单,并清理过期订单。
|
||||
@@ -131,21 +122,27 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
return
|
||||
|
||||
futures: list[tuple[str, Future]] = [
|
||||
(
|
||||
"数据提交",
|
||||
run.executor.submit(
|
||||
collector_push,
|
||||
run.account_cfg.account_id,
|
||||
assets,
|
||||
positions,
|
||||
),
|
||||
)
|
||||
]
|
||||
(
|
||||
"数据提交",
|
||||
run.executor.submit(
|
||||
collector_push,
|
||||
run.account_cfg.account_id,
|
||||
assets,
|
||||
positions,
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
# 2. 验证可用资金;低于资金安全线时禁止开新仓。
|
||||
allow_open_by_cash = assets.available >= assets.total * run.account_cfg.min_cash_ratio
|
||||
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)
|
||||
log.info(
|
||||
"[Status] 禁止开仓:可用资金不足,可用=%.2f,总资产=%.2f",
|
||||
assets.available,
|
||||
assets.total,
|
||||
)
|
||||
|
||||
# 3. 获取大盘状态,只有大盘信号允许时才执行开仓。
|
||||
market_ok = market_allow_open()
|
||||
@@ -169,20 +166,37 @@ def RunOnce(run: Runtime, signals:list[SignalItem]) -> None:
|
||||
log.exception("[行情] 获取行情失败,代码数量=%d", len(all_codes))
|
||||
return
|
||||
|
||||
log.info("[RunOnce] 本轮就绪,持仓=%d,候选=%d,大盘允许=%s,资金允许=%s", len(positions), len(allow_open), market_ok, allow_open_by_cash)
|
||||
|
||||
log.info(
|
||||
"[RunOnce] 本轮就绪,持仓=%d,候选=%d,大盘允许=%s,资金允许=%s",
|
||||
len(positions),
|
||||
len(allow_open),
|
||||
market_ok,
|
||||
allow_open_by_cash,
|
||||
)
|
||||
|
||||
# 启动线程,开始计算
|
||||
# 7. 持仓计算。
|
||||
futures.append(("持仓计算",run.executor.submit(manage_positions,run,ticks,positions,market_ok,assets.available)))
|
||||
futures.append(
|
||||
(
|
||||
"持仓计算",
|
||||
run.executor.submit(
|
||||
manage_positions, run, ticks, positions, market_ok, assets.available
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# 8. 开仓计算:必须同时存在有效信号且大盘允许开仓。
|
||||
if allow_open and market_ok and allow_open_by_cash:
|
||||
futures.append(("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open)))
|
||||
futures.append(
|
||||
("开仓计算", run.executor.submit(open_signal, run, ticks, allow_open))
|
||||
)
|
||||
|
||||
# 9. 开始执行
|
||||
for name, future in futures:
|
||||
_wait_worker(name, future)
|
||||
log.info("[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000))
|
||||
log.info(
|
||||
"[RunOnce] 本轮完成,耗时=%d毫秒", int((time.monotonic() - started_at) * 1000)
|
||||
)
|
||||
|
||||
|
||||
def _wait_worker(name: str, future: Future) -> None:
|
||||
|
||||
@@ -6,16 +6,18 @@ from datetime import datetime
|
||||
|
||||
from libs import calc_buy_volume
|
||||
from sdk import OP_BUY
|
||||
from .runtime import Runtime
|
||||
from .order import PlaceOrderRequest
|
||||
from libs.runtime import Runtime
|
||||
from libs.order import PlaceOrderRequest
|
||||
import logging as log
|
||||
|
||||
|
||||
def open_signal(run:Runtime, ticks, open_signals) -> None:
|
||||
def open_signal(run: Runtime, ticks, open_signals) -> None:
|
||||
"""逐个验证开仓信号并提交买入委托。"""
|
||||
for item in open_signals:
|
||||
if item.code in run.account_cfg.excluded_codes:
|
||||
log.info("[Open] %s 信号=%s,跳过:已配置为排除股票", item.code, item.signal_key)
|
||||
log.info(
|
||||
"[Open] %s 信号=%s,跳过:已配置为排除股票", item.code, item.signal_key
|
||||
)
|
||||
continue
|
||||
# 1. 验证信号配置允许开仓的时间区间。
|
||||
signal_config = run.global_cfg.signals.get(item.signal_key)
|
||||
@@ -36,8 +38,10 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
|
||||
continue
|
||||
|
||||
# 2. 检查该证券是否已有买入委托锁,防止重复下单。
|
||||
if run.orders.busy(item.code,"BUY"):
|
||||
log.info("[Open] %s 信号=%s,跳过:买入委托处理中", item.code, item.signal_key)
|
||||
if run.orders.busy(item.code, "BUY"):
|
||||
log.info(
|
||||
"[Open] %s 信号=%s,跳过:买入委托处理中", item.code, item.signal_key
|
||||
)
|
||||
continue
|
||||
|
||||
# 3. 验证行情和最新价格是否有效。
|
||||
@@ -54,14 +58,34 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
|
||||
continue
|
||||
|
||||
# 当前价高于昨收价可开仓
|
||||
if signal_config.gt_last_price_is_open and item.last_close>0 and price>item.last_close:
|
||||
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, price)
|
||||
log.info("[Open] %s 信号=%s,买入=%d股,原因=现价高于昨收", item.code, item.signal_key, volume)
|
||||
log.info(
|
||||
"[Open] %s 信号=%s,买入=%d股,原因=现价高于昨收",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
volume,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
log.info("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc)
|
||||
log.info(
|
||||
"[Open] %s 信号=%s,买入=%d股失败:%s",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
volume,
|
||||
exc,
|
||||
)
|
||||
except Exception:
|
||||
log.exception("[Open] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume)
|
||||
log.exception(
|
||||
"[Open] %s 信号=%s,买入=%d股异常",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
volume,
|
||||
)
|
||||
continue
|
||||
|
||||
# 4. 等待价格从观察低点反弹,防止直接接下跌中的“飞刀”。
|
||||
@@ -70,14 +94,29 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
|
||||
|
||||
try:
|
||||
do_open(run, item.code, volume, item.signal_key, price)
|
||||
log.info("[Open] %s 信号=%s,买入=%d股,原因=反弹已确认", item.code, item.signal_key, volume)
|
||||
log.info(
|
||||
"[Open] %s 信号=%s,买入=%d股,原因=反弹已确认",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
volume,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
log.warning("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc)
|
||||
log.warning(
|
||||
"[Open] %s 信号=%s,买入=%d股失败:%s",
|
||||
item.code,
|
||||
item.signal_key,
|
||||
volume,
|
||||
exc,
|
||||
)
|
||||
except Exception:
|
||||
log.exception("[Open] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume)
|
||||
|
||||
log.exception(
|
||||
"[Open] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume
|
||||
)
|
||||
|
||||
def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float) -> None:
|
||||
|
||||
def do_open(
|
||||
run: Runtime, code: str, volume: int, signal_key: str, price: float
|
||||
) -> None:
|
||||
"""生成本地订单号并按最新价提交开仓委托。"""
|
||||
order_id = run.orders.new_order_id("BUY")
|
||||
request = PlaceOrderRequest(
|
||||
@@ -89,9 +128,7 @@ def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float)
|
||||
kind="base",
|
||||
)
|
||||
|
||||
#run.state.new_order(PendingOrder(order_id, code, "base", volume))
|
||||
|
||||
if not run.orders.place(run.client,request):
|
||||
if not run.orders.place(run.client, request):
|
||||
raise RuntimeError("订单提交失败")
|
||||
|
||||
run.open_watch.forget(code)
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
"""趋势策略委托簿,对应 Go 客户端的 ``logic/order.go``。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Lock
|
||||
from cachelib import SimpleCache
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
from sdk import Client,ORDER_SIDE_BY_OFFSET,APIError,OrderItem
|
||||
|
||||
# 表示委托仍在处理、可能继续成交的 QMT 状态。
|
||||
BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"}
|
||||
COMPLETED_STATUSES = {"56"}
|
||||
TRACKED_STATUSES = BUSY_STATUSES | COMPLETED_STATUSES
|
||||
CANCELABLE_STATUSES = {"49", "50", "51", "52"}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PlaceOrderRequest:
|
||||
"""``OrderBook.place`` 提交委托所需的全部参数。"""
|
||||
op: int
|
||||
code: str
|
||||
volume: int
|
||||
order_id: str
|
||||
strategy_name: str
|
||||
kind: str = ""
|
||||
|
||||
|
||||
class OrderBook:
|
||||
"""线程安全的活动委托缓存。"""
|
||||
|
||||
def __init__(self, lock_timeout_sec: int = 180, cancel_timeout_sec: float = 10) -> None:
|
||||
self.lock_timeout_sec = max(1, lock_timeout_sec)
|
||||
self.cancel_timeout_sec = timedelta(seconds=cancel_timeout_sec)
|
||||
self.data: list[OrderItem] = []
|
||||
self.busy_keys: set[str] = set()
|
||||
self.busy_cache = SimpleCache(threshold=10_000, default_timeout=self.lock_timeout_sec)
|
||||
self.mutex = Lock()
|
||||
|
||||
@staticmethod
|
||||
def new_order_id(side:str) -> str:
|
||||
"""生成 ``trend-xxxxxxxx`` 格式的本地订单号。"""
|
||||
return f"trend-{side}-{secrets.token_hex(10)}"
|
||||
|
||||
def busy(self, code: str, side: str) -> bool:
|
||||
"""判断证券是否存在仍在处理中的同方向委托。"""
|
||||
with self.mutex:
|
||||
key = self._busy_key(side, code)
|
||||
return key in self.busy_keys or self.busy_cache.has(key)
|
||||
|
||||
@staticmethod
|
||||
def _busy_key(side: str, code: str) -> str:
|
||||
return f"{side}-{code}"
|
||||
|
||||
def refresh(self, client: Client, orders: list[OrderItem]) -> None:
|
||||
"""用账户快照刷新委托,并撤销超时的活动委托。"""
|
||||
current = datetime.now()
|
||||
data: list[OrderItem] = []
|
||||
busy_keys: set[str] = set()
|
||||
canceled = 0
|
||||
|
||||
for item in orders:
|
||||
# 不处理状态不对的
|
||||
if item.status not in TRACKED_STATUSES:
|
||||
continue
|
||||
if item.status in BUSY_STATUSES:
|
||||
busy_keys.add(self._busy_key(item.side, item.code))
|
||||
# 清理过期的
|
||||
if (
|
||||
item.created_at is not None
|
||||
and item.status in CANCELABLE_STATUSES
|
||||
and current - item.created_at > self.cancel_timeout_sec
|
||||
):
|
||||
client.cancel_by_id(item.id)
|
||||
canceled += 1
|
||||
logging.info("[Order] 超时撤单,代码=%s,方向=%s,柜台订单=%s", item.code, item.side, item.id)
|
||||
continue
|
||||
|
||||
# 缓存本次有效订单
|
||||
data.append(item)
|
||||
|
||||
with self.mutex:
|
||||
self.data = data
|
||||
self.busy_keys = busy_keys
|
||||
logging.info("[Order] 刷新完成,跟踪=%d,处理中=%d,撤销=%d", len(data), len(busy_keys), canceled)
|
||||
|
||||
def place(self, client: Client, request: PlaceOrderRequest) -> bool:
|
||||
"""按最新价提交委托,并立即写入本地方向锁。"""
|
||||
side = ORDER_SIDE_BY_OFFSET.get(str(request.op), "")
|
||||
if not side:
|
||||
logging.warning("[Order] 下单失败,代码=%s,原因=未知买卖方向(%s)", request.code, request.op)
|
||||
return False
|
||||
|
||||
key = self._busy_key(side, request.code)
|
||||
with self.mutex:
|
||||
if key in self.busy_keys or self.busy_cache.has(key):
|
||||
logging.info("[Order] 跳过重复下单,代码=%s,方向=%s", request.code, side)
|
||||
return False
|
||||
self.busy_cache.set(key, True, timeout=self.lock_timeout_sec)
|
||||
|
||||
try:
|
||||
result = client.passorder(
|
||||
op_type=request.op,
|
||||
stock_code=request.code,
|
||||
volume=request.volume,
|
||||
strategy_name=request.strategy_name,
|
||||
order_id=request.order_id,
|
||||
)
|
||||
except APIError as exc:
|
||||
logging.exception("[Order] 下单失败,代码=%s,本地订单=%s,HTTP状态=%d,错误=%s", request.code, request.order_id, exc.status_code, exc.message or str(exc))
|
||||
return False
|
||||
except (httpx.RequestError, ValueError):
|
||||
# 响应异常不能证明柜台未受理,保留缓存防重,不自动重试。
|
||||
logging.exception("[Order] 下单请求或响应异常,代码=%s,本地订单=%s", request.code, request.order_id)
|
||||
return False
|
||||
|
||||
logging.info("[Order] 下单已受理,代码=%s,方向=%s,数量=%d,本地订单=%s,返回=%s", request.code, side, request.volume, request.order_id, result)
|
||||
return True
|
||||
@@ -8,8 +8,8 @@ from libs.calc import calc_buy_volume, calculate_min_profit_rate
|
||||
from libs.grid_take_profit import GridState
|
||||
from sdk import OP_BUY, OP_SELL, PositionItem, Tick
|
||||
|
||||
from .order import PlaceOrderRequest
|
||||
from .runtime import Runtime
|
||||
from libs.order import PlaceOrderRequest
|
||||
from libs.runtime import Runtime
|
||||
import logging as log
|
||||
|
||||
LOSS_TIERS = [-50.0]
|
||||
@@ -34,11 +34,15 @@ def manage_positions(
|
||||
# 遍历处理每个持仓
|
||||
for position in positions:
|
||||
try:
|
||||
available = max(0,0,available)
|
||||
available = max(0, 0, available)
|
||||
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
|
||||
@@ -47,7 +51,11 @@ 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(
|
||||
@@ -77,18 +85,29 @@ def manage_positions(
|
||||
elif runtime.account_cfg.enable_loss_add_position:
|
||||
loss_add_action = "大盘信号不允许"
|
||||
|
||||
if pnl_rate>=0:
|
||||
if pnl_rate >= 0:
|
||||
log.info(
|
||||
"[Position ↑ ] 代码=%s,名称=%s,盈亏=%.2f%%,止盈=%s,补仓=%s",
|
||||
code, position.stock_name, pnl_rate, profit_action, loss_add_action,
|
||||
code,
|
||||
position.stock_name,
|
||||
pnl_rate,
|
||||
profit_action,
|
||||
loss_add_action,
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
"[Position ↓ ] 代码=%s,名称=%s,盈亏=%.2f%%,止盈=%s,补仓=%s",
|
||||
code, position.stock_name, pnl_rate, profit_action, loss_add_action,
|
||||
code,
|
||||
position.stock_name,
|
||||
pnl_rate,
|
||||
profit_action,
|
||||
loss_add_action,
|
||||
)
|
||||
except Exception:
|
||||
log.exception("[Position] 持仓处理异常,代码=%s,继续处理后续持仓", position.stock_code)
|
||||
log.exception(
|
||||
"[Position] 持仓处理异常,代码=%s,继续处理后续持仓",
|
||||
position.stock_code,
|
||||
)
|
||||
|
||||
|
||||
def handle_profit(
|
||||
@@ -132,8 +151,7 @@ def handle_profit(
|
||||
)
|
||||
if not runtime.orders.place(runtime.client, request):
|
||||
return TradeDecision(False, "止盈委托失败")
|
||||
|
||||
|
||||
|
||||
return TradeDecision(True, f"卖出 {volume} 股,订单={order_id}")
|
||||
|
||||
|
||||
@@ -142,10 +160,12 @@ def handle_loss(
|
||||
position: PositionItem,
|
||||
tick: Tick,
|
||||
pnl_rate: float,
|
||||
available: float
|
||||
available: float,
|
||||
) -> TradeDecision:
|
||||
"""按亏损档位、反弹确认和本轮剩余预算提交补仓。"""
|
||||
add_num = get_add_num(hands=int(position.volume/100),market_value=position.market_value)
|
||||
add_num = get_add_num(
|
||||
hands=int(position.volume / 100), market_value=position.market_value
|
||||
)
|
||||
if add_num >= len(LOSS_TIERS) or add_num < 0:
|
||||
return TradeDecision(False, f"补仓次数无效:{add_num}")
|
||||
if pnl_rate > LOSS_TIERS[add_num]:
|
||||
@@ -171,10 +191,10 @@ def handle_loss(
|
||||
strategy_name=runtime.account_cfg.strategy,
|
||||
kind="add",
|
||||
)
|
||||
|
||||
|
||||
if not runtime.orders.place(runtime.client, request):
|
||||
return TradeDecision(False, "补仓订单委托失败")
|
||||
|
||||
|
||||
runtime.add_watch.forget(position.stock_code)
|
||||
return TradeDecision(True, f"买入 {volume} 股,订单={order_id}", amount)
|
||||
|
||||
@@ -182,9 +202,10 @@ def handle_loss(
|
||||
def _position_key(runtime: Runtime, code: str) -> str:
|
||||
return f"{runtime.account_cfg.account_id}:{code}"
|
||||
|
||||
def get_add_num(hands:int,market_value:float) -> int:
|
||||
if market_value>10000:
|
||||
|
||||
def get_add_num(hands: int, market_value: float) -> int:
|
||||
if market_value > 10000:
|
||||
return -1
|
||||
if hands < 2:
|
||||
return 0
|
||||
return -1
|
||||
return -1
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""趋势策略单次运行所需的上下文对象。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from config import AccountConfig, GlobalConfig
|
||||
from sdk import Client
|
||||
from libs.grid_take_profit import GridTrailingTracker
|
||||
|
||||
from .order import OrderBook
|
||||
from .watch import DipWatch
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Runtime:
|
||||
"""集中保存趋势策略运行期间共享的依赖和状态。
|
||||
|
||||
将这些对象集中到一个 dataclass 后,开仓、持仓管理和单轮调度函数
|
||||
只需接收一个 ``Runtime``,无需重复传递大量参数。
|
||||
|
||||
Attributes:
|
||||
client: QMT HTTP 客户端,用于查询账户、行情和提交委托。
|
||||
global_cfg: 公共配置,包含 QMT、外部 API 和信号配置。
|
||||
account_cfg: 当前主机的账户及交易策略配置。
|
||||
state: 策略持仓状态的本地持久化存储。
|
||||
orders: 当前活动委托和证券方向锁。
|
||||
open_watch: 新开仓使用的价格反弹观察器。
|
||||
add_watch: 亏损补仓使用的价格反弹观察器。
|
||||
profit_tracker: 跨轮保存的账户持仓最高盈利网格跟踪器。
|
||||
"""
|
||||
|
||||
# 外部服务与账户配置。
|
||||
client: Client
|
||||
global_cfg: GlobalConfig
|
||||
account_cfg: AccountConfig
|
||||
|
||||
# 策略运行过程中共享的状态组件。
|
||||
orders: OrderBook
|
||||
open_watch: DipWatch
|
||||
add_watch: DipWatch
|
||||
profit_tracker: GridTrailingTracker
|
||||
executor: ThreadPoolExecutor
|
||||
@@ -1,104 +0,0 @@
|
||||
import logging as log
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from threading import Lock
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _Entry:
|
||||
last_close: float
|
||||
expires_at: datetime
|
||||
|
||||
|
||||
class DipWatch:
|
||||
"""观察价格低点,并在价格达到指定反弹幅度时触发。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
expire_seconds: float = 300,
|
||||
rebound_threshold: float = 1.5, # 反弹力度 1.5%
|
||||
) -> None:
|
||||
self.expire_seconds = expire_seconds
|
||||
self.rebound_threshold = rebound_threshold
|
||||
self.data: dict[str, _Entry] = {}
|
||||
self.lock = Lock()
|
||||
|
||||
def triggered(
|
||||
self,
|
||||
tag: str,
|
||||
code: str,
|
||||
price: float,
|
||||
now: datetime | None = None,
|
||||
) -> bool:
|
||||
"""更新观察价格;达到反弹阈值时返回 ``True``。"""
|
||||
if price <= 0:
|
||||
log.warning("[%s Watch] %s 价格无效:%.2f", tag, code, price)
|
||||
return False
|
||||
|
||||
current = now or datetime.now()
|
||||
with self.lock:
|
||||
watch = self.data.get(code)
|
||||
|
||||
if watch is None:
|
||||
self._start(code, price, current)
|
||||
log.info(
|
||||
"[%s Watch] %s 开始观察,收盘价=%.2f",
|
||||
tag,
|
||||
code,
|
||||
price,
|
||||
)
|
||||
return False
|
||||
|
||||
if current >= watch.expires_at:
|
||||
self._start(code, price, current)
|
||||
log.info("[%sWatch] %s 观察已过期,重新观察,收盘价=%.2f", tag, code, price)
|
||||
return False
|
||||
|
||||
if price < watch.last_close:
|
||||
old_price = watch.last_close
|
||||
self._start(code, price, current)
|
||||
log.info(
|
||||
"[%s Watch] %s 刷新低点,原收盘价=%.2f,新收盘价=%.2f",
|
||||
tag,
|
||||
code,
|
||||
old_price,
|
||||
price,
|
||||
)
|
||||
return False
|
||||
|
||||
rebound = (price - watch.last_close) / watch.last_close * 100
|
||||
if rebound < self.rebound_threshold:
|
||||
log.info(
|
||||
"[%s Watch] %s 等待反弹,收盘价=%.2f,现价=%.2f,反弹=%.2f%%,阈值=%.2f%%",
|
||||
tag,
|
||||
code,
|
||||
watch.last_close,
|
||||
price,
|
||||
rebound,
|
||||
self.rebound_threshold,
|
||||
)
|
||||
return False
|
||||
|
||||
del self.data[code]
|
||||
log.info(
|
||||
"[%s Watch] %s 反弹触发,收盘价=%.2f,现价=%.2f,反弹=%.2f%%",
|
||||
tag,
|
||||
code,
|
||||
watch.last_close,
|
||||
price,
|
||||
rebound,
|
||||
)
|
||||
return True
|
||||
|
||||
def forget(self, code: str) -> None:
|
||||
"""清除指定股票的价格观察状态。"""
|
||||
with self.lock:
|
||||
removed = self.data.pop(code, None)
|
||||
if removed is not None:
|
||||
log.info("[Watch] %s 已清除观察状态", code)
|
||||
|
||||
def _start(self, code: str, price: float, now: datetime) -> None:
|
||||
self.data[code] = _Entry(
|
||||
last_close=price,
|
||||
expires_at=now + timedelta(seconds=self.expire_seconds),
|
||||
)
|
||||
Reference in New Issue
Block a user