fix(trend): use money RR, track DCA fills, snapshot before close
Align running-plan header and DCA table with risk-budget RR, record actual fill prices after each leg, and save pre-close snapshots on stop/TP/handoff across hub and exchanges. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+120
-20
@@ -336,6 +336,59 @@ def weighted_avg_entry(legs: list[tuple[float, float]]) -> Optional[float]:
|
||||
return cost / total
|
||||
|
||||
|
||||
def parse_leg_fill_prices(plan: dict) -> list[float]:
|
||||
"""首仓 + 各档补仓实际成交价列表。"""
|
||||
try:
|
||||
raw = json.loads((plan or {}).get("leg_fill_prices_json") or "[]")
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
out: list[float] = []
|
||||
for item in raw:
|
||||
try:
|
||||
out.append(float(item))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return out
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def append_leg_fill_price_json(existing_json: str | None, fill_px: float) -> str:
|
||||
fills = parse_leg_fill_prices({"leg_fill_prices_json": existing_json})
|
||||
fills.append(float(fill_px))
|
||||
return json.dumps(fills, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def calc_trend_plan_money_metrics(plan: dict) -> dict:
|
||||
"""运行中计划头部:按快照风险金额计算盈亏比(止盈盈利 U / 风险 U)。"""
|
||||
out = {"money_rr": None, "risk_amount_u": None}
|
||||
p = plan or {}
|
||||
try:
|
||||
direction = (p.get("direction") or "long").strip().lower()
|
||||
user_tp = float(p.get("take_profit"))
|
||||
avg = float(p.get("avg_entry_price"))
|
||||
open_amt = float(p.get("order_amount_open") or p.get("first_order_amount") or 0)
|
||||
snapshot = float(p.get("snapshot_available_usdt"))
|
||||
risk_percent = float(p.get("risk_percent"))
|
||||
except (TypeError, ValueError):
|
||||
return out
|
||||
if avg <= 0 or open_amt <= 0:
|
||||
return out
|
||||
risk_u = calc_risk_budget_usdt(snapshot, risk_percent)
|
||||
if risk_u is None or risk_u <= 0:
|
||||
return out
|
||||
out["risk_amount_u"] = risk_u
|
||||
try:
|
||||
contract_size = float(p.get("contract_size") or 1.0)
|
||||
if contract_size <= 0:
|
||||
contract_size = 1.0
|
||||
except (TypeError, ValueError):
|
||||
contract_size = 1.0
|
||||
profit_u = calc_tp_profit_usdt(direction, avg, user_tp, open_amt, contract_size)
|
||||
out["money_rr"] = calc_money_reward_risk_ratio(profit_u, risk_u)
|
||||
return out
|
||||
|
||||
|
||||
def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
||||
"""
|
||||
预览:表单止盈价下每档累计持仓的盈利 U;止损金额 = 快照×风险;盈亏比按金额对比。
|
||||
@@ -455,7 +508,7 @@ def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
||||
|
||||
|
||||
def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]:
|
||||
"""运行中计划:为 dca_levels 补充加仓后均价、止盈盈利 U、止损金额 U、金额盈亏比。"""
|
||||
"""运行中计划:补仓表按实际成交价重算触发价/均价/金额盈亏比;未补档仍用计划触发价预估。"""
|
||||
if not levels:
|
||||
return levels
|
||||
p = plan or {}
|
||||
@@ -473,6 +526,13 @@ def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict
|
||||
if risk_u is None or risk_u <= 0:
|
||||
return levels
|
||||
|
||||
fills = parse_leg_fill_prices(p)
|
||||
try:
|
||||
legs_done = int(p.get("legs_done") or 0)
|
||||
except (TypeError, ValueError):
|
||||
legs_done = 0
|
||||
first_done = int(p.get("first_order_done") or 0) != 0
|
||||
|
||||
ref_raw = p.get("live_price_ref")
|
||||
if ref_raw in (None, ""):
|
||||
ref_raw = p.get("avg_entry_price")
|
||||
@@ -494,32 +554,72 @@ def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict
|
||||
for lv in levels:
|
||||
row = dict(lv)
|
||||
is_first = row.get("leg_key") == "first" or row.get("label") == "首仓" or row.get("i") == 0
|
||||
row_cum = cum_contracts
|
||||
if is_first:
|
||||
amt = row.get("contracts")
|
||||
try:
|
||||
amt_f = float(amt if amt is not None else first_amt)
|
||||
amt_f = float(row.get("contracts") if row.get("contracts") is not None else first_amt)
|
||||
except (TypeError, ValueError):
|
||||
amt_f = first_amt
|
||||
accumulated = [(ref, amt_f)]
|
||||
cum_contracts = amt_f
|
||||
row["avg_entry"] = ref
|
||||
if first_done:
|
||||
fill_px = fills[0] if fills else None
|
||||
if fill_px is None:
|
||||
try:
|
||||
fill_px = float(p.get("avg_entry_price") or ref)
|
||||
except (TypeError, ValueError):
|
||||
fill_px = ref
|
||||
accumulated = [(float(fill_px), amt_f)]
|
||||
cum_contracts = amt_f
|
||||
row_cum = cum_contracts
|
||||
row["avg_entry"] = float(fill_px)
|
||||
else:
|
||||
accumulated = [(ref, amt_f)]
|
||||
cum_contracts = amt_f
|
||||
row_cum = cum_contracts
|
||||
row["avg_entry"] = ref
|
||||
else:
|
||||
price = row.get("price")
|
||||
contracts = row.get("contracts")
|
||||
if price is not None and contracts is not None:
|
||||
try:
|
||||
leg_contracts = float(contracts)
|
||||
accumulated.append((float(price), leg_contracts))
|
||||
avg = weighted_avg_entry(accumulated)
|
||||
if avg is not None:
|
||||
row["avg_entry"] = avg
|
||||
cum_contracts += leg_contracts
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
leg_num = int(row.get("i") or 0)
|
||||
except (TypeError, ValueError):
|
||||
leg_num = 0
|
||||
grid_trigger = row.get("price")
|
||||
try:
|
||||
grid_trigger_f = float(grid_trigger) if grid_trigger is not None else None
|
||||
except (TypeError, ValueError):
|
||||
grid_trigger_f = None
|
||||
try:
|
||||
leg_contracts = float(row.get("contracts") or 0)
|
||||
except (TypeError, ValueError):
|
||||
leg_contracts = 0.0
|
||||
done = row.get("status") == "done" or (leg_num > 0 and leg_num <= legs_done)
|
||||
if done and leg_contracts > 0:
|
||||
fill_idx = leg_num
|
||||
if len(fills) > fill_idx:
|
||||
fill_px = float(fills[fill_idx])
|
||||
elif grid_trigger_f is not None:
|
||||
fill_px = grid_trigger_f
|
||||
else:
|
||||
fill_px = ref
|
||||
row["price"] = fill_px
|
||||
accumulated.append((fill_px, leg_contracts))
|
||||
cum_contracts += leg_contracts
|
||||
row_cum = cum_contracts
|
||||
avg = weighted_avg_entry(accumulated)
|
||||
if avg is not None:
|
||||
row["avg_entry"] = avg
|
||||
elif grid_trigger_f is not None and leg_contracts > 0:
|
||||
row["price"] = grid_trigger_f
|
||||
projected = accumulated + [(grid_trigger_f, leg_contracts)]
|
||||
avg = weighted_avg_entry(projected)
|
||||
if avg is not None:
|
||||
row["avg_entry"] = avg
|
||||
row_cum = cum_contracts + leg_contracts
|
||||
elif grid_trigger_f is not None:
|
||||
row["price"] = grid_trigger_f
|
||||
|
||||
avg_entry = row.get("avg_entry")
|
||||
if avg_entry is not None:
|
||||
if avg_entry is not None and row_cum > 0:
|
||||
profit_u = calc_tp_profit_usdt(
|
||||
direction, float(avg_entry), user_tp, cum_contracts, contract_size
|
||||
direction, float(avg_entry), user_tp, row_cum, contract_size
|
||||
)
|
||||
row["take_profit_price"] = user_tp
|
||||
row["profit_u"] = profit_u
|
||||
|
||||
Reference in New Issue
Block a user