"""趋势回调策略:纯计算与校验(无 ccxt / Flask)。各所 adapter 负责张数精度与下单。""" from __future__ import annotations import json from typing import Any, Callable, Optional, Tuple AmountPreciseFn = Callable[[str, float], Optional[float]] def calc_risk_fraction(direction: str, entry_price: float, stop_loss: float) -> Optional[float]: try: entry = float(entry_price) sl = float(stop_loss) if entry <= 0 or sl <= 0: return None if (direction or "long").strip().lower() == "short": risk = sl - entry else: risk = entry - sl if risk <= 0: return None return risk / entry except (TypeError, ValueError): return None 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]: """在 (止损, 补仓区间远侧边界) 内生成 n_legs 个触发价(不含端点)。""" 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): t = i / float(n_legs + 1) out.append(sl + t * span) out.sort(reverse=True) else: if sl <= upper: return out span = sl - upper for i in range(1, n_legs + 1): t = i / float(n_legs + 1) out.append(upper + t * span) out.sort() return [round(p, 10) for p in out] def pick_dca_legs_and_per_leg( exchange_symbol: str, remainder_total: float, want_legs: int, amount_precise: AmountPreciseFn, min_amount: float = 0.0, ) -> Tuple[int, float]: """按最小张数约束自动减少档位数。返回 (有效档数, 每档参考张数)。""" legs = max(1, int(want_legs)) rem = float(remainder_total) min_amt = float(min_amount or 0.0) while legs >= 1: per = rem / legs per_p = amount_precise(exchange_symbol, per) if per_p is None or per_p <= 0: legs -= 1 continue if min_amt and per_p + 1e-12 < min_amt: legs -= 1 continue return legs, per_p one = amount_precise(exchange_symbol, rem) if one is None or one <= 0: return 0, 0.0 return 1, one def build_leg_amounts_json( exchange_symbol: str, remainder_total: float, want_legs: int, amount_precise: AmountPreciseFn, min_amount: float = 0.0, ) -> Tuple[int, str, float]: """拆分补仓张数 JSON。返回 (档位数, json列表, 每档参考)。""" rem = amount_precise(exchange_symbol, float(remainder_total)) if rem is None or rem <= 0: return 0, "[]", 0.0 n, _ = pick_dca_legs_and_per_leg(exchange_symbol, rem, want_legs, amount_precise, min_amount) if n <= 0: return 0, "[]", 0.0 if n <= 1: one = amount_precise(exchange_symbol, rem) if one is None or one <= 0: return 0, "[]", 0.0 return 1, json.dumps([one]), one unit = amount_precise(exchange_symbol, rem / n) if unit is None or unit <= 0: one = amount_precise(exchange_symbol, rem) if one is None or one <= 0: return 0, "[]", 0.0 return 1, json.dumps([one]), one parts: list[float] = [] acc = 0.0 for _ in range(n - 1): parts.append(unit) acc += unit last = amount_precise(exchange_symbol, max(0.0, rem - acc)) if last is None or last <= 0: one = amount_precise(exchange_symbol, rem) if one is None or one <= 0: return 0, "[]", 0.0 return 1, json.dumps([one]), one parts.append(last) return n, json.dumps(parts), unit def compute_trend_plan_core( *, direction: str, stop_loss: float, add_upper: float, risk_percent: float, snapshot_usdt: float, leverage: int, live_price: float, target_order_amount: float, exchange_symbol: str, dca_legs: int, amount_precise: AmountPreciseFn, min_amount: float = 0.0, full_margin_buffer_ratio: float = 0.95, ) -> Tuple[Optional[dict[str, Any]], Optional[str]]: """在已有 target_order_amount 时组装预览 payload(张数由调用方 prepare_order_amount 计算)。""" rf = calc_risk_fraction(direction, add_upper, stop_loss) if rf is None or rf <= 0: return None, "止损与补仓区间边界组合无法计算风险比例" risk_budget = float(snapshot_usdt) * (float(risk_percent) / 100.0) notional = risk_budget / rf margin_plan = notional / float(leverage) margin_plan = min(margin_plan, float(snapshot_usdt) * float(full_margin_buffer_ratio)) if margin_plan <= 0: return None, "计划保证金过小" first_amt = amount_precise(exchange_symbol, float(target_order_amount) * 0.5) if first_amt is None or first_amt <= 0: return None, "首仓张数过小(低于交易所最小张数),请提高风险比例或杠杆" remainder_total = amount_precise(exchange_symbol, max(0.0, float(target_order_amount) - float(first_amt))) if remainder_total is None: remainder_total = 0.0 n_legs, leg_json, per_ref = build_leg_amounts_json( exchange_symbol, remainder_total, dca_legs, amount_precise, min_amount ) if n_legs <= 0: return None, "剩余计划张数不足以拆出补仓档,请提高风险比例或放宽止损与补仓区间间距" grid = build_grid_prices(direction, stop_loss, add_upper, n_legs) if len(grid) != n_legs: return None, "补仓网格生成失败" try: leg_list = json.loads(leg_json) except Exception: leg_list = [] payload = { "direction": direction, "stop_loss": float(stop_loss), "add_upper": float(add_upper), "risk_percent": float(risk_percent), "snapshot_available_usdt": float(snapshot_usdt), "live_price_ref": float(live_price), "plan_margin_capital": float(margin_plan), "target_order_amount": float(target_order_amount), "first_order_amount": float(first_amt), "remainder_total": float(remainder_total), "dca_legs": int(n_legs), "per_leg_amount": float(per_ref), "grid_prices_json": json.dumps(grid), "leg_amounts_json": leg_json, "grid": grid, "leg_amounts": leg_list, } return payload, None