feat: 非交易时段禁开仓、移动保本与交易结果分类。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+222
-27
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user