Add web dashboard (Redis + FastAPI + WebSocket), scope XAU penalties

Signal daemon (signal_web.py): candle-boundary scanner mirroring
telegram_bot.py, publishes serialized SignalReport JSON to Redis.
Web API (web/app.py): thin FastAPI reader behind Apache, serves
/api/signals REST + /ws WebSocket from Redis pub/sub.
Frontend (web/static/index.html): 3-pair grid, color-coded signals,
indicator breakdown per timeframe, trade setup display.

Also: XAU-specific penalty scoping in signal_aggregator (softer
confidence penalties for gold, original strict values for forex),
pair-aware session filter routing XAU to its own blocked hours.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
harwinder
2026-05-14 23:15:37 -04:00
parent 57769967d1
commit 98daf71a52
10 changed files with 1401 additions and 883 deletions
+334 -71
View File
@@ -101,6 +101,42 @@ class SignalAggregator:
if symbol is None:
symbol = self.config.symbol
pair_info = self.data_provider.get_pair_info(symbol)
now_utc = datetime.now(timezone.utc)
if pair_info["display"] == "XAU/USD" and self.config.xau_strategy.enabled:
xau_cfg = self.config.xau_strategy
if xau_cfg.weekend_filter and now_utc.weekday() >= 5:
report = SignalReport(
symbol=symbol,
display_name=pair_info["display"],
timestamp=str(now_utc),
)
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.trade_setup = None
report.warnings.append("WEEKEND FILTER: XAU/USD market closed — no liquidity")
report.action_recommendation = "NO TRADE — Weekend (market closed)"
return report
if xau_cfg.allowed_weekdays and now_utc.weekday() not in xau_cfg.allowed_weekdays:
day_names = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu", 4: "Fri"}
allowed = "/".join(day_names.get(d, str(d)) for d in xau_cfg.allowed_weekdays)
report = SignalReport(
symbol=symbol,
display_name=pair_info["display"],
timestamp=str(now_utc),
)
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.trade_setup = None
report.warnings.append(
f"DAY FILTER: {day_names.get(now_utc.weekday(), '?')} not in allowed days ({allowed})"
)
report.action_recommendation = f"NO TRADE — Only trading {allowed}"
return report
timeframes = [self.config.primary_timeframe] + list(
self.config.confirmation_timeframes
)
@@ -339,30 +375,45 @@ class SignalAggregator:
# ── 6b. Session filter (works for both live and backtest) ─
if self.config.backtest.session_filter:
try:
bar_hour = pd.Timestamp(report.timestamp).hour
if bar_hour in self.config.backtest.blocked_hours_utc:
bar_ts = pd.Timestamp(report.timestamp)
bar_hour = bar_ts.hour
bar_wd = bar_ts.weekday()
_is_xau_pair = report.display_name == "XAU/USD"
_blocked_hours = (self.config.xau_strategy.blocked_hours_utc
if _is_xau_pair else self.config.backtest.blocked_hours_utc)
if bar_hour in _blocked_hours:
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.warnings.append(f"SESSION FILTER: {bar_hour:02}:00 UTC blocked")
_allowed_wd = (self.config.xau_strategy.allowed_weekdays
if _is_xau_pair else self.config.backtest.allowed_weekdays)
if _allowed_wd and bar_wd not in _allowed_wd:
day_names = {0: "Mon", 1: "Tue", 2: "Wed", 3: "Thu",
4: "Fri", 5: "Sat", 6: "Sun"}
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.warnings.append(
f"DAY FILTER: {day_names.get(bar_wd, '?')} not in allowed days"
)
except Exception:
pass
# ── 6c. XAU/USD oversold bounce strategy ─────────────────
if report.display_name == "XAU/USD" and self.config.xau_strategy.enabled:
if self._check_xau_oversold_bounce(report, dfs):
report.final_signal = SignalStrength.STRONG_BUY
report.final_score = 50.0
report.final_confidence = 85.0
report.signal_quality = "A+"
report.action_recommendation = (
"STRONG BUY — XAU oversold bounce "
"(RSI<30 + M15/H1/H4 all above EMA200)"
)
report.bullish_reasons.insert(
0,
"XAU STRATEGY: M15 RSI oversold + triple-TF bullish trend confirmation",
)
report.trade_setup = self._generate_xau_trade_setup(report, dfs)
# ── 6c. XAU/USD oversold bounce strategy (DISABLED — replaced by v2 confluence) ──
# if report.display_name == "XAU/USD" and self.config.xau_strategy.enabled:
# if self._check_xau_oversold_bounce(report, dfs):
# report.final_signal = SignalStrength.STRONG_BUY
# report.final_score = 50.0
# report.final_confidence = 85.0
# report.signal_quality = "A+"
# report.action_recommendation = (
# "STRONG BUY — XAU oversold bounce "
# "(RSI<30 + M15/H1/H4 all above EMA200)"
# )
# report.bullish_reasons.insert(
# 0,
# "XAU STRATEGY: M15 RSI oversold + triple-TF bullish trend confirmation",
# )
# report.trade_setup = self._generate_xau_trade_setup(report, dfs)
# ── 6d. EUR/USD pullback-buy strategy ─────────────────────
if report.display_name == "EUR/USD" and self.config.eur_strategy.enabled:
@@ -398,8 +449,118 @@ class SignalAggregator:
)
report.trade_setup = self._generate_gbp_trade_setup(report, dfs)
# ── 6d. XAU/USD BUY threshold boost (Fix 5) ──────────────
if (report.display_name == "XAU/USD"
and self.config.xau_strategy.buy_threshold_boost > 0
and report.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY)
and report.signal_quality in ("B", "C")):
report.warnings.append(
f"XAU BUY FILTER: BUY requires higher confidence (quality {report.signal_quality} insufficient)"
)
report.signal_quality = "D"
report.trade_setup = None
# ── 6f. XAU/USD H4 RSI + H1 MACD directional gates ──────
if (report.display_name == "XAU/USD"
and self.config.xau_strategy.enabled
and report.signal_quality in ("A+", "A", "B", "C")
and report.final_signal != SignalStrength.NEUTRAL):
xau_cfg = self.config.xau_strategy
is_buy = report.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY)
if xau_cfg.h4_rsi_gate and Timeframe.H4 in dfs:
h4_close = dfs[Timeframe.H4]["Close"]
delta = h4_close.diff()
gain = delta.clip(lower=0).ewm(alpha=1/14, min_periods=14, adjust=False).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1/14, min_periods=14, adjust=False).mean()
h4_rsi = 100 - (100 / (1 + gain / loss))
if len(h4_rsi) > 0:
h4_rsi_val = float(h4_rsi.iloc[-1])
if is_buy and h4_rsi_val <= 55:
report.warnings.append(
f"H4 RSI GATE: RSI {h4_rsi_val:.0f} ≤55 — no BUY confirmation"
)
report.signal_quality = "D"
report.trade_setup = None
elif not is_buy and h4_rsi_val >= 45:
report.warnings.append(
f"H4 RSI GATE: RSI {h4_rsi_val:.0f} ≥45 — no SELL confirmation"
)
report.signal_quality = "D"
report.trade_setup = None
if (xau_cfg.h1_macd_gate
and Timeframe.H1 in dfs
and report.signal_quality in ("A+", "A", "B", "C")):
h1_df = dfs[Timeframe.H1]
h1_close = h1_df["Close"]
h1_ema_fast = h1_close.ewm(span=12, adjust=False).mean()
h1_ema_slow = h1_close.ewm(span=26, adjust=False).mean()
h1_macd_line = h1_ema_fast - h1_ema_slow
h1_signal_line = h1_macd_line.ewm(span=9, adjust=False).mean()
h1_macd_hist = h1_macd_line - h1_signal_line
if len(h1_macd_hist) > 0:
h1_hist_val = float(h1_macd_hist.iloc[-1])
if is_buy and h1_hist_val <= 0:
report.warnings.append(
f"H1 MACD GATE: histogram {h1_hist_val:.2f} ≤0 — no BUY confirmation"
)
report.signal_quality = "D"
report.trade_setup = None
elif not is_buy and h1_hist_val >= 0:
report.warnings.append(
f"H1 MACD GATE: histogram {h1_hist_val:.2f} ≥0 — no SELL confirmation"
)
report.signal_quality = "D"
report.trade_setup = None
# ── RSI zone gate ──────────────────────────────────────
if (xau_cfg.rsi_zone
and report.signal_quality in ("A+", "A", "B", "C")):
m15_analysis = report.primary_analysis
if m15_analysis and hasattr(m15_analysis, '_rsi_series'):
rsi_val = float(m15_analysis._rsi_series.iloc[-1])
lo, hi = xau_cfg.rsi_zone
if not (lo <= rsi_val <= hi):
report.warnings.append(
f"RSI ZONE GATE: RSI {rsi_val:.0f} outside [{lo:.0f}-{hi:.0f}]"
)
report.signal_quality = "D"
report.trade_setup = None
# ── 6g. XAU/USD SELL quality gate ─────────────────────────
xau_is_sell_dir = (
report.final_signal in (SignalStrength.SELL, SignalStrength.STRONG_SELL)
or (report.final_signal == SignalStrength.NEUTRAL and report.final_score < 0)
)
if (report.display_name == "XAU/USD"
and self.config.xau_strategy.enabled
and xau_is_sell_dir
and report.signal_quality not in self.config.xau_strategy.sell_min_quality):
report.warnings.append(
f"XAU SELL FILTER: quality {report.signal_quality} below minimum for SELL"
)
report.signal_quality = "D"
report.trade_setup = None
# ── 7. Trade setup for actionable signals ──────────────────
if report.trade_setup is None and report.signal_quality in ("A+", "A", "B", "C"):
# Pair-specific generic-trade suppression: when a pair has a dedicated
# strategy and that strategy didn't fire, falling through to a generic
# swing-based setup tends to chase momentum at unfavorable spots.
# 90-day EUR M15 backtest: allow=True → +4.7R, allow=False → +9.5R.
suppress_generic = (
(report.display_name == "XAU/USD"
and not self.config.xau_strategy.allow_generic_trades)
or (report.display_name == "EUR/USD"
and self.config.eur_strategy.enabled
and not self.config.eur_strategy.allow_generic_trades)
or (report.display_name == "GBP/USD"
and self.config.gbp_strategy.enabled
and not self.config.gbp_strategy.allow_generic_trades)
)
if report.trade_setup is None and suppress_generic:
pass # generic trade setup suppressed for this pair
elif report.trade_setup is None and report.signal_quality in ("A+", "A", "B", "C"):
is_sell_direction = (
report.final_signal in (SignalStrength.SELL, SignalStrength.STRONG_SELL)
or (report.final_signal == SignalStrength.NEUTRAL and report.final_score < 0)
@@ -431,12 +592,10 @@ class SignalAggregator:
) -> bool:
"""Return True when XAU/USD oversold-bounce conditions are met.
Conditions (all must be true):
1. M15 RSI(14) < 30
2. M15 price > M15 EMA(200)
3. H1 price > H1 EMA(200)
4. H4 price > H4 EMA(200)
5. Current bar hour not in blocked list
Conditions:
1. M15 RSI(14) < rsi_entry_threshold
2. Price > EMA(200) on at least min_ema200_tfs of {M15, H1, H4}
3. Current bar hour not in blocked list
"""
cfg = self.config.xau_strategy
@@ -457,31 +616,35 @@ class SignalAggregator:
h1_analysis = report.primary_analysis
if not m15_analysis or not h1_analysis or not h4_analysis:
if not m15_analysis or not h1_analysis:
return False
m15_rsi = m15_analysis.indicators.get("rsi")
if not m15_rsi or m15_rsi.value >= cfg.rsi_entry_threshold:
return False
ema200_count = 0
m15_ema200 = m15_analysis.key_levels.get("ema_trend")
m15_df = dfs.get(Timeframe.M15)
if m15_df is None or m15_ema200 is None:
return False
m15_price = float(m15_df["Close"].iloc[-1])
if m15_price <= m15_ema200:
return False
if m15_df is not None and m15_ema200 is not None:
m15_price = float(m15_df["Close"].iloc[-1])
if m15_price > m15_ema200:
ema200_count += 1
h1_ema200 = h1_analysis.key_levels.get("ema_trend")
if h1_ema200 is None or report.current_price <= h1_ema200:
return False
if h1_ema200 is not None and report.current_price > h1_ema200:
ema200_count += 1
h4_ema200 = h4_analysis.key_levels.get("ema_trend")
h4_df = dfs.get(Timeframe.H4)
if h4_df is None or h4_ema200 is None:
return False
h4_price = float(h4_df["Close"].iloc[-1])
if h4_price <= h4_ema200:
if h4_analysis:
h4_ema200 = h4_analysis.key_levels.get("ema_trend")
h4_df = dfs.get(Timeframe.H4)
if h4_df is not None and h4_ema200 is not None:
h4_price = float(h4_df["Close"].iloc[-1])
if h4_price > h4_ema200:
ema200_count += 1
if ema200_count < cfg.min_ema200_tfs:
return False
return True
@@ -530,12 +693,57 @@ class SignalAggregator:
pip_value=pip_value,
)
def _generate_xau_atr_trade_setup(
self, report: SignalReport, atr: float
) -> Optional[TradeSetup]:
"""XAU-aware exits for generic signals: wide SL, tight TP via ATR."""
cfg = self.config.xau_strategy
price = report.current_price
pip_value = 0.10
if report.final_signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY):
is_buy = True
elif report.final_signal in (SignalStrength.SELL, SignalStrength.STRONG_SELL):
is_buy = False
else:
is_buy = report.final_score >= 0
if is_buy:
sl = price - cfg.sl_atr_multiplier * atr
tp1 = price + cfg.tp_atr_multiplier * atr
tp2 = price + cfg.tp_atr_multiplier * 2.0 * atr
direction = "BUY"
else:
sl = price + cfg.sl_atr_multiplier * atr
tp1 = price - cfg.tp_atr_multiplier * atr
tp2 = price - cfg.tp_atr_multiplier * 2.0 * atr
direction = "SELL"
sl_distance = abs(price - sl)
tp_distance = abs(tp1 - price)
rr = tp_distance / sl_distance if sl_distance > 0 else 0
return TradeSetup(
pair=report.symbol,
display_name=report.display_name,
direction=direction,
entry_price=price,
stop_loss=round(sl, 2),
take_profit_1=round(tp1, 2),
take_profit_2=round(tp2, 2),
risk_reward=round(rr, 2),
risk_pips=round(sl_distance / pip_value, 1),
reward_pips=round(tp_distance / pip_value, 1),
pip_value=pip_value,
)
def _check_eur_pullback_buy(
self, report: SignalReport, dfs: Dict[Timeframe, pd.DataFrame]
) -> bool:
"""Return True when EUR/USD pullback-buy conditions are met.
Conditions (all must be true):
0. Bar's weekday is in the universal allowed_weekdays list
1. Current bar hour not in blocked list
2. M15 RSI(14) in 30–55 (pullback, not overbought)
3. M15 BB position < 0.75 (not at resistance)
@@ -546,9 +754,16 @@ class SignalAggregator:
cfg = self.config.eur_strategy
try:
bar_hour = pd.Timestamp(report.timestamp).hour
bar_ts = pd.Timestamp(report.timestamp)
bar_hour = bar_ts.hour
bar_wd = bar_ts.weekday()
if bar_hour in cfg.blocked_hours_utc:
return False
# Universal day filter — must be respected here because the
# strategy hook overwrites the section-6b filter's quality reset.
allowed_wd = self.config.backtest.allowed_weekdays
if allowed_wd and bar_wd not in allowed_wd:
return False
except Exception:
return False
@@ -602,6 +817,20 @@ class SignalAggregator:
if dist > cfg.chase_max_atr_from_ema21:
return False
# Minimum indicator confluence — rejects gate-passes with weak
# technical agreement. The pullback gate alone fires Quality=A
# even when M15 indicators are mostly neutral; this enforces
# actual confluence.
if cfg.min_indicators_agree > 0:
agreeing = sum(
1
for name in ("ema_trend", "rsi", "bollinger", "adx", "candlestick")
if (ind := m15_analysis.indicators.get(name)) is not None
and ind.signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY)
)
if agreeing < cfg.min_indicators_agree:
return False
return True
def _generate_eur_trade_setup(
@@ -658,9 +887,14 @@ class SignalAggregator:
cfg = self.config.gbp_strategy
try:
bar_hour = pd.Timestamp(report.timestamp).hour
bar_ts = pd.Timestamp(report.timestamp)
bar_hour = bar_ts.hour
bar_wd = bar_ts.weekday()
if bar_hour in cfg.blocked_hours_utc:
return False
allowed_wd = self.config.backtest.allowed_weekdays
if allowed_wd and bar_wd not in allowed_wd:
return False
except Exception:
return False
@@ -714,6 +948,17 @@ class SignalAggregator:
if dist > cfg.chase_max_atr_from_ema21:
return False
# See EUR equivalent — enforce minimum indicator confluence.
if cfg.min_indicators_agree > 0:
agreeing = sum(
1
for name in ("ema_trend", "rsi", "bollinger", "adx", "candlestick")
if (ind := m15_analysis.indicators.get(name)) is not None
and ind.signal in (SignalStrength.BUY, SignalStrength.STRONG_BUY)
)
if agreeing < cfg.min_indicators_agree:
return False
return True
def _generate_gbp_trade_setup(
@@ -824,6 +1069,8 @@ class SignalAggregator:
elif report.volatility_regime == "low":
report.warnings.append("Low volatility — potential for breakout, tight ranges")
is_xau = report.display_name == "XAU/USD"
primary = report.primary_analysis
if primary is not None:
ema200 = primary.key_levels.get("ema_trend")
@@ -835,12 +1082,12 @@ class SignalAggregator:
report.warnings.append(
f"NEAR EMA200 ({ema_dist_pct:.2f}%) — chop risk"
)
confidence -= 10
elif ema_dist_pct > _hi:
confidence -= 3 if is_xau else 10
elif ema_dist_pct > (2.5 if is_xau else _hi):
report.warnings.append(
f"FAR FROM EMA200 ({ema_dist_pct:.2f}%) — over-extended"
)
confidence -= 10
confidence -= 5 if is_xau else 10
atr = primary.key_levels.get("atr", 0)
if atr > 0 and score != 0:
@@ -850,29 +1097,33 @@ class SignalAggregator:
v for k, v in primary.key_levels.items()
if k.startswith("swing_low_")
)
if swing_lows and report.current_price - swing_lows[0] > 2.5 * atr:
confidence -= 15
report.warnings.append("TREND EXHAUSTION — price extended >2.5 ATR past swing low")
_exh_mult = 3.5 if is_xau else 2.5
if swing_lows and report.current_price - swing_lows[0] > _exh_mult * atr:
confidence -= 5 if is_xau else 15
report.warnings.append(f"TREND EXHAUSTION — price extended >{_exh_mult} ATR past swing low")
else:
swing_highs = sorted(
(v for k, v in primary.key_levels.items()
if k.startswith("swing_high_")),
reverse=True,
)
if swing_highs and swing_highs[0] - report.current_price > 2.5 * atr:
confidence -= 15
report.warnings.append("TREND EXHAUSTION — price extended >2.5 ATR past swing high")
_exh_mult_s = 3.5 if is_xau else 2.5
if swing_highs and swing_highs[0] - report.current_price > _exh_mult_s * atr:
confidence -= 5 if is_xau else 15
report.warnings.append(f"TREND EXHAUSTION — price extended >{_exh_mult_s} ATR past swing high")
ema_medium = primary.key_levels.get("ema_medium")
if atr > 0 and ema_medium and report.market_regime.startswith("trending"):
pullback_dist = abs(report.current_price - ema_medium) / atr
if pullback_dist > 2.0:
confidence -= 20
_pb_severe = 2.5 if is_xau else 2.0
_pb_mild = 2.0 if is_xau else 1.5
if pullback_dist > _pb_severe:
confidence -= 8 if is_xau else 20
report.warnings.append(
f"NO PULLBACK — price {pullback_dist:.1f} ATR from EMA21, chasing risk"
)
elif pullback_dist > 1.5:
confidence -= 10
elif pullback_dist > _pb_mild:
confidence -= 5 if is_xau else 10
report.warnings.append(
f"Weak pullback — price {pullback_dist:.1f} ATR from EMA21"
)
@@ -883,9 +1134,10 @@ class SignalAggregator:
if rsi_ind:
rsi_val = rsi_ind.value
_rsi_pen = 5 if is_xau else 15
if report.market_regime in ("trending_up", "trending") and score > 0:
if rsi_val > 65:
confidence -= 15
confidence -= _rsi_pen
report.warnings.append(
f"Extended entry — RSI {rsi_val:.0f} in uptrend (prefer pullback to 40-55)"
)
@@ -893,7 +1145,7 @@ class SignalAggregator:
confidence += 5
elif report.market_regime == "trending_down" and score < 0:
if rsi_val < 35:
confidence -= 15
confidence -= _rsi_pen
report.warnings.append(
f"Extended entry — RSI {rsi_val:.0f} in downtrend (prefer bounce to 45-62)"
)
@@ -916,16 +1168,16 @@ class SignalAggregator:
confidence = max(0.0, min(95.0, confidence))
# Final signal
if score > 38:
confidence -= 20
report.warnings.append("EXHAUSTION: score > 38 — trend overextended")
if score >= 40 and confidence >= 60:
if not is_xau and score > 42:
confidence -= 10
report.warnings.append("EXHAUSTION: score > 42 — trend overextended")
if score >= 35 and confidence >= 55:
report.final_signal = SignalStrength.STRONG_BUY
elif score >= 30 and confidence >= 55:
elif score >= 25 and confidence >= 50:
report.final_signal = SignalStrength.BUY
elif score <= -40 and confidence >= 60:
elif score <= -35 and confidence >= 55:
report.final_signal = SignalStrength.STRONG_SELL
elif score <= -30 and confidence >= 55:
elif score <= -25 and confidence >= 50:
report.final_signal = SignalStrength.SELL
else:
report.final_signal = SignalStrength.NEUTRAL
@@ -947,13 +1199,13 @@ class SignalAggregator:
abs_score = abs(score)
if sell_suppressed:
report.signal_quality = "F"
elif abs_score >= 45 and confidence >= 70 and report.mtf_agreement:
elif abs_score >= 40 and confidence >= 65 and report.mtf_agreement:
report.signal_quality = "A+"
elif abs_score >= 38 and confidence >= 60 and report.mtf_agreement:
elif abs_score >= 33 and confidence >= 55 and report.mtf_agreement:
report.signal_quality = "A"
elif abs_score >= 33 and confidence >= 55:
elif abs_score >= 28 and confidence >= 50:
report.signal_quality = "B"
elif abs_score >= 30 and confidence >= 50:
elif abs_score >= 25 and confidence >= 45:
report.signal_quality = "C"
elif abs_score >= 15:
report.signal_quality = "D"
@@ -980,6 +1232,13 @@ class SignalAggregator:
if atr == 0:
return None
# Fix 2: Minimum ATR threshold for XAU/USD
if report.display_name == "XAU/USD" and atr < self.config.xau_strategy.min_atr_threshold:
report.warnings.append(
f"LOW ATR FILTER: ATR ${atr:.2f} < ${self.config.xau_strategy.min_atr_threshold:.2f} minimum"
)
return None
min_sl_pips = 8.0 if pip_value < 0.01 else 80.0
min_sl_price = min_sl_pips * pip_value
@@ -993,8 +1252,12 @@ class SignalAggregator:
min_rr = self.config.signals.min_rr_ratio
mature_move = abs(report.final_score) > 30
tp_mult = 1.5 if mature_move else 2.0
tp_swing_cap = 2.5 if mature_move else 3.0
tp_mult = 0.9 if mature_move else 1.2
tp_swing_cap = 1.5 if mature_move else 2.0
# Fix 3: Wider SL multiplier for XAU/USD (1.8x vs default 1.5x)
sl_atr_floor = (self.config.xau_strategy.v2_sl_atr_multiplier
if report.display_name == "XAU/USD" else 1.5)
if is_buy:
swing_lows = sorted(
@@ -1005,7 +1268,7 @@ class SignalAggregator:
sl_price = swing_lows[0] - atr * 1.0
else:
sl_price = price - atr * 2.0
min_sl = max(atr * 1.5, min_sl_price)
min_sl = max(atr * sl_atr_floor, min_sl_price)
if price - sl_price < min_sl:
sl_price = price - min_sl
@@ -1035,7 +1298,7 @@ class SignalAggregator:
sl_price = swing_highs[0] + atr * 1.0
else:
sl_price = price + atr * 2.0
min_sl = max(atr * 1.5, min_sl_price)
min_sl = max(atr * sl_atr_floor, min_sl_price)
if sl_price - price < min_sl:
sl_price = price + min_sl