# Copyright (c) 2025-2026 马建军. All rights reserved. # 专有软件 — 未经授权禁止复制、传播、转售。 # 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。 # 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md """趋势回调:纯计算(期货整数手)。""" from __future__ import annotations import json import math from typing import Any, Optional, Tuple from contract_specs import get_contract_spec def validate_trend_bounds(direction: str, stop_loss: float, add_upper: float) -> Optional[str]: direction = (direction or "long").strip().lower() if direction == "long": if not (float(stop_loss) < float(add_upper)): return "做多:止损须低于补仓上沿" else: if not (float(stop_loss) > float(add_upper)): return "做空:止损须高于补仓下沿" return None def build_grid_prices(direction: str, sl: float, upper: float, n_legs: int) -> list[float]: sl, upper = float(sl), float(upper) out: list[float] = [] if n_legs <= 0: return out direction = (direction or "long").strip().lower() if direction == "long": if upper <= sl: return out span = upper - sl for i in range(1, n_legs + 1): out.append(sl + (i / float(n_legs + 1)) * span) out.sort(reverse=True) else: if sl <= upper: return out span = sl - upper for i in range(1, n_legs + 1): out.append(upper + (i / float(n_legs + 1)) * span) out.sort() return [round(p, 4) for p in out] def compute_trend_plan_futures( *, direction: str, stop_loss: float, add_upper: float, take_profit: float, risk_percent: float, capital: float, live_price: float, ths_code: str, dca_legs: int = 5, ) -> Tuple[Optional[dict[str, Any]], Optional[str]]: err = validate_trend_bounds(direction, stop_loss, add_upper) if err: return None, err spec = get_contract_spec(ths_code) mult = spec["mult"] d = (direction or "long").strip().lower() if d == "short": worst_per_lot = (float(stop_loss) - float(add_upper)) * mult else: worst_per_lot = (float(add_upper) - float(stop_loss)) * mult if worst_per_lot <= 0: return None, "止损与补仓边界无法计算风险" budget = float(capital) * float(risk_percent) / 100.0 total_lots = int(math.floor(budget / worst_per_lot)) if total_lots < 3: return None, f"按 {risk_percent}% 风险,总手数至少需 3 手才能拆分首仓+补仓(当前 {total_lots} 手)" first_lots = total_lots // 2 remainder = total_lots - first_lots legs = max(1, min(int(dca_legs), remainder)) per_leg = remainder // legs leg_amounts = [per_leg] * (legs - 1) + [remainder - per_leg * (legs - 1)] if any(x < 1 for x in leg_amounts): legs = 1 leg_amounts = [remainder] grid = build_grid_prices(d, stop_loss, add_upper, len(leg_amounts)) margin_rate = spec["margin_rate"] plan_margin = float(live_price) * mult * total_lots * margin_rate return { "direction": d, "stop_loss": float(stop_loss), "add_upper": float(add_upper), "take_profit": float(take_profit), "risk_percent": float(risk_percent), "capital_snapshot": float(capital), "live_price_ref": float(live_price), "target_lots": total_lots, "first_lots": first_lots, "remainder_lots": remainder, "dca_legs": len(leg_amounts), "leg_amounts": leg_amounts, "leg_amounts_json": json.dumps(leg_amounts), "grid_prices_json": json.dumps(grid), "grid": grid, "plan_margin": round(plan_margin, 2), "mult": mult, }, None def trend_dca_level_reached(direction: str, mark_price: float, level: float) -> bool: d = (direction or "long").strip().lower() pf, lv = float(mark_price), float(level) return pf <= lv if d == "long" else pf >= lv def trend_strategy_periods() -> list[dict[str, str]]: """策略页可选 K 线周期。""" from kline_chart import MARKET_PERIODS skip = frozenset({"timeshare", "w"}) return [p for p in MARKET_PERIODS if p["key"] not in skip] def trend_period_label(key: str) -> str: k = (key or "").strip() for p in trend_strategy_periods(): if p["key"] == k: return p["label"] return k or "15分" def normalize_trend_period(key: str) -> str: valid = {p["key"] for p in trend_strategy_periods()} k = (key or "15m").strip() return k if k in valid else "15m" def _avg_after_entries(entries: list[tuple[float, int]]) -> float: total = sum(q for _, q in entries) if total <= 0: return 0.0 return sum(p * q for p, q in entries) / total def enrich_trend_plan_preview( plan: dict, *, symbol: str, symbol_name: str = "", period: str = "15m", ) -> dict[str, Any]: """补全预览:周期、风险金额、分档表格(对齐币圈预览样式)。""" out = dict(plan) d = (out.get("direction") or "long").strip().lower() sl = float(out["stop_loss"]) tp = float(out["take_profit"]) mult = float(out.get("mult") or 1) entry0 = float(out["live_price_ref"]) first_lots = int(out["first_lots"]) leg_amounts = [int(x) for x in (out.get("leg_amounts") or [])] grid = [float(x) for x in (out.get("grid") or [])] capital = float(out.get("capital_snapshot") or 0) risk_pct = float(out.get("risk_percent") or 0) budget = capital * risk_pct / 100.0 remainder = int(out.get("remainder_lots") or sum(leg_amounts)) out["symbol"] = symbol out["symbol_name"] = symbol_name or symbol out["period"] = normalize_trend_period(period) out["period_label"] = trend_period_label(out["period"]) out["stop_loss_budget"] = round(budget, 2) out["direction_label"] = "做多" if d == "long" else "做空" entries: list[tuple[float, int]] = [(entry0, first_lots)] rows: list[dict[str, Any]] = [] def leg_metrics() -> tuple[float, float, float, Optional[float]]: total = sum(q for _, q in entries) avg = _avg_after_entries(entries) if d == "long": profit = (tp - avg) * total * mult loss = (avg - sl) * total * mult else: profit = (avg - tp) * total * mult loss = (sl - avg) * total * mult rr = profit / loss if loss > 0 else None return ( round(avg, 4), round(profit, 2), round(loss, 2), round(rr, 2) if rr is not None else None, ) avg, profit, loss, rr = leg_metrics() rows.append({ "level": "首仓", "price": round(entry0, 4), "lots": first_lots, "avg_after": avg, "profit_at_tp": profit, "loss_at_sl": loss, "rr_ratio": rr, }) out["first_rr_ratio"] = rr for i, lots in enumerate(leg_amounts): price = grid[i] if i < len(grid) else sl entries.append((float(price), int(lots))) avg, profit, loss, rr = leg_metrics() rows.append({ "level": f"补仓{i + 1}", "price": round(float(price), 4), "lots": int(lots), "avg_after": avg, "profit_at_tp": profit, "loss_at_sl": loss, "rr_ratio": rr, }) out["preview_rows"] = rows out["summary_line"] = ( f"{out['symbol_name']} {out['symbol']} {out['direction_label']} {out['period_label']}" f" | 权益 {capital:.2f} 元" f" | 参考价 {entry0}" f" | 计划保证金 ≈ {out.get('plan_margin')} 元" f" | 总手 {out.get('target_lots')}(首仓 {first_lots} + 补仓 {remainder})" ) out["detail_line"] = ( f"止损价 {sl} | 止损金额 {out['stop_loss_budget']} 元(权益 × 风险 {risk_pct}%)" f" | 补仓边界 {float(out['add_upper'])} | 止盈价 {tp}" f" | 首仓盈亏比 {out['first_rr_ratio'] if out['first_rr_ratio'] is not None else '—'}" ) return out