feat: 非交易时段禁开仓、移动保本与交易结果分类。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 13:33:17 +08:00
parent 598a1407e1
commit f31164076f
9 changed files with 387 additions and 49 deletions
+222 -27
View File
@@ -37,8 +37,13 @@ _closing_lock = threading.Lock()
MONITOR_ORDER_COLUMNS = (
"ALTER TABLE trade_order_monitors ADD COLUMN sl_vt_order_id TEXT",
"ALTER TABLE trade_order_monitors ADD COLUMN tp_vt_order_id TEXT",
"ALTER TABLE trade_order_monitors ADD COLUMN trailing_be INTEGER DEFAULT 0",
"ALTER TABLE trade_order_monitors ADD COLUMN initial_stop_loss REAL",
"ALTER TABLE trade_order_monitors ADD COLUMN trailing_r_locked INTEGER DEFAULT 0",
)
TRADE_RESULTS = ("止损", "止盈", "移动止盈", "保本止盈", "手动平仓")
def ensure_monitor_order_columns(conn) -> None:
for sql in MONITOR_ORDER_COLUMNS:
@@ -139,31 +144,55 @@ def _monitor_type_label(raw: str) -> str:
return mapping.get(raw or "", raw or "程序监控")
def _write_trade_log(
def _result_for_close(mon: dict, reason: str) -> str:
"""平仓结果:止损 / 止盈 / 移动止盈 / 保本止盈 / 手动平仓。"""
if reason == "manual":
return "手动平仓"
if reason == "take_profit":
return "止盈"
if not mon.get("trailing_be"):
return "止损"
locked = int(mon.get("trailing_r_locked") or 0)
if locked >= 2:
return "移动止盈"
if locked >= 1:
return "保本止盈"
return "止损"
def write_trade_log(
conn,
mon: dict,
*,
symbol: str,
direction: str,
entry_price: float,
close_price: float,
reason: str,
lots: float,
result: str,
trading_mode: str,
stop_loss: Optional[float] = None,
take_profit: Optional[float] = None,
open_time: str = "",
symbol_name: str = "",
market_code: str = "",
sina_code: str = "",
monitor_type: str = "期货下单",
capital: float = 0.0,
) -> None:
"""止盈/止损触发平仓后写入 trade_logs。"""
sym = (mon.get("symbol") or "").strip()
direction = (mon.get("direction") or "long").strip().lower()
entry = float(mon.get("entry_price") or close_price)
sl_raw = mon.get("stop_loss")
tp_raw = mon.get("take_profit")
sl = float(sl_raw) if sl_raw is not None else entry
tp = float(tp_raw) if tp_raw is not None else entry
lots = float(mon.get("lots") or 1)
open_time = (mon.get("open_time") or "").strip()
"""写入 trade_logs(程序平仓 / 手动平仓)"""
sym = (symbol or "").strip()
direction = (direction or "long").strip().lower()
entry = float(entry_price or close_price)
sl = float(stop_loss) if stop_loss is not None else entry
tp = float(take_profit) if take_profit is not None else entry
close_time = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M")
codes = ths_to_codes(sym) or {}
sina_code = codes.get("sina_code") or ""
symbol_name = mon.get("symbol_name") or sym
market_code = mon.get("market_code") or codes.get("market_code") or ""
if not sina_code or not market_code:
codes = ths_to_codes(sym) or {}
sina_code = sina_code or codes.get("sina_code") or ""
market_code = market_code or codes.get("market_code") or ""
if not symbol_name:
symbol_name = sym
metrics = calc_position_metrics(
direction, entry, sl, tp, lots, close_price, capital, sym,
@@ -173,7 +202,6 @@ def _write_trade_log(
sym, entry, close_price, lots, open_time, close_time, trading_mode=trading_mode,
)
pnl_net = round(pnl - fee, 2)
result = "止盈" if reason == "take_profit" else "止损"
try:
from app import holding_to_minutes
@@ -192,11 +220,11 @@ def _write_trade_log(
symbol_name,
market_code,
sina_code,
_monitor_type_label(mon.get("monitor_type") or ""),
monitor_type,
direction,
entry,
sl_raw if sl_raw is not None else sl,
tp_raw if tp_raw is not None else tp,
stop_loss if stop_loss is not None else sl,
take_profit if take_profit is not None else tp,
close_price,
lots,
metrics.get("margin"),
@@ -206,14 +234,169 @@ def _write_trade_log(
pnl,
fee,
pnl_net,
result,
result if result in TRADE_RESULTS else "手动平仓",
),
)
try:
from stats_engine import refresh_stats_cache
refresh_stats_cache(conn, capital)
except Exception as exc:
logger.debug("stats refresh after SL/TP close: %s", exc)
logger.debug("stats refresh after close: %s", exc)
def _write_trade_log(
conn,
mon: dict,
*,
close_price: float,
reason: str,
trading_mode: str,
capital: float = 0.0,
) -> None:
sym = (mon.get("symbol") or "").strip()
sl_raw = mon.get("stop_loss")
tp_raw = mon.get("take_profit")
initial_sl = mon.get("initial_stop_loss")
write_trade_log(
conn,
symbol=sym,
direction=mon.get("direction") or "long",
entry_price=float(mon.get("entry_price") or close_price),
close_price=close_price,
lots=float(mon.get("lots") or 1),
result=_result_for_close(mon, reason),
trading_mode=trading_mode,
stop_loss=float(initial_sl) if initial_sl is not None else (
float(sl_raw) if sl_raw is not None else None
),
take_profit=float(tp_raw) if tp_raw is not None else None,
open_time=(mon.get("open_time") or "").strip(),
symbol_name=mon.get("symbol_name") or sym,
market_code=mon.get("market_code") or "",
monitor_type=_monitor_type_label(mon.get("monitor_type") or ""),
capital=capital,
)
def write_manual_close_trade_log(
conn,
mon: Optional[dict],
*,
symbol: str,
direction: str,
lots: float,
close_price: float,
entry_price: float,
trading_mode: str,
capital: float = 0.0,
stop_loss: Optional[float] = None,
take_profit: Optional[float] = None,
open_time: str = "",
symbol_name: str = "",
market_code: str = "",
) -> None:
"""程序内点击平仓按钮 → 手动平仓。"""
if mon:
write_trade_log(
conn,
symbol=(mon.get("symbol") or symbol).strip(),
direction=mon.get("direction") or direction,
entry_price=float(mon.get("entry_price") or entry_price),
close_price=close_price,
lots=float(mon.get("lots") or lots),
result="手动平仓",
trading_mode=trading_mode,
stop_loss=float(mon["initial_stop_loss"]) if mon.get("initial_stop_loss") is not None else (
float(mon["stop_loss"]) if mon.get("stop_loss") is not None else stop_loss
),
take_profit=float(mon["take_profit"]) if mon.get("take_profit") is not None else take_profit,
open_time=(mon.get("open_time") or open_time).strip(),
symbol_name=mon.get("symbol_name") or symbol_name,
market_code=mon.get("market_code") or market_code,
monitor_type=_monitor_type_label(mon.get("monitor_type") or ""),
capital=capital,
)
return
write_trade_log(
conn,
symbol=symbol,
direction=direction,
entry_price=entry_price,
close_price=close_price,
lots=lots,
result="手动平仓",
trading_mode=trading_mode,
stop_loss=stop_loss,
take_profit=take_profit,
open_time=open_time,
symbol_name=symbol_name,
market_code=market_code,
capital=capital,
)
def _update_trailing_stop_loss(
conn,
mon: dict,
mark: float,
*,
be_tick_mult: int,
) -> dict:
"""达 1R 移保本(开仓±N跳),达 2R 移 1R,依次类推。"""
if not mon.get("trailing_be"):
return mon
entry = float(mon.get("entry_price") or 0)
initial_sl = mon.get("initial_stop_loss")
if initial_sl is None:
initial_sl = mon.get("stop_loss")
try:
initial_sl_f = float(initial_sl) if initial_sl is not None else None
except (TypeError, ValueError):
return mon
if not entry or initial_sl_f is None:
return mon
direction = (mon.get("direction") or "long").strip().lower()
sym = (mon.get("symbol") or "").strip()
tick = _tick_size(sym)
r = abs(entry - initial_sl_f)
if r < tick * 0.5:
return mon
profit_r = (mark - entry) / r if direction == "long" else (entry - mark) / r
if profit_r < 1.0:
return mon
level = int(profit_r)
locked = int(mon.get("trailing_r_locked") or 0)
if level <= locked:
return mon
if level == 1:
new_sl = entry + be_tick_mult * tick if direction == "long" else entry - be_tick_mult * tick
else:
new_sl = entry + (level - 1) * r if direction == "long" else entry - (level - 1) * r
new_sl = round(new_sl, 4)
try:
current_sl = float(mon.get("stop_loss") or 0)
except (TypeError, ValueError):
current_sl = 0.0
if direction == "long" and new_sl <= current_sl + tick * 0.01:
return mon
if direction == "short" and new_sl >= current_sl - tick * 0.01:
return mon
mid = mon.get("id")
conn.execute(
"UPDATE trade_order_monitors SET stop_loss=?, trailing_r_locked=? WHERE id=?",
(new_sl, level, mid),
)
conn.commit()
mon["stop_loss"] = new_sl
mon["trailing_r_locked"] = level
logger.info("移动保本 monitor=%s %dR 止损→%s", mid, level, new_sl)
return mon
def _sl_triggered(direction: str, sl: float, mark: float, tick: float) -> bool:
@@ -359,14 +542,14 @@ def _execute_local_close(
)
conn.execute("UPDATE trade_order_monitors SET status='closed' WHERE id=?", (mon["id"],))
conn.commit()
label = "止盈" if reason == "take_profit" else "止损"
result_label = _result_for_close(mon, reason)
logger.info(
"止盈止损本地触发 monitor=%s reason=%s %s %s %d手 @%s",
mon.get("id"), reason, sym, direction, lots, mark,
"止盈止损本地触发 monitor=%s result=%s %s %s %d手 @%s",
mon.get("id"), result_label, sym, direction, lots, mark,
)
if notify_fn:
try:
notify_fn(f"{label}平仓 {sym} {direction} {lots}手 @{mark},已记入交易记录")
notify_fn(f"{result_label} {sym} {direction} {lots}手 @{mark},已记入交易记录")
except Exception as exc:
logger.debug("SL/TP notify failed: %s", exc)
@@ -377,6 +560,7 @@ def check_monitors_locally(
*,
capital: float = 0.0,
notify_fn: Callable[[str], None] | None = None,
be_tick_mult: int = 2,
) -> int:
"""扫描 active 监控,本地比对行情;触发止盈/止损(含跳空穿透)后立刻市价平仓并记交易记录。"""
ensure_monitor_order_columns(conn)
@@ -417,6 +601,13 @@ def check_monitors_locally(
continue
tick = _tick_size(sym)
if mon.get("trailing_be"):
mon = _update_trailing_stop_loss(conn, mon, mark, be_tick_mult=be_tick_mult)
try:
sl_f = float(mon["stop_loss"]) if mon.get("stop_loss") is not None else sl_f
except (TypeError, ValueError):
pass
reason = None
if tp_f is not None and _tp_triggered(direction, tp_f, mark, tick):
reason = "take_profit"
@@ -507,6 +698,7 @@ def start_sl_tp_guard_worker(
get_mode_fn: Callable[[], str],
init_tables_fn: Callable | None = None,
get_capital_fn: Callable | None = None,
get_be_tick_buffer_fn: Callable[[], int] | None = None,
notify_fn: Callable[[str], None] | None = None,
interval: int = CHECK_INTERVAL_SEC,
) -> None:
@@ -547,6 +739,9 @@ def start_sl_tp_guard_worker(
mode,
capital=capital,
notify_fn=notify_fn,
be_tick_mult=(
get_be_tick_buffer_fn() if get_be_tick_buffer_fn else 2
),
)
if n:
logger.info("止盈止损本地监控: 触发平仓 %d", n)