fix(trend): correct DCA triggers and partial-position PnL across exchanges
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+12
-235
@@ -4700,242 +4700,14 @@ def _trend_weighted_avg(old_avg, old_amt, fill_px, add_amt):
|
||||
return float(fill_px or 0)
|
||||
|
||||
|
||||
def _trend_plan_stop_status(result_label):
|
||||
if result_label == "止盈":
|
||||
return "stopped_tp"
|
||||
if result_label == "止损":
|
||||
return "stopped_sl"
|
||||
return "stopped_manual"
|
||||
|
||||
|
||||
def _trend_plan_trade_exists(conn, plan_id):
|
||||
try:
|
||||
return conn.execute(
|
||||
"SELECT id FROM trade_records WHERE trend_plan_id=? LIMIT 1",
|
||||
(int(plan_id),),
|
||||
).fetchone() is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _trend_finalize_plan(conn, row, result_label, exit_price, closed_at=None):
|
||||
"""平仓后记账、撤单、结束计划。"""
|
||||
plan_id = int(row["id"])
|
||||
active = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'",
|
||||
(plan_id,),
|
||||
).fetchone()
|
||||
if not active:
|
||||
return
|
||||
row = active
|
||||
sym = row["symbol"]
|
||||
direction = row["direction"] or "long"
|
||||
ex_sym = row["exchange_symbol"] or normalize_exchange_symbol(sym)
|
||||
closed_at = closed_at or app_now_str()
|
||||
opened_at = row["opened_at"] or app_now_str()
|
||||
hold_seconds = calc_hold_seconds(opened_at, parse_dt_for_trading_day(closed_at) or app_now())
|
||||
margin_cap = float(row["plan_margin_capital"] or 0)
|
||||
lev = int(row["leverage"] or 1)
|
||||
avg_e = float(row["avg_entry_price"] or 0)
|
||||
pnl_amount = calc_pnl(direction, avg_e, float(exit_price), margin_cap, lev)
|
||||
res = normalize_result_with_pnl(result_label, pnl_amount)
|
||||
risk_amt = calc_risk_amount_from_plan(direction, float(row["add_upper"]), float(row["stop_loss"]), margin_cap, lev)
|
||||
planned_rr = calc_rr_ratio(direction, avg_e, float(row["stop_loss"]), float(row["take_profit"]))
|
||||
try:
|
||||
cancel_all_open_orders_for_symbol(ex_sym)
|
||||
except Exception:
|
||||
try:
|
||||
cancel_gate_swap_trigger_orders(ex_sym)
|
||||
except Exception:
|
||||
pass
|
||||
st = _trend_plan_stop_status(result_label)
|
||||
cur = conn.execute(
|
||||
"UPDATE trend_pullback_plans SET status=?, message=? WHERE id=? AND status='active'",
|
||||
(st, res, plan_id),
|
||||
)
|
||||
if not getattr(cur, "rowcount", 0):
|
||||
return
|
||||
conn.commit()
|
||||
try:
|
||||
from strategy_trend_register import build_trend_config
|
||||
from strategy_wechat_notify import notify_trend_plan_ended
|
||||
|
||||
_tcfg = build_trend_config(sys.modules[__name__])
|
||||
notify_trend_plan_ended(
|
||||
_tcfg,
|
||||
plan_id=plan_id,
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
end_type=result_label,
|
||||
result_label=res,
|
||||
exit_price=float(exit_price) if exit_price is not None else None,
|
||||
pnl_amount=float(pnl_amount) if pnl_amount is not None else None,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from strategy_trend_register import build_trend_config
|
||||
|
||||
cfg = app.extensions.get("strategy_trend_cfg") or build_trend_config(
|
||||
sys.modules[__name__]
|
||||
)
|
||||
closed = conn.execute(
|
||||
"SELECT * FROM trend_pullback_plans WHERE id=?", (plan_id,)
|
||||
).fetchone()
|
||||
if closed:
|
||||
from strategy_snapshot_lib import save_trend_plan_snapshot
|
||||
|
||||
save_trend_plan_snapshot(
|
||||
cfg,
|
||||
conn,
|
||||
closed,
|
||||
result_label=result_label,
|
||||
exit_price=float(exit_price),
|
||||
pnl_amount=float(pnl_amount) if pnl_amount is not None else None,
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
if _trend_plan_trade_exists(conn, plan_id):
|
||||
return
|
||||
session_date = row["session_date"] or get_trading_day()
|
||||
session_capital = update_session_capital(conn, session_date, pnl_amount)
|
||||
insert_trade_record(
|
||||
conn,
|
||||
symbol=sym,
|
||||
monitor_type=MONITOR_TYPE_TREND,
|
||||
direction=direction,
|
||||
trigger_price=avg_e,
|
||||
stop_loss=float(row["stop_loss"]),
|
||||
initial_stop_loss=float(row["stop_loss"]),
|
||||
take_profit=float(row["take_profit"]),
|
||||
margin_capital=margin_cap,
|
||||
leverage=lev,
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
trade_style="trend_pullback",
|
||||
risk_amount=risk_amt,
|
||||
planned_rr=planned_rr,
|
||||
actual_rr=calc_actual_rr(pnl_amount, risk_amt),
|
||||
result=res,
|
||||
opened_at=opened_at,
|
||||
closed_at=closed_at,
|
||||
trend_plan_id=plan_id,
|
||||
)
|
||||
send_wechat_msg(
|
||||
build_wechat_close_message(
|
||||
symbol=sym,
|
||||
direction=direction,
|
||||
result=f"{res}({MONITOR_TYPE_TREND})",
|
||||
pnl_amount=pnl_amount,
|
||||
hold_seconds=hold_seconds,
|
||||
trigger_price=avg_e,
|
||||
current_price=float(exit_price),
|
||||
stop_loss=float(row["stop_loss"]),
|
||||
take_profit=float(row["take_profit"]),
|
||||
close_order_id="-",
|
||||
extra_note="计划本金口径:启动时合约可用余额快照;止盈由程序监控",
|
||||
session_capital_fallback=session_capital,
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def check_trend_pullback_plans():
|
||||
ok_live, _ = ensure_exchange_live_ready()
|
||||
if not ok_live:
|
||||
return
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT * FROM trend_pullback_plans WHERE status='active'").fetchall()
|
||||
for row in rows:
|
||||
try:
|
||||
sym = row["symbol"]
|
||||
direction = (row["direction"] or "long").lower()
|
||||
ex_sym = row["exchange_symbol"] or normalize_exchange_symbol(sym)
|
||||
sl = float(row["stop_loss"])
|
||||
upper = float(row["add_upper"])
|
||||
tp = float(row["take_profit"])
|
||||
lev = int(row["leverage"] or 1)
|
||||
p = get_price(sym)
|
||||
if not p:
|
||||
continue
|
||||
pf = float(p)
|
||||
last_p = row["last_mark_price"]
|
||||
last_pf = float(last_p) if last_p is not None else pf
|
||||
"""轮询趋势回调:共用 strategy_trend_register(补仓触达 + 空仓连续确认)。"""
|
||||
from strategy_trend_register import build_trend_config, check_trend_pullback_plans as _check
|
||||
|
||||
pos = get_live_position_contracts(ex_sym, direction)
|
||||
if pos is None:
|
||||
continue
|
||||
|
||||
legs_done = int(row["legs_done"] or 0)
|
||||
dca_legs = int(row["dca_legs"] or 0)
|
||||
leg_amounts = []
|
||||
try:
|
||||
leg_amounts = [float(x) for x in json.loads(row["leg_amounts_json"] or "[]")]
|
||||
except Exception:
|
||||
leg_amounts = []
|
||||
grid = []
|
||||
try:
|
||||
grid = json.loads(row["grid_prices_json"] or "[]")
|
||||
except Exception:
|
||||
grid = []
|
||||
|
||||
hit_tp = (direction == "long" and pf >= tp) or (direction == "short" and pf <= tp)
|
||||
if hit_tp and pos > 0:
|
||||
try:
|
||||
exchange.set_leverage(lev, ex_sym)
|
||||
side = "sell" if direction == "long" else "buy"
|
||||
params = build_gate_order_params(direction, reduce_only=True)
|
||||
close_resp = exchange.create_order(ex_sym, "market", side, float(pos), None, params)
|
||||
exit_p = extract_trade_price_from_order(close_resp) or pf
|
||||
except Exception as e:
|
||||
if not is_no_position_error(str(e)):
|
||||
continue
|
||||
exit_p = pf
|
||||
_trend_finalize_plan(conn, row, "止盈", exit_p)
|
||||
continue
|
||||
|
||||
if pos <= 0 and int(row["first_order_done"] or 0):
|
||||
exit_p = pf
|
||||
_trend_finalize_plan(conn, row, "止损", exit_p)
|
||||
continue
|
||||
|
||||
if int(row["first_order_done"] or 0) and legs_done < len(grid) and legs_done < len(leg_amounts):
|
||||
level = float(grid[legs_done])
|
||||
fired = False
|
||||
if direction == "long":
|
||||
if last_pf > level and pf <= level:
|
||||
fired = True
|
||||
else:
|
||||
if last_pf < level and pf >= level:
|
||||
fired = True
|
||||
if fired:
|
||||
amt = float(exchange.amount_to_precision(ex_sym, leg_amounts[legs_done]))
|
||||
if amt > 0:
|
||||
add_resp = _trend_market_add_contracts(ex_sym, direction, amt, lev)
|
||||
fill_px = extract_trade_price_from_order(add_resp) or pf
|
||||
old_avg = float(row["avg_entry_price"] or fill_px)
|
||||
old_open = float(row["order_amount_open"] or 0)
|
||||
new_open = old_open + amt
|
||||
new_avg = _trend_weighted_avg(old_avg, old_open, fill_px, amt)
|
||||
conn.execute(
|
||||
"UPDATE trend_pullback_plans SET legs_done=?, avg_entry_price=?, order_amount_open=?, last_mark_price=? WHERE id=?",
|
||||
(legs_done + 1, new_avg, new_open, pf, row["id"]),
|
||||
)
|
||||
row = conn.execute("SELECT * FROM trend_pullback_plans WHERE id=?", (row["id"],)).fetchone()
|
||||
try:
|
||||
_trend_refresh_stop_only(ex_sym, direction, sl)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
conn.execute(
|
||||
"UPDATE trend_pullback_plans SET last_mark_price=? WHERE id=?",
|
||||
(pf, row["id"]),
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
conn.commit()
|
||||
conn.close()
|
||||
cfg = app.extensions.get("strategy_trend_cfg") or build_trend_config(
|
||||
sys.modules[__name__]
|
||||
)
|
||||
_check(cfg)
|
||||
|
||||
|
||||
# 关键位监控(前端已下线时仍保留函数体,后台默认不再调用)
|
||||
@@ -7192,7 +6964,12 @@ def stop_trend_pullback(pid):
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_trend_finalize_plan(conn, row, "手动平仓", exit_p)
|
||||
from strategy_trend_register import _finalize_plan, build_trend_config
|
||||
|
||||
cfg = app.extensions.get("strategy_trend_cfg") or build_trend_config(
|
||||
sys.modules[__name__]
|
||||
)
|
||||
_finalize_plan(cfg, conn, row, "手动平仓", exit_p)
|
||||
except Exception as e:
|
||||
conn.execute(
|
||||
"UPDATE trend_pullback_plans SET status='stopped_manual', message=? "
|
||||
|
||||
Reference in New Issue
Block a user