From 665c12fc319d1a001a6122ed6562b82f543e7c4a Mon Sep 17 00:00:00 2001 From: eezd Date: Thu, 15 Oct 2026 17:33:06 +0800 Subject: [PATCH] Keep demo stops at least 1 x ATR14(5m) from the limit price When the limit price sat near the far edge of the zone, the stop beyond the far edge was well inside normal 5m noise (plan #34: 0.11%). The stop is now the farther of far edge +/- 0.5 x ATR and entry +/- 1 x ATR. --- backend/src/alphapulse/trading/analyst.py | 2 +- backend/src/alphapulse/trading/plan.py | 19 +++++++++++-------- backend/tests/test_plan.py | 13 ++++++++++--- web/src/pages/trading/components/PlanCard.vue | 2 +- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/backend/src/alphapulse/trading/analyst.py b/backend/src/alphapulse/trading/analyst.py index 650483d..c35f41d 100644 --- a/backend/src/alphapulse/trading/analyst.py +++ b/backend/src/alphapulse/trading/analyst.py @@ -61,7 +61,7 @@ INSTRUCTIONS = f"""你是 AlphaPulse 的模拟盘交易分析器:为一个币 ## 程序如何执行 - 计划(action=plan)被采纳后,程序立即在 entry_price 挂限价单(maker 手续费),同时在交易所挂只减仓止损(按标记价触发)。到 valid_hours 仍未成交则撤单,并重新请你分析。 -- 止损:区间远端外 {rules.STOP_ATR_FACTOR}×ATR14(5m),做多放在 zone_low 下方,做空放在 zone_high 上方。 +- 止损:区间远端外 {rules.STOP_ATR_FACTOR}×ATR14(5m),做多放在 zone_low 下方,做空放在 zone_high 上方;且距挂单价至少 {rules.MIN_STOP_ATR}×ATR14(5m),挂单价贴近远端时按这条算。 - 止盈:按成交均价,做多 {_ladder("long")};做空 {_ladder("short")}。止盈 1 成交后撤销未成交的入场部分,止损移到保本价。 - 首次成交后最长持仓 {rules.MAX_HOLD_HOURS} 小时,到时市价平仓。平仓后程序会请你重新分析。 - 仓位:单笔风险为交易资金的 {_pct(rules.RISK_PCT)},置信度低时减半;名义仓位合计不超过 {rules.MAX_LEVERAGE} 倍交易资金。 diff --git a/backend/src/alphapulse/trading/plan.py b/backend/src/alphapulse/trading/plan.py index 1ddb2aa..897b14d 100644 --- a/backend/src/alphapulse/trading/plan.py +++ b/backend/src/alphapulse/trading/plan.py @@ -1,7 +1,7 @@ """交易计划的纯计算:模型提交的区间和限价、校验、止损与仓位、止盈阶梯、应有订单。 只做短线做波动:模型划出支撑区(压力区)并给出区内的限价,采纳后立即挂限价单(maker 手续费); -止损放在区间远端外,与入场单一起挂;止盈按成交均价的固定比例。 +止损放在区间远端外(且离挂单价至少 1×ATR),与入场单一起挂;止盈按成交均价的固定比例。 下方常量与 main/README 的数字重复,并写进了分析器的提示词;改 README 时要同步修改。 """ @@ -24,8 +24,10 @@ SETUP_VALID_HOURS = 2 MAX_HOLD_HOURS = 4 ATR_INTERVAL = "5m" ATR_PERIOD = 14 -# 止损放在区间远端外 STOP_ATR_FACTOR × ATR14(5m);离挂单价超过 MAX_STOP_PCT 的计划不做 +# 止损放在区间远端外 STOP_ATR_FACTOR × ATR14(5m),且离挂单价至少 MIN_STOP_ATR × ATR: +# 挂单价贴近远端时,只按远端算的止损会落在 5m 正常波动里(#34 只有 0.11%)。离挂单价超过 MAX_STOP_PCT 的计划不做 STOP_ATR_FACTOR = Decimal("0.5") +MIN_STOP_ATR = Decimal("1") MAX_STOP_PCT = Decimal("0.01") # 止盈阶梯:(距成交均价的涨跌幅,平仓比例)。牛市里空单不恋战,第一档就平掉大部分 TAKE_PROFIT_STEPS: dict[str, tuple[tuple[Decimal, Decimal], ...]] = { @@ -145,11 +147,11 @@ def atr(bars: list[Bar], period: int = ATR_PERIOD) -> Decimal: return sum(ranges, Decimal(0)) / period -def setup_stop(direction: Direction, far_edge: Decimal, atr_value: Decimal, tick: Decimal) -> Decimal: - """止损 = 区间远端外 STOP_ATR_FACTOR × ATR,向远离入场的方向取整""" +def setup_stop(direction: Direction, entry: Decimal, far_edge: Decimal, atr_value: Decimal, tick: Decimal) -> Decimal: + """止损 = 区间远端外 STOP_ATR_FACTOR × ATR 与挂单价外 MIN_STOP_ATR × ATR 中离挂单价更远的一个,向远离入场的方向取整""" if direction == "long": - return round_to_step(far_edge - STOP_ATR_FACTOR * atr_value, tick, ROUND_FLOOR) - return round_to_step(far_edge + STOP_ATR_FACTOR * atr_value, tick, ROUND_CEILING) + return round_to_step(min(far_edge - STOP_ATR_FACTOR * atr_value, entry - MIN_STOP_ATR * atr_value), tick, ROUND_FLOOR) + return round_to_step(max(far_edge + STOP_ATR_FACTOR * atr_value, entry + MIN_STOP_ATR * atr_value), tick, ROUND_CEILING) def size_entry(direction: Direction, price: Decimal, stop: Decimal, *, capital: Decimal, other_notional: Decimal, @@ -242,12 +244,13 @@ def check_setup(plan: TradePlan, ctx: PlanContext) -> dict: errors.append(f"valid_hours 不能超过 {SETUP_VALID_HOURS}") if errors: raise PlanError(errors) - stop = setup_stop(direction, setup.zone_low if long else setup.zone_high, ctx.atr, rules.tick_size) + stop = setup_stop(direction, entry, setup.zone_low if long else setup.zone_high, ctx.atr, rules.tick_size) try: sizing = size_entry(direction, entry, stop, capital=ctx.capital, other_notional=ctx.other_notional, confidence=plan.confidence, rules=rules) except PlanError as e: - raise PlanError([f"止损放在区间{'下沿' if long else '上沿'}外 {STOP_ATR_FACTOR}×ATR(5m)={stop}:{e}。" + raise PlanError([f"止损 {stop}(区间{'下沿' if long else '上沿'}外 {STOP_ATR_FACTOR}×ATR(5m),且距挂单价至少 " + f"{MIN_STOP_ATR}×ATR(5m),ATR={ctx.atr.quantize(rules.tick_size)}):{e}。" "区间太宽或挂单价离区间远端太远,请收窄区间或调整挂单价"]) from e return { "direction": direction, diff --git a/backend/tests/test_plan.py b/backend/tests/test_plan.py index a7660fa..dc369a8 100644 --- a/backend/tests/test_plan.py +++ b/backend/tests/test_plan.py @@ -82,14 +82,21 @@ def test_atr_and_stop_from_example(): value = atr(bars_until("10:25")) assert value.quantize(Decimal("0.0001")) == Decimal("302.9714") # 做多:支撑区 80200–80700 内挂 80500;止损 80200 − 0.5 × 302.97 = 80048.51 → 向下取整 80048.5,距挂单价 0.56% - stop = setup_stop("long", Decimal("80200"), value, BTC.tick_size) + stop = setup_stop("long", Decimal("80500"), Decimal("80200"), value, BTC.tick_size) assert stop == Decimal("80048.5") entry = size_entry("long", Decimal("80500"), stop, capital=Decimal("1000"), other_notional=Decimal("0"), confidence="medium", rules=BTC) assert entry["stop_pct"] == "0.56" # 10 ÷ (451.5 + (80500 + 80048.5) × 0.0004 = 515.72) = 0.01939… → 0.0193 assert entry["quantity"] == "0.0193" - # 做空:压力区上沿 81700 + 0.5 × 362.56 = 81881.28 → 向上取整 81881.3 - assert setup_stop("short", Decimal("81700"), atr(bars_until("10:55")), BTC.tick_size) == Decimal("81881.3") + # 做空:区间上沿 81700 + 0.5 × 362.56 = 81881.28 离挂单价 81600 不到 1×ATR,取 81600 + 362.56 = 81962.56 → 向上取整 81962.6 + assert setup_stop("short", Decimal("81600"), Decimal("81700"), atr(bars_until("10:55")), BTC.tick_size) == Decimal("81962.6") + + +def test_stop_keeps_one_atr_from_entry(): + # 计划 #34:做空挂 81690,区间上沿 81720.9,ATR 118.5。按远端算是 81780.2(0.11%),至少 1×ATR 后为 81808.5 + assert setup_stop("short", Decimal("81690"), Decimal("81720.9"), Decimal("118.5"), BTC.tick_size) == Decimal("81808.5") + # 做多挂在下沿上方 50:80250 − 302.97 = 79947.03 → 向下取整 79947.0 + assert setup_stop("long", Decimal("80250"), Decimal("80200"), atr(bars_until("10:25")), BTC.tick_size) == Decimal("79947.0") def test_size_entry_rejects_far_stop_and_halves_low_confidence(): diff --git a/web/src/pages/trading/components/PlanCard.vue b/web/src/pages/trading/components/PlanCard.vue index 5d85244..d697275 100644 --- a/web/src/pages/trading/components/PlanCard.vue +++ b/web/src/pages/trading/components/PlanCard.vue @@ -135,7 +135,7 @@ const ladder = computed(() => sizing.value?.entry && entryRow.value (名义 {{ sizing.entry.notional }} USDT,{{ sizing.entry.leverage }} 倍交易资金)
- 止损{{ currentStop ?? sizing.entry.stop }}(初始 {{ sizing.entry.stop }},距挂单价 {{ sizing.entry.stop_pct }}%;区间远端外 0.5×ATR(5m),止盈 1 后移到保本价) + 止损{{ currentStop ?? sizing.entry.stop }}(初始 {{ sizing.entry.stop }},距挂单价 {{ sizing.entry.stop_pct }}%;区间远端外 0.5×ATR(5m)、距挂单价至少 1×ATR(5m),止盈 1 后移到保本价)
止损亏损{{ sizing.entry.risk_at_stop }} USDT(交易资金的 {{ sizing.entry.risk_pct }}%){{ sizing.entry.capped_by_leverage ? ",受 3 倍上限限制" : "" }}