feat: trend pullback preview TP per DCA leg with unified stop loss across exchanges
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -191,3 +191,201 @@ def compute_trend_plan_core(
|
||||
"leg_amounts": leg_list,
|
||||
}
|
||||
return payload, None
|
||||
|
||||
|
||||
def calc_planned_reward_risk_ratio(
|
||||
direction: str, entry_price: float, stop_loss: float, take_profit: float
|
||||
) -> Optional[float]:
|
||||
"""盈亏比(reward/risk),与四所 calc_rr_ratio 口径一致。"""
|
||||
try:
|
||||
entry = float(entry_price)
|
||||
sl = float(stop_loss)
|
||||
tp = float(take_profit)
|
||||
if entry <= 0 or sl <= 0 or tp <= 0:
|
||||
return None
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
risk = sl - entry
|
||||
reward = entry - tp
|
||||
else:
|
||||
risk = entry - sl
|
||||
reward = tp - entry
|
||||
if risk <= 0 or reward <= 0:
|
||||
return None
|
||||
return round(reward / risk, 4)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def calc_take_profit_for_rr(
|
||||
direction: str, entry_price: float, stop_loss: float, reward_risk_ratio: float
|
||||
) -> Optional[float]:
|
||||
"""按统一止损与目标 RR 反推止盈价。"""
|
||||
try:
|
||||
entry = float(entry_price)
|
||||
sl = float(stop_loss)
|
||||
rr = float(reward_risk_ratio)
|
||||
if entry <= 0 or sl <= 0 or rr <= 0:
|
||||
return None
|
||||
direction = (direction or "long").strip().lower()
|
||||
if direction == "short":
|
||||
risk = sl - entry
|
||||
if risk <= 0:
|
||||
return None
|
||||
return round(entry - rr * risk, 10)
|
||||
risk = entry - sl
|
||||
if risk <= 0:
|
||||
return None
|
||||
return round(entry + rr * risk, 10)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def weighted_avg_entry(legs: list[tuple[float, float]]) -> Optional[float]:
|
||||
"""按 (成交价, 张数) 加权均价。"""
|
||||
total = 0.0
|
||||
cost = 0.0
|
||||
for price, amount in legs or []:
|
||||
try:
|
||||
p = float(price)
|
||||
a = float(amount)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if a <= 0:
|
||||
continue
|
||||
total += a
|
||||
cost += p * a
|
||||
if total <= 0:
|
||||
return None
|
||||
return cost / total
|
||||
|
||||
|
||||
def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
||||
"""
|
||||
预览:参考价首仓止盈 + 每档补仓后止盈;止损统一为计划止损(加仓后最大止损)。
|
||||
返回 (增强后的 preview 字段, 表格行列表,含首仓行)。
|
||||
"""
|
||||
p = dict(preview or {})
|
||||
direction = (p.get("direction") or "long").strip().lower()
|
||||
try:
|
||||
ref = float(p.get("live_price_ref"))
|
||||
sl = float(p.get("stop_loss"))
|
||||
user_tp = float(p.get("take_profit"))
|
||||
first_amt = float(p.get("first_order_amount"))
|
||||
except (TypeError, ValueError):
|
||||
return p, []
|
||||
|
||||
rr = calc_planned_reward_risk_ratio(direction, ref, sl, user_tp)
|
||||
if rr is None:
|
||||
return p, []
|
||||
|
||||
first_tp = calc_take_profit_for_rr(direction, ref, sl, rr)
|
||||
p["preview_target_rr"] = rr
|
||||
p["preview_first_take_profit"] = first_tp
|
||||
p["preview_unified_stop_loss"] = sl
|
||||
|
||||
try:
|
||||
grid = json.loads(p.get("grid_prices_json") or "[]")
|
||||
if not isinstance(grid, list):
|
||||
grid = []
|
||||
except Exception:
|
||||
grid = []
|
||||
try:
|
||||
leg_amounts = json.loads(p.get("leg_amounts_json") or "[]")
|
||||
if not isinstance(leg_amounts, list):
|
||||
leg_amounts = []
|
||||
except Exception:
|
||||
leg_amounts = []
|
||||
|
||||
rows: list[dict] = [
|
||||
{
|
||||
"i": 0,
|
||||
"label": "首仓",
|
||||
"price": ref,
|
||||
"contracts": first_amt,
|
||||
"avg_entry": ref,
|
||||
"take_profit": first_tp,
|
||||
"stop_loss": sl,
|
||||
"is_first": True,
|
||||
}
|
||||
]
|
||||
accumulated: list[tuple[float, float]] = [(ref, first_amt)]
|
||||
for i, pair in enumerate(zip(grid, leg_amounts), 1):
|
||||
try:
|
||||
price = float(pair[0])
|
||||
contracts = float(pair[1])
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
accumulated.append((price, contracts))
|
||||
avg = weighted_avg_entry(accumulated)
|
||||
tp_after = calc_take_profit_for_rr(direction, avg, sl, rr) if avg is not None else None
|
||||
rows.append(
|
||||
{
|
||||
"i": i,
|
||||
"label": f"补仓{i}",
|
||||
"price": price,
|
||||
"contracts": contracts,
|
||||
"avg_entry": avg,
|
||||
"take_profit": tp_after,
|
||||
"stop_loss": sl,
|
||||
"is_first": False,
|
||||
}
|
||||
)
|
||||
return p, rows
|
||||
|
||||
|
||||
def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]:
|
||||
"""运行中计划:为 dca_levels 补充加仓后均价、止盈、统一止损。"""
|
||||
if not levels:
|
||||
return levels
|
||||
p = plan or {}
|
||||
direction = (p.get("direction") or "long").strip().lower()
|
||||
try:
|
||||
sl = float(p.get("stop_loss"))
|
||||
user_tp = float(p.get("take_profit"))
|
||||
first_amt = float(p.get("first_order_amount"))
|
||||
except (TypeError, ValueError):
|
||||
return levels
|
||||
|
||||
ref_raw = p.get("live_price_ref")
|
||||
if ref_raw in (None, ""):
|
||||
ref_raw = p.get("avg_entry_price")
|
||||
try:
|
||||
ref = float(ref_raw)
|
||||
except (TypeError, ValueError):
|
||||
return levels
|
||||
|
||||
rr = calc_planned_reward_risk_ratio(direction, ref, sl, user_tp)
|
||||
if rr is None:
|
||||
return levels
|
||||
|
||||
out: list[dict] = []
|
||||
accumulated: list[tuple[float, float]] = []
|
||||
for lv in levels:
|
||||
row = dict(lv)
|
||||
is_first = row.get("leg_key") == "first" or row.get("label") == "首仓" or row.get("i") == 0
|
||||
if is_first:
|
||||
amt = row.get("contracts")
|
||||
try:
|
||||
amt_f = float(amt if amt is not None else first_amt)
|
||||
except (TypeError, ValueError):
|
||||
amt_f = first_amt
|
||||
accumulated = [(ref, amt_f)]
|
||||
row["avg_entry"] = ref
|
||||
row["take_profit"] = calc_take_profit_for_rr(direction, ref, sl, rr)
|
||||
row["stop_loss"] = sl
|
||||
else:
|
||||
price = row.get("price")
|
||||
contracts = row.get("contracts")
|
||||
if price is not None and contracts is not None:
|
||||
try:
|
||||
accumulated.append((float(price), float(contracts)))
|
||||
avg = weighted_avg_entry(accumulated)
|
||||
if avg is not None:
|
||||
row["avg_entry"] = avg
|
||||
row["take_profit"] = calc_take_profit_for_rr(direction, avg, sl, rr)
|
||||
row["stop_loss"] = sl
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user