fix: trend preview uses USDT profit, snapshot risk budget, and money RR across four exchanges
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2839,6 +2839,7 @@ def parse_and_compute_trend_pullback_plan(form_dict):
|
|||||||
"leg_amounts_json": leg_json,
|
"leg_amounts_json": leg_json,
|
||||||
"grid": grid,
|
"grid": grid,
|
||||||
"leg_amounts": leg_list,
|
"leg_amounts": leg_list,
|
||||||
|
"contract_size": float(market.get("contractSize") or 1),
|
||||||
}
|
}
|
||||||
return payload, None
|
return payload, None
|
||||||
|
|
||||||
@@ -5611,6 +5612,14 @@ def render_main_page(page="trade"):
|
|||||||
|
|
||||||
trend_preview = row_to_dict(pr)
|
trend_preview = row_to_dict(pr)
|
||||||
preview_expires_ms = int(pr["expires_at_ms"])
|
preview_expires_ms = int(pr["expires_at_ms"])
|
||||||
|
if not trend_preview.get("contract_size"):
|
||||||
|
try:
|
||||||
|
ensure_markets_loaded()
|
||||||
|
ex_sym = trend_preview.get("exchange_symbol") or trend_preview.get("symbol")
|
||||||
|
mk = exchange.market(ex_sym)
|
||||||
|
trend_preview["contract_size"] = float(mk.get("contractSize") or 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
trend_preview, trend_preview_levels = build_trend_preview_level_rows(trend_preview)
|
trend_preview, trend_preview_levels = build_trend_preview_level_rows(trend_preview)
|
||||||
elif pr:
|
elif pr:
|
||||||
trend_preview_expired = True
|
trend_preview_expired = True
|
||||||
|
|||||||
+16
-3
@@ -597,7 +597,14 @@ def _fetch_preview(pid):
|
|||||||
from strategy_trend_lib import build_trend_preview_level_rows
|
from strategy_trend_lib import build_trend_preview_level_rows
|
||||||
|
|
||||||
enriched, level_rows = build_trend_preview_level_rows(d)
|
enriched, level_rows = build_trend_preview_level_rows(d)
|
||||||
for key in ("preview_target_rr", "preview_first_take_profit", "preview_unified_stop_loss"):
|
for key in (
|
||||||
|
"preview_target_rr",
|
||||||
|
"preview_first_take_profit",
|
||||||
|
"preview_unified_stop_loss",
|
||||||
|
"preview_risk_amount_u",
|
||||||
|
"preview_first_profit_u",
|
||||||
|
"preview_take_profit_price",
|
||||||
|
):
|
||||||
if key in enriched:
|
if key in enriched:
|
||||||
d[key] = enriched[key]
|
d[key] = enriched[key]
|
||||||
d["preview_level_rows"] = level_rows
|
d["preview_level_rows"] = level_rows
|
||||||
@@ -607,9 +614,15 @@ def _fetch_preview(pid):
|
|||||||
"label": row.get("label"),
|
"label": row.get("label"),
|
||||||
"price": row.get("price"),
|
"price": row.get("price"),
|
||||||
"contracts": row.get("contracts"),
|
"contracts": row.get("contracts"),
|
||||||
|
"cum_contracts": row.get("cum_contracts"),
|
||||||
"avg_entry": row.get("avg_entry"),
|
"avg_entry": row.get("avg_entry"),
|
||||||
"take_profit": row.get("take_profit"),
|
"take_profit_price": row.get("take_profit_price"),
|
||||||
"stop_loss": row.get("stop_loss"),
|
"profit_u": row.get("profit_u"),
|
||||||
|
"risk_u": row.get("risk_u"),
|
||||||
|
"rr": row.get("rr"),
|
||||||
|
"stop_loss_price": row.get("stop_loss_price"),
|
||||||
|
"take_profit": row.get("profit_u"),
|
||||||
|
"stop_loss": row.get("risk_u"),
|
||||||
}
|
}
|
||||||
for row in level_rows
|
for row in level_rows
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -47,19 +47,20 @@
|
|||||||
{{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x |
|
{{ trend_preview.symbol }} {{ '做多' if trend_preview.direction == 'long' else '做空' }} {{ trend_preview.leverage }}x |
|
||||||
预览可用快照 <strong>{{ mf(trend_preview.snapshot_available_usdt) }}</strong> U | 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }} |
|
预览可用快照 <strong>{{ mf(trend_preview.snapshot_available_usdt) }}</strong> U | 参考价 {{ price_fmt(trend_preview.symbol, trend_preview.live_price_ref) }} |
|
||||||
计划保证金≈{{ mf(trend_preview.plan_margin_capital) }} U | 总张≈{{ amt_disp(trend_preview.symbol, trend_preview.target_order_amount) }}(首仓 {{ amt_disp(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_disp(trend_preview.symbol, trend_preview.remainder_total) }})<br>
|
计划保证金≈{{ mf(trend_preview.plan_margin_capital) }} U | 总张≈{{ amt_disp(trend_preview.symbol, trend_preview.target_order_amount) }}(首仓 {{ amt_disp(trend_preview.symbol, trend_preview.first_order_amount) }} + 补仓 {{ amt_disp(trend_preview.symbol, trend_preview.remainder_total) }})<br>
|
||||||
统一止损 {{ price_fmt(trend_preview.symbol, trend_preview.preview_unified_stop_loss or trend_preview.stop_loss) }} | {{ trend_add_zone_label(trend_preview.direction) }} {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} | 表单止盈 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} | 目标RR {% if trend_preview.preview_target_rr is not none %}{{ '%.2f'|format(trend_preview.preview_target_rr) }}{% else %}—{% endif %} | 风险比例 {{ trend_preview.risk_percent }}%
|
止损价 {{ price_fmt(trend_preview.symbol, trend_preview.preview_unified_stop_loss or trend_preview.stop_loss) }} | 止损金额 {% if trend_preview.preview_risk_amount_u is not none %}{{ mf(trend_preview.preview_risk_amount_u) }}U{% else %}—{% endif %}(快照×风险{{ trend_preview.risk_percent }}%)| {{ trend_add_zone_label(trend_preview.direction) }} {{ price_fmt(trend_preview.symbol, trend_preview.add_upper) }} | 止盈价 {{ price_fmt(trend_preview.symbol, trend_preview.take_profit) }} | 首仓盈亏比 {% if trend_preview.preview_target_rr is not none %}{{ '%.2f'|format(trend_preview.preview_target_rr) }}{% else %}—{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap" style="margin-bottom:10px">
|
<div class="table-wrap" style="margin-bottom:10px">
|
||||||
<table>
|
<table>
|
||||||
<tr><th>档位</th><th>触发/参考价</th><th>张数</th><th>加仓后均价</th><th>止盈</th><th>止损</th></tr>
|
<tr><th>档位</th><th>触发/参考价</th><th>张数</th><th>加仓后均价</th><th>止盈盈利(U)</th><th>止损(U)</th><th>盈亏比</th></tr>
|
||||||
{% for row in trend_preview_levels %}
|
{% for row in trend_preview_levels %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.label or row.i }}</td>
|
<td>{{ row.label or row.i }}</td>
|
||||||
<td>{{ price_fmt(trend_preview.symbol, row.price) }}</td>
|
<td>{{ price_fmt(trend_preview.symbol, row.price) }}</td>
|
||||||
<td>{{ amt_disp(trend_preview.symbol, row.contracts) }}</td>
|
<td>{{ amt_disp(trend_preview.symbol, row.contracts) }}</td>
|
||||||
<td>{% if row.avg_entry is not none %}{{ price_fmt(trend_preview.symbol, row.avg_entry) }}{% else %}—{% endif %}</td>
|
<td>{% if row.avg_entry is not none %}{{ price_fmt(trend_preview.symbol, row.avg_entry) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if row.take_profit is not none %}{{ price_fmt(trend_preview.symbol, row.take_profit) }}{% else %}—{% endif %}</td>
|
<td>{% if row.profit_u is not none %}{{ mf(row.profit_u) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if row.stop_loss is not none %}{{ price_fmt(trend_preview.symbol, row.stop_loss) }}{% else %}—{% endif %}</td>
|
<td>{% if row.risk_u is not none %}{{ mf(row.risk_u) }}{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if row.rr is not none %}{{ '%.2f'|format(row.rr) }}{% else %}—{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
@@ -166,15 +167,16 @@
|
|||||||
<div class="plan-dca-block">
|
<div class="plan-dca-block">
|
||||||
<div class="plan-dca-title">补仓计划明细</div>
|
<div class="plan-dca-title">补仓计划明细</div>
|
||||||
<table class="plan-dca-table">
|
<table class="plan-dca-table">
|
||||||
<tr><th>档位</th><th>触发价</th><th>张数</th><th>加仓后均价</th><th>止盈</th><th>止损</th><th>状态</th></tr>
|
<tr><th>档位</th><th>触发价</th><th>张数</th><th>加仓后均价</th><th>止盈盈利(U)</th><th>止损(U)</th><th>盈亏比</th><th>状态</th></tr>
|
||||||
{% for lv in t.dca_levels %}
|
{% for lv in t.dca_levels %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ lv.label }}</td>
|
<td>{{ lv.label }}</td>
|
||||||
<td>{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}</td>
|
<td>{% if lv.price is not none %}{{ price_fmt(sym, lv.price) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if lv.contracts is not none %}{{ amt_disp(sym, lv.contracts) }}{% else %}—{% endif %}</td>
|
<td>{% if lv.contracts is not none %}{{ amt_disp(sym, lv.contracts) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if lv.avg_entry is not none %}{{ price_fmt(sym, lv.avg_entry) }}{% else %}—{% endif %}</td>
|
<td>{% if lv.avg_entry is not none %}{{ price_fmt(sym, lv.avg_entry) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if lv.take_profit is not none %}{{ price_fmt(sym, lv.take_profit) }}{% else %}—{% endif %}</td>
|
<td>{% if lv.profit_u is not none %}{{ mf(lv.profit_u) }}{% else %}—{% endif %}</td>
|
||||||
<td>{% if lv.stop_loss is not none %}{{ price_fmt(sym, lv.stop_loss) }}{% else %}—{% endif %}</td>
|
<td>{% if lv.risk_u is not none %}{{ mf(lv.risk_u) }}{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if lv.rr is not none %}{{ '%.2f'|format(lv.rr) }}{% else %}—{% endif %}</td>
|
||||||
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label }}</td>
|
<td class="{% if lv.status == 'done' %}st-done{% else %}st-pending{% endif %}">{{ lv.status_label }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
+145
-38
@@ -241,6 +241,48 @@ def calc_take_profit_for_rr(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def calc_risk_budget_usdt(snapshot_usdt: float, risk_percent: float) -> Optional[float]:
|
||||||
|
"""计划止损金额 U = 可用快照 × 风险比例。"""
|
||||||
|
try:
|
||||||
|
snap = float(snapshot_usdt)
|
||||||
|
rp = float(risk_percent)
|
||||||
|
if snap <= 0 or rp <= 0:
|
||||||
|
return None
|
||||||
|
return round(snap * rp / 100.0, 4)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def calc_money_reward_risk_ratio(profit_u: float, risk_u: float) -> Optional[float]:
|
||||||
|
"""金额盈亏比 = 止盈盈利 U / 止损金额 U。"""
|
||||||
|
try:
|
||||||
|
r = float(risk_u)
|
||||||
|
p = float(profit_u)
|
||||||
|
if r <= 0:
|
||||||
|
return None
|
||||||
|
return round(p / r, 4)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def calc_tp_profit_usdt(
|
||||||
|
direction: str,
|
||||||
|
avg_entry: float,
|
||||||
|
take_profit_price: float,
|
||||||
|
contracts: float,
|
||||||
|
contract_size: float = 1.0,
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""到达止盈价时,按累计张数与加仓后均价的盈利 U。"""
|
||||||
|
try:
|
||||||
|
from hub_position_metrics import estimate_linear_swap_upnl_usdt
|
||||||
|
|
||||||
|
return estimate_linear_swap_upnl_usdt(
|
||||||
|
direction, float(avg_entry), float(take_profit_price), float(contracts), float(contract_size)
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def weighted_avg_entry(legs: list[tuple[float, float]]) -> Optional[float]:
|
def weighted_avg_entry(legs: list[tuple[float, float]]) -> Optional[float]:
|
||||||
"""按 (成交价, 张数) 加权均价。"""
|
"""按 (成交价, 张数) 加权均价。"""
|
||||||
total = 0.0
|
total = 0.0
|
||||||
@@ -262,7 +304,7 @@ def weighted_avg_entry(legs: list[tuple[float, float]]) -> Optional[float]:
|
|||||||
|
|
||||||
def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
||||||
"""
|
"""
|
||||||
预览:参考价首仓止盈 + 每档补仓后止盈;止损统一为计划止损(加仓后最大止损)。
|
预览:表单止盈价下每档累计持仓的盈利 U;止损金额 = 快照×风险;盈亏比按金额对比。
|
||||||
返回 (增强后的 preview 字段, 表格行列表,含首仓行)。
|
返回 (增强后的 preview 字段, 表格行列表,含首仓行)。
|
||||||
"""
|
"""
|
||||||
p = dict(preview or {})
|
p = dict(preview or {})
|
||||||
@@ -272,16 +314,24 @@ def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
|||||||
sl = float(p.get("stop_loss"))
|
sl = float(p.get("stop_loss"))
|
||||||
user_tp = float(p.get("take_profit"))
|
user_tp = float(p.get("take_profit"))
|
||||||
first_amt = float(p.get("first_order_amount"))
|
first_amt = float(p.get("first_order_amount"))
|
||||||
|
snapshot = float(p.get("snapshot_available_usdt"))
|
||||||
|
risk_percent = float(p.get("risk_percent"))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return p, []
|
return p, []
|
||||||
|
|
||||||
rr = calc_planned_reward_risk_ratio(direction, ref, sl, user_tp)
|
risk_u = calc_risk_budget_usdt(snapshot, risk_percent)
|
||||||
if rr is None:
|
if risk_u is None or risk_u <= 0:
|
||||||
return p, []
|
return p, []
|
||||||
|
|
||||||
first_tp = calc_take_profit_for_rr(direction, ref, sl, rr)
|
try:
|
||||||
p["preview_target_rr"] = rr
|
contract_size = float(p.get("contract_size") or 1.0)
|
||||||
p["preview_first_take_profit"] = first_tp
|
if contract_size <= 0:
|
||||||
|
contract_size = 1.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
contract_size = 1.0
|
||||||
|
|
||||||
|
p["preview_risk_amount_u"] = risk_u
|
||||||
|
p["preview_take_profit_price"] = user_tp
|
||||||
p["preview_unified_stop_loss"] = sl
|
p["preview_unified_stop_loss"] = sl
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -297,45 +347,81 @@ def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
leg_amounts = []
|
leg_amounts = []
|
||||||
|
|
||||||
rows: list[dict] = [
|
def _row_dict(
|
||||||
{
|
*,
|
||||||
"i": 0,
|
i: int,
|
||||||
"label": "首仓",
|
label: str,
|
||||||
"price": ref,
|
price: float,
|
||||||
"contracts": first_amt,
|
leg_contracts: float,
|
||||||
"avg_entry": ref,
|
cum_contracts: float,
|
||||||
"take_profit": first_tp,
|
avg: float,
|
||||||
"stop_loss": sl,
|
is_first: bool,
|
||||||
"is_first": True,
|
) -> dict:
|
||||||
|
profit_u = calc_tp_profit_usdt(direction, avg, user_tp, cum_contracts, contract_size)
|
||||||
|
rr_money = calc_money_reward_risk_ratio(profit_u, risk_u) if profit_u is not None else None
|
||||||
|
return {
|
||||||
|
"i": i,
|
||||||
|
"label": label,
|
||||||
|
"price": price,
|
||||||
|
"contracts": leg_contracts,
|
||||||
|
"cum_contracts": cum_contracts,
|
||||||
|
"avg_entry": avg,
|
||||||
|
"take_profit_price": user_tp,
|
||||||
|
"profit_u": profit_u,
|
||||||
|
"risk_u": risk_u,
|
||||||
|
"rr": rr_money,
|
||||||
|
"stop_loss_price": sl,
|
||||||
|
"take_profit": profit_u,
|
||||||
|
"stop_loss": risk_u,
|
||||||
|
"is_first": is_first,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cum_contracts = first_amt
|
||||||
|
first_profit = calc_tp_profit_usdt(direction, ref, user_tp, cum_contracts, contract_size)
|
||||||
|
first_rr = calc_money_reward_risk_ratio(first_profit, risk_u) if first_profit is not None else None
|
||||||
|
p["preview_first_profit_u"] = first_profit
|
||||||
|
p["preview_target_rr"] = first_rr
|
||||||
|
p["preview_first_take_profit"] = user_tp
|
||||||
|
|
||||||
|
rows: list[dict] = [
|
||||||
|
_row_dict(
|
||||||
|
i=0,
|
||||||
|
label="首仓",
|
||||||
|
price=ref,
|
||||||
|
leg_contracts=first_amt,
|
||||||
|
cum_contracts=cum_contracts,
|
||||||
|
avg=ref,
|
||||||
|
is_first=True,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
accumulated: list[tuple[float, float]] = [(ref, first_amt)]
|
accumulated: list[tuple[float, float]] = [(ref, first_amt)]
|
||||||
for i, pair in enumerate(zip(grid, leg_amounts), 1):
|
for i, pair in enumerate(zip(grid, leg_amounts), 1):
|
||||||
try:
|
try:
|
||||||
price = float(pair[0])
|
price = float(pair[0])
|
||||||
contracts = float(pair[1])
|
leg_contracts = float(pair[1])
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
continue
|
continue
|
||||||
accumulated.append((price, contracts))
|
accumulated.append((price, leg_contracts))
|
||||||
avg = weighted_avg_entry(accumulated)
|
avg = weighted_avg_entry(accumulated)
|
||||||
tp_after = calc_take_profit_for_rr(direction, avg, sl, rr) if avg is not None else None
|
if avg is None:
|
||||||
|
continue
|
||||||
|
cum_contracts += leg_contracts
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
_row_dict(
|
||||||
"i": i,
|
i=i,
|
||||||
"label": f"补仓{i}",
|
label=f"补仓{i}",
|
||||||
"price": price,
|
price=price,
|
||||||
"contracts": contracts,
|
leg_contracts=leg_contracts,
|
||||||
"avg_entry": avg,
|
cum_contracts=cum_contracts,
|
||||||
"take_profit": tp_after,
|
avg=avg,
|
||||||
"stop_loss": sl,
|
is_first=False,
|
||||||
"is_first": False,
|
)
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return p, rows
|
return p, rows
|
||||||
|
|
||||||
|
|
||||||
def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]:
|
def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]:
|
||||||
"""运行中计划:为 dca_levels 补充加仓后均价、止盈、统一止损。"""
|
"""运行中计划:为 dca_levels 补充加仓后均价、止盈盈利 U、止损金额 U、金额盈亏比。"""
|
||||||
if not levels:
|
if not levels:
|
||||||
return levels
|
return levels
|
||||||
p = plan or {}
|
p = plan or {}
|
||||||
@@ -344,9 +430,15 @@ def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict
|
|||||||
sl = float(p.get("stop_loss"))
|
sl = float(p.get("stop_loss"))
|
||||||
user_tp = float(p.get("take_profit"))
|
user_tp = float(p.get("take_profit"))
|
||||||
first_amt = float(p.get("first_order_amount"))
|
first_amt = float(p.get("first_order_amount"))
|
||||||
|
snapshot = float(p.get("snapshot_available_usdt"))
|
||||||
|
risk_percent = float(p.get("risk_percent"))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return levels
|
return levels
|
||||||
|
|
||||||
|
risk_u = calc_risk_budget_usdt(snapshot, risk_percent)
|
||||||
|
if risk_u is None or risk_u <= 0:
|
||||||
|
return levels
|
||||||
|
|
||||||
ref_raw = p.get("live_price_ref")
|
ref_raw = p.get("live_price_ref")
|
||||||
if ref_raw in (None, ""):
|
if ref_raw in (None, ""):
|
||||||
ref_raw = p.get("avg_entry_price")
|
ref_raw = p.get("avg_entry_price")
|
||||||
@@ -355,12 +447,16 @@ def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return levels
|
return levels
|
||||||
|
|
||||||
rr = calc_planned_reward_risk_ratio(direction, ref, sl, user_tp)
|
try:
|
||||||
if rr is None:
|
contract_size = float(p.get("contract_size") or 1.0)
|
||||||
return levels
|
if contract_size <= 0:
|
||||||
|
contract_size = 1.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
contract_size = 1.0
|
||||||
|
|
||||||
out: list[dict] = []
|
out: list[dict] = []
|
||||||
accumulated: list[tuple[float, float]] = []
|
accumulated: list[tuple[float, float]] = []
|
||||||
|
cum_contracts = 0.0
|
||||||
for lv in levels:
|
for lv in levels:
|
||||||
row = dict(lv)
|
row = dict(lv)
|
||||||
is_first = row.get("leg_key") == "first" or row.get("label") == "首仓" or row.get("i") == 0
|
is_first = row.get("leg_key") == "first" or row.get("label") == "首仓" or row.get("i") == 0
|
||||||
@@ -371,21 +467,32 @@ def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
amt_f = first_amt
|
amt_f = first_amt
|
||||||
accumulated = [(ref, amt_f)]
|
accumulated = [(ref, amt_f)]
|
||||||
|
cum_contracts = amt_f
|
||||||
row["avg_entry"] = ref
|
row["avg_entry"] = ref
|
||||||
row["take_profit"] = calc_take_profit_for_rr(direction, ref, sl, rr)
|
|
||||||
row["stop_loss"] = sl
|
|
||||||
else:
|
else:
|
||||||
price = row.get("price")
|
price = row.get("price")
|
||||||
contracts = row.get("contracts")
|
contracts = row.get("contracts")
|
||||||
if price is not None and contracts is not None:
|
if price is not None and contracts is not None:
|
||||||
try:
|
try:
|
||||||
accumulated.append((float(price), float(contracts)))
|
leg_contracts = float(contracts)
|
||||||
|
accumulated.append((float(price), leg_contracts))
|
||||||
avg = weighted_avg_entry(accumulated)
|
avg = weighted_avg_entry(accumulated)
|
||||||
if avg is not None:
|
if avg is not None:
|
||||||
row["avg_entry"] = avg
|
row["avg_entry"] = avg
|
||||||
row["take_profit"] = calc_take_profit_for_rr(direction, avg, sl, rr)
|
cum_contracts += leg_contracts
|
||||||
row["stop_loss"] = sl
|
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
avg_entry = row.get("avg_entry")
|
||||||
|
if avg_entry is not None:
|
||||||
|
profit_u = calc_tp_profit_usdt(
|
||||||
|
direction, float(avg_entry), user_tp, cum_contracts, contract_size
|
||||||
|
)
|
||||||
|
row["take_profit_price"] = user_tp
|
||||||
|
row["profit_u"] = profit_u
|
||||||
|
row["risk_u"] = risk_u
|
||||||
|
row["rr"] = calc_money_reward_risk_ratio(profit_u, risk_u) if profit_u is not None else None
|
||||||
|
row["take_profit"] = profit_u
|
||||||
|
row["stop_loss"] = risk_u
|
||||||
|
row["stop_loss_price"] = sl
|
||||||
out.append(row)
|
out.append(row)
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ def parse_trend_plan(cfg: dict, form_dict) -> tuple[Optional[dict], Optional[str
|
|||||||
leg_list = json.loads(leg_json)
|
leg_list = json.loads(leg_json)
|
||||||
except Exception:
|
except Exception:
|
||||||
leg_list = []
|
leg_list = []
|
||||||
|
contract_size = float(market.get("contractSize") or 1)
|
||||||
return {
|
return {
|
||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"exchange_symbol": exchange_symbol,
|
"exchange_symbol": exchange_symbol,
|
||||||
@@ -271,6 +272,7 @@ def parse_trend_plan(cfg: dict, form_dict) -> tuple[Optional[dict], Optional[str
|
|||||||
"leg_amounts_json": leg_json,
|
"leg_amounts_json": leg_json,
|
||||||
"grid": grid,
|
"grid": grid,
|
||||||
"leg_amounts": leg_list,
|
"leg_amounts": leg_list,
|
||||||
|
"contract_size": contract_size,
|
||||||
}, None
|
}, None
|
||||||
|
|
||||||
|
|
||||||
@@ -486,6 +488,12 @@ def enrich_trend_plan(cfg: dict, row) -> dict:
|
|||||||
d["floating_mark"] = None
|
d["floating_mark"] = None
|
||||||
else:
|
else:
|
||||||
d["floating_pnl"] = d["floating_mark"] = None
|
d["floating_pnl"] = d["floating_mark"] = None
|
||||||
|
get_cs = getattr(m, "get_contract_size", None)
|
||||||
|
if callable(get_cs):
|
||||||
|
try:
|
||||||
|
d["contract_size"] = float(get_cs(ex_sym))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
from strategy_snapshot_lib import attach_trend_dca_levels
|
from strategy_snapshot_lib import attach_trend_dca_levels
|
||||||
|
|
||||||
d = attach_trend_dca_levels(d)
|
d = attach_trend_dca_levels(d)
|
||||||
@@ -1097,6 +1105,14 @@ def load_trend_page_context(conn, request_obj, cfg: dict) -> dict[str, Any]:
|
|||||||
|
|
||||||
trend_preview = _row(cfg, pr)
|
trend_preview = _row(cfg, pr)
|
||||||
preview_expires_ms = int(pr["expires_at_ms"])
|
preview_expires_ms = int(pr["expires_at_ms"])
|
||||||
|
get_cs = getattr(m, "get_contract_size", None)
|
||||||
|
if callable(get_cs) and not trend_preview.get("contract_size"):
|
||||||
|
try:
|
||||||
|
trend_preview["contract_size"] = float(
|
||||||
|
get_cs(trend_preview.get("exchange_symbol") or trend_preview.get("symbol") or "")
|
||||||
|
)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
trend_preview, trend_preview_levels = build_trend_preview_level_rows(trend_preview)
|
trend_preview, trend_preview_levels = build_trend_preview_level_rows(trend_preview)
|
||||||
elif pr:
|
elif pr:
|
||||||
trend_preview_expired = True
|
trend_preview_expired = True
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""趋势回调预览:参考价首仓止盈与补仓后止盈。"""
|
"""趋势回调预览:止盈盈利 U、止损金额 U、金额盈亏比。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -11,41 +11,47 @@ sys.path.insert(0, str(ROOT))
|
|||||||
|
|
||||||
from strategy_trend_lib import ( # noqa: E402
|
from strategy_trend_lib import ( # noqa: E402
|
||||||
build_trend_preview_level_rows,
|
build_trend_preview_level_rows,
|
||||||
calc_planned_reward_risk_ratio,
|
calc_money_reward_risk_ratio,
|
||||||
calc_take_profit_for_rr,
|
calc_risk_budget_usdt,
|
||||||
|
calc_tp_profit_usdt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestTrendPreviewTp(unittest.TestCase):
|
class TestTrendPreviewTp(unittest.TestCase):
|
||||||
def test_short_ref_price_first_tp_matches_form_tp(self):
|
def test_risk_budget_from_snapshot(self):
|
||||||
ref, sl, tp = 72.6, 75.5, 65.0
|
self.assertAlmostEqual(calc_risk_budget_usdt(110.73, 5), 5.5365, places=2)
|
||||||
rr = calc_planned_reward_risk_ratio("short", ref, sl, tp)
|
|
||||||
self.assertIsNotNone(rr)
|
|
||||||
first_tp = calc_take_profit_for_rr("short", ref, sl, rr)
|
|
||||||
self.assertAlmostEqual(first_tp, tp, places=2)
|
|
||||||
|
|
||||||
def test_preview_levels_include_first_and_dca_tp(self):
|
def test_short_profit_at_form_take_profit(self):
|
||||||
|
profit = calc_tp_profit_usdt("short", 72.53, 66.0, 1114, 0.00167)
|
||||||
|
self.assertIsNotNone(profit)
|
||||||
|
self.assertGreater(profit, 0)
|
||||||
|
rr = calc_money_reward_risk_ratio(profit, 5.5365)
|
||||||
|
self.assertIsNotNone(rr)
|
||||||
|
self.assertGreater(rr, 1.5)
|
||||||
|
|
||||||
|
def test_preview_levels_use_money_rr(self):
|
||||||
preview = {
|
preview = {
|
||||||
"direction": "short",
|
"direction": "short",
|
||||||
"live_price_ref": 72.6,
|
"live_price_ref": 72.53,
|
||||||
"stop_loss": 75.5,
|
"stop_loss": 75.5,
|
||||||
"take_profit": 65.0,
|
"take_profit": 66.0,
|
||||||
"first_order_amount": 1113,
|
"first_order_amount": 1114,
|
||||||
|
"snapshot_available_usdt": 110.73,
|
||||||
|
"risk_percent": 5,
|
||||||
|
"contract_size": 0.00167,
|
||||||
"grid_prices_json": json.dumps([73.42, 73.83]),
|
"grid_prices_json": json.dumps([73.42, 73.83]),
|
||||||
"leg_amounts_json": json.dumps([222, 222]),
|
"leg_amounts_json": json.dumps([222, 222]),
|
||||||
}
|
}
|
||||||
enriched, rows = build_trend_preview_level_rows(preview)
|
enriched, rows = build_trend_preview_level_rows(preview)
|
||||||
self.assertEqual(enriched["preview_unified_stop_loss"], 75.5)
|
self.assertAlmostEqual(enriched["preview_risk_amount_u"], 5.5365, places=2)
|
||||||
self.assertAlmostEqual(enriched["preview_first_take_profit"], 65.0, places=1)
|
self.assertEqual(enriched["preview_take_profit_price"], 66.0)
|
||||||
self.assertEqual(len(rows), 3)
|
self.assertEqual(len(rows), 3)
|
||||||
self.assertEqual(rows[0]["label"], "首仓")
|
self.assertEqual(rows[0]["label"], "首仓")
|
||||||
self.assertAlmostEqual(rows[0]["take_profit"], 65.0, places=1)
|
self.assertEqual(rows[0]["risk_u"], enriched["preview_risk_amount_u"])
|
||||||
self.assertEqual(rows[0]["stop_loss"], 75.5)
|
self.assertIsNotNone(rows[0]["profit_u"])
|
||||||
self.assertIsNotNone(rows[1]["avg_entry"])
|
self.assertAlmostEqual(rows[0]["rr"], rows[0]["profit_u"] / 5.5365, places=2)
|
||||||
self.assertIsNotNone(rows[1]["take_profit"])
|
self.assertEqual(rows[1]["risk_u"], enriched["preview_risk_amount_u"])
|
||||||
self.assertEqual(rows[1]["stop_loss"], 75.5)
|
self.assertGreater(rows[2]["profit_u"], rows[1]["profit_u"])
|
||||||
# 做空:补仓价上移 → 均价上移 → 同等 RR 下止盈价上移
|
|
||||||
self.assertGreater(rows[2]["take_profit"], rows[1]["take_profit"])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user