File size: 18,763 Bytes
aa79b15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6db631e
 
 
 
 
 
 
 
aa79b15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6db631e
 
 
 
aa79b15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c8a3a1
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
"""analysis/timeframes.py β€” HTF/MTF/LTF/WTF timeframe analysis functions."""
import numpy as np
import pandas as pd
from config import (RSI_OB, RSI_OS, RSI_MA_PERIOD, ADX_MIN, MTF_MIN_SCORE)
from analysis.structure import detect_swings, find_sr, analyse_structure, analyse_volume, analyse_sr, analyse_momentum
from data.oanda import pip, digs

def analyse_htf(df):
    """
    D1 bias using EMA20 / EMA50 / SMA200 stack.

    Bullish: EMA20 > EMA50 > SMA200  AND  price above EMA20
    Bearish: EMA20 < EMA50 < SMA200  AND  price below EMA20
    Neutral: mixed / conflicting

    3-point scoring:
      +1 EMA20 vs EMA50 direction
      +1 Full EMA stack aligned (EMA20/50/SMA200)
      +1 Price on correct side of EMA20
    """
    row = df.iloc[-1]

    ema_bull  = row["ma_fast"] > row["ma_slow"]   # EMA20 > EMA50
    ema_bear  = row["ma_fast"] < row["ma_slow"]

    # Full stack check (EMA20 > EMA50 > SMA200)
    has_trend = "ma_trend" in df.columns and not pd.isna(row.get("ma_trend", float("nan")))
    stack_bull = bool(row.get("ema_stack_bull", False)) if has_trend else ema_bull
    stack_bear = bool(row.get("ema_stack_bear", False)) if has_trend else ema_bear

    # Price vs EMA20
    price_bull = row["close"] > row["ma_fast"]
    price_bear = row["close"] < row["ma_fast"]

    # SMA200 slope (compare to 5 bars ago)
    if has_trend:
        sma200_now  = row["ma_trend"]
        sma200_prev = df["ma_trend"].iloc[-6]
        sma_up   = sma200_now > sma200_prev
        sma_down = sma200_now < sma200_prev
        sma_label = "rising" if sma_up else "falling" if sma_down else "flat"
        sma_val   = f"{sma200_now:.5f}"
    else:
        sma_up = sma_down = False; sma_label = "N/A"; sma_val = "N/A"

    bull = sum([ema_bull, stack_bull, price_bull])
    bear = sum([ema_bear, stack_bear, price_bear])

    if bull >= 2 and bull > bear:   bias = 1
    elif bear >= 2 and bear > bull: bias = -1
    else:                           bias = 0

    return {
    "ema_fast": float(row["ema_fast"]),
    "ema_mid": float(row["ema_mid"]),
    "ema_slow": float(row["ema_slow"]),
    "bias": bias,
    "rsi": float(row["rsi"]),
    "atr": float(row["atr"]),
    "sr_levels": find_sr(df, instrument, n=8),
}



def detect_rsi_ma_bounce(df, bias):
    """
    RSI SMA-9 Bounce β€” 4H bonus confluence check.

    Bullish bounce (bias=1):
      RSI dipped below its SMA9 recently (last 5 bars)
      RSI is now back above SMA9 and rising
      β†’ Momentum pulled back to average and is resuming upward

    Bearish bounce (bias=-1):
      RSI rallied above its SMA9 recently (last 5 bars)
      RSI is now back below SMA9 and falling
      β†’ Momentum bounced to average and is resuming downward

    This is a bonus +0.5 β€” not a required check.
    Most valuable in trending markets (ADX > 20).

    Returns: (detected: bool, note: str, rsi_val: float, rsi_ma_val: float)
    """
    if "rsi_ma" not in df.columns or len(df) < RSI_MA_PERIOD + 3:
        return False, "RSI MA not available", 0, 0

    row    = df.iloc[-1]
    prev   = df.iloc[-2]
    recent = df.iloc[-6:-1]   # last 5 bars before current

    rsi_now    = row["rsi"]
    rsi_ma_now = row["rsi_ma"]
    rsi_prev   = prev["rsi"]

    if pd.isna(rsi_ma_now): return False, "RSI MA not ready", rsi_now, 0

    if bias == 1:   # Bullish β€” RSI bouncing up off SMA9
        # Recently dipped below SMA9
        dipped_below = any(recent["rsi"] < recent["rsi_ma"])
        # Now back above SMA9
        back_above   = rsi_now > rsi_ma_now
        # RSI rising
        rsi_rising   = rsi_now > rsi_prev
        # SMA9 itself is rising or flat (not falling steeply)
        ma_direction = rsi_ma_now >= df["rsi_ma"].iloc[-4]

        detected = dipped_below and back_above and rsi_rising
        note     = (f"RSI({rsi_now:.1f}) bounced off SMA9({rsi_ma_now:.1f}) ↑ "
                    f"{'βœ…' if detected else '❌'} "
                    f"| dip:{'βœ…' if dipped_below else '❌'} "
                    f"above:{'βœ…' if back_above else '❌'} "
                    f"rising:{'βœ…' if rsi_rising else '❌'}")

    else:           # Bearish β€” RSI bouncing down off SMA9
        # Recently rallied above SMA9
        rallied_above = any(recent["rsi"] > recent["rsi_ma"])
        # Now back below SMA9
        back_below    = rsi_now < rsi_ma_now
        # RSI falling
        rsi_falling   = rsi_now < rsi_prev
        # SMA9 itself is falling or flat
        ma_direction  = rsi_ma_now <= df["rsi_ma"].iloc[-4]

        detected = rallied_above and back_below and rsi_falling
        note     = (f"RSI({rsi_now:.1f}) rejected at SMA9({rsi_ma_now:.1f}) ↓ "
                    f"{'βœ…' if detected else '❌'} "
                    f"| rally:{'βœ…' if rallied_above else '❌'} "
                    f"below:{'βœ…' if back_below else '❌'} "
                    f"falling:{'βœ…' if rsi_falling else '❌'}")

    return detected, note, round(rsi_now, 1), round(rsi_ma_now, 1)




def find_weekly_sr(df_wtf, instrument, n=6):
    """
    Weekly S&R levels β€” highest quality levels because they represent
    multi-week institutional accumulation/distribution zones.
    Uses swing highs/lows from W1 chart with larger clustering zone.
    """
    if df_wtf.empty or len(df_wtf) < 5:
        return []
    df_sw = detect_swings(df_wtf, n=2)
    highs = df_sw["swing_high"].dropna()
    lows  = df_sw["swing_low"].dropna()
    raw   = pd.concat([highs, lows]).sort_values().values
    if not len(raw): return []
    # Weekly levels cluster in a wider zone (20 pips)
    zone = 20 * pip(instrument)
    clusters = []; cur = [raw[0]]
    for p_val in raw[1:]:
        if p_val - cur[-1] <= zone: cur.append(p_val)
        else: clusters.append(cur); cur = [p_val]
    clusters.append(cur)
    clusters.sort(key=len, reverse=True)
    return [round(np.mean(c), digs(instrument)) for c in clusters[:n]]


def check_round_number(price, instrument, pips=20):
    """
    Round number proximity check.
    Round numbers (1.3000, 150.00, 2.0000) act as magnets β€”
    price gravitates toward them and often reacts strongly.

    Checks if price is within `pips` of a round number.
    Returns (near: bool, nearest_round: float, distance_pips: float)
    """
    p = pip(instrument)
    d = digs(instrument)

    # Determine round number step based on instrument
    if "_JPY" in instrument:
        # JPY pairs: round at 0.5 and whole numbers (e.g. 150.00, 150.50)
        step = 0.5
    elif instrument == "XAU_USD":
        # Gold: round at 10s (1800, 1810, 1900)
        step = 10.0
    else:
        # All others: round at 0.0100 and 0.0050 (e.g. 1.3000, 1.3050)
        step = 0.01

    # Find nearest round number
    nearest = round(round(price / step) * step, d)
    distance_pips = abs(price - nearest) / p

    near = distance_pips <= pips
    return near, nearest, round(distance_pips, 1)


def check_multitf_sr(df_htf, df_mtf, instrument, price):
    """
    Multi-timeframe S&R confluence.
    A level that appears on BOTH D1 and 4H is far stronger than one
    that only shows on 4H alone.

    Returns:
      confluence: bool β€” a level is confirmed on both TFs
      level: float β€” the confluent level
      note: str β€” description
    """
    d1_levels  = find_sr(df_htf, instrument, n=8)
    h4_levels  = find_sr(df_mtf, instrument, n=8)
    zone = 15 * pip(instrument)   # 15 pip matching zone between TFs
    d    = digs(instrument)

    confluent = []
    for d1_l in d1_levels:
        for h4_l in h4_levels:
            if abs(d1_l - h4_l) <= zone:
                avg = (d1_l + h4_l) / 2
                dist = abs(price - avg) / pip(instrument)
                confluent.append((avg, dist))

    if not confluent:
        return False, None, "No D1+4H confluent levels"

    # Find nearest confluent level to current price
    confluent.sort(key=lambda x: x[1])
    best_level, best_dist = confluent[0]

    near = best_dist <= 30  # within 30 pips
    return near, round(best_level, d), f"D1+4H level @ {best_level:.{d}f} ({best_dist:.0f} pips away)"


def analyse_wtf(df):
    """
    Weekly (W1) macro bias β€” top of the stack.

    Uses same EMA20/EMA50/SMA200 logic as D1 but on weekly candles.
    Weekly bias is the most important layer β€” sets the macro direction.

    Scoring (3 points):
      +1  EMA20 vs EMA50 direction
      +1  Full stack aligned (EMA20/50/SMA200)
      +1  Price on correct side of weekly EMA20

    Returns bias: 1 (bullish), -1 (bearish), 0 (neutral)
    Plus extra context: key weekly levels, trend strength
    """
    if df.empty or len(df) < 10:
        return {"bias": 0, "bull_pts": 0, "bear_pts": 0,
                "ema20": None, "ema50": None, "sma200": "N/A",
                "sma_slope": "N/A", "rsi": 50.0, "adx": 0.0,
                "stack_bull": False, "stack_bear": False,
                "note": "Insufficient weekly data"}

    row = df.iloc[-1]

    ema_bull  = row["ma_fast"] > row["ma_slow"]
    ema_bear  = row["ma_fast"] < row["ma_slow"]

    has_trend  = "ma_trend" in df.columns and not pd.isna(row.get("ma_trend", float("nan")))
    stack_bull = bool(row.get("ema_stack_bull", False)) if has_trend else ema_bull
    stack_bear = bool(row.get("ema_stack_bear", False)) if has_trend else ema_bear

    price_bull = row["close"] > row["ma_fast"]
    price_bear = row["close"] < row["ma_fast"]

    if has_trend:
        sma200_now  = row["ma_trend"]
        sma200_prev = df["ma_trend"].iloc[-4]   # 4 weeks ago
        sma_label   = "rising" if sma200_now > sma200_prev else "falling" if sma200_now < sma200_prev else "flat"
        sma_val     = f"{sma200_now:.5f}"
    else:
        sma_label = "N/A"; sma_val = "N/A"; sma200_now = None

    bull = sum([ema_bull, stack_bull, price_bull])
    bear = sum([ema_bear, stack_bear, price_bear])

    if bull >= 2 and bull > bear:   bias = 1
    elif bear >= 2 and bear > bull: bias = -1
    else:                           bias = 0

    # Weekly key levels β€” swing highs/lows as major S/R
    w_sw = detect_swings(df, n=2)
    recent_w = w_sw.iloc[-20:]
    w_highs  = recent_w["swing_high"].dropna().values[-3:] if len(recent_w["swing_high"].dropna()) >= 1 else []
    w_lows   = recent_w["swing_low"].dropna().values[-3:]  if len(recent_w["swing_low"].dropna()) >= 1 else []

    return {
        "bias":        bias,
        "bull_pts":    bull,
        "bear_pts":    bear,
        "ema20":       round(row["ma_fast"], 5),
        "ema50":       round(row["ma_slow"], 5),
        "sma200":      sma_val,
        "sma200_raw":  sma200_now,
        "sma_slope":   sma_label,
        "rsi":         round(row["rsi"], 1),
        "adx":         round(row["adx"], 1),
        "stack_bull":  stack_bull,
        "stack_bear":  stack_bear,
        "zone":        row["price_zone"],
        "w_highs":     [round(h, 5) for h in w_highs],
        "w_lows":      [round(l, 5) for l in w_lows],
        "w_sr":        [],
        "note":        "",
    }

def analyse_mtf(df,htf_bias,instrument):
    if htf_bias==0: return {"confirmed":False,"score":0,"max":7,"checks":{}}
    row=df.iloc[-1]; checks={}; score=0
    def chk(key,ok,note):
        nonlocal score; checks[key]=(ok,note); score+=int(ok)
    # EMA20 vs EMA50 direction
    ema_dir_ok = row["ma_fast"]>row["ma_slow"] if htf_bias==1 else row["ma_fast"]<row["ma_slow"]
    chk("1_ema20_50", ema_dir_ok, f"EMA20({'>' if htf_bias==1 else '<'})EMA50: {row['ma_fast']:.5f} vs {row['ma_slow']:.5f}")

    # Full EMA stack: EMA20/EMA50/SMA200 all aligned
    has_stack = "ema_stack_bull" in df.columns
    if has_stack:
        stack_ok = bool(row.get("ema_stack_bull",False)) if htf_bias==1 else bool(row.get("ema_stack_bear",False))
        sma200   = f"{row['ma_trend']:.5f}" if "ma_trend" in df.columns else "N/A"
        chk("1b_ema_stack", stack_ok, f"EMA20/50/SMA200 stack {'βœ…' if stack_ok else '❌'} | SMA200:{sma200}")
    zone_ok=row["price_zone"] in (("above_both","between_pb_up") if htf_bias==1 else ("below_both","between_pb_dn"))
    zone_labels = {
        "above_both":    "above both EMAs β€” strong trend βœ…",
        "between_pb_up": "between EMAs β€” healthy pullback βœ…",
        "below_both":    "below both EMAs β€” strong downtrend βœ…",
        "between_pb_dn": "between EMAs β€” bearish pullback βœ…",
        "mixed":         "mixed β€” price not clearly positioned ❌",
    }
    zone_note = zone_labels.get(row["price_zone"], row["price_zone"])
    chk("2_zone", zone_ok, f"Zone: {row['price_zone']} β†’ {zone_note}")
    rsi_ok=row["rsi"]<RSI_OB if htf_bias==1 else row["rsi"]>RSI_OS
    chk("3_rsi",rsi_ok,f"RSI:{row['rsi']:.1f}")
    ok,note=analyse_structure(df,htf_bias); chk("4_struct",ok,note)
    ok,note=analyse_volume(df,htf_bias);    chk("5_vol",ok,note)
    ok,note=analyse_sr(df,htf_bias,instrument); chk("6_sr",ok,note)
    ok,note=analyse_momentum(df,htf_bias);  chk("7_mom",ok,note)
    mtf_max = 8 if any("ema_stack" in k for k in checks) else 7

    # ── RSI MA Bounce bonus (+0.5, not counted in max score) ──
    rsi_bounce_ok, rsi_bounce_note, rsi_val, rsi_ma_val = detect_rsi_ma_bounce(df, htf_bias)
    rsi_bonus = 0.5 if rsi_bounce_ok else 0.0

    return {
    "zone": row["price_zone"],
    "sr_levels": find_sr(df, instrument, n=8),
}

def analyse_ltf(df, htf_bias):
    """
    LTF (1H or 30M) entry trigger. Two conditions must pass:

    1. EMA cross β€” EMA20 crossed EMA50 in bias direction within last 5 bars
       OR EMA20 is currently on correct side (crossed more than 5 bars ago
       but still valid β€” strong trend continuation)

    2. RSI confirmation β€” two modes:
       PULLBACK mode (normal): RSI reversed from extreme
         β†’ RSI dipped near OS (sell: spiked near OB) and now reversing
       TREND mode (strong trend, ADX > 30 on LTF):
         β†’ RSI just needs to be on correct side and falling/rising
         β†’ Used when price is in a straight trend without RSI spikes
         β†’ e.g. NZD/USD strong downtrend: RSI at 30 and falling = valid SELL
    """
    if htf_bias == 0:
        return {"triggered":False,"signal":0,"cross_ok":False,"rsi_ok":False,
                "rsi_mode":"none"}

    row    = df.iloc[-1]
    prev   = df.iloc[-2]
    recent = df.iloc[-10:]   # look back 10 bars for RSI extreme

    # ── EMA Cross check ──────────────────────────────────
    # Primary: crossover bar in last 5 bars
    # Secondary: EMA20 already on correct side + crossed in last 10 bars
    # Tertiary: EMA20 aligned + strong trend (ADX > 25) β€” cross happened earlier but trend intact
    if htf_bias == 1:
        cross_recent   = any(df["cross"].iloc[-5:] == 1)
        cross_aligned  = row["ma_fast"] > row["ma_slow"]
        adx_strong_ltf = row["adx"] > 25  # require momentum for trend entries
        cross_extended = any(df["cross"].iloc[-10:] == 1) and adx_strong_ltf
        # cross_trend: EMA aligned + ADX > 25 β€” prevents false entries in ranging markets
        cross_trend    = cross_aligned and adx_strong_ltf and row["price_zone"] in ("above_both", "between_pb_up")
    else:
        cross_recent   = any(df["cross"].iloc[-5:] == -1)
        cross_aligned  = row["ma_fast"] < row["ma_slow"]
        adx_strong_ltf = row["adx"] > 25
        cross_extended = any(df["cross"].iloc[-10:] == -1) and adx_strong_ltf
        # cross_trend: EMA aligned + ADX > 25
        cross_trend    = cross_aligned and adx_strong_ltf and row["price_zone"] in ("below_both", "between_pb_dn")

    cross_ok = cross_recent or cross_extended or cross_trend

    # ── RSI check β€” two modes ────────────────────────────
    adx_strong = row["adx"] > 30   # strong trend mode

    if htf_bias == 1:
        # PULLBACK mode: RSI dipped near oversold then recovering
        rsi_pullback = ((recent["rsi"] < RSI_OS + 10).any()
                        and row["rsi"] > prev["rsi"]
                        and row["rsi"] < RSI_OB)
        # TREND mode: RSI below 55 and rising (strong bull, ADX > 30)
        rsi_trend    = adx_strong and row["rsi"] < 55 and row["rsi"] > prev["rsi"]
        # MOMENTUM mode: RSI above 55 and rising regardless of ADX
        # β†’ Bullish counterpart: RSI in bullish territory and rising = valid
        rsi_momentum = (row["rsi"] > 55
                        and row["rsi"] > prev["rsi"]
                        and row["rsi"] < RSI_OB)
        rsi_ok       = rsi_pullback or rsi_trend or rsi_momentum
        rsi_mode     = ("trend"    if rsi_trend    and not rsi_pullback
                        else "momentum" if rsi_momentum and not rsi_pullback
                        else "pullback")

    else:
        # PULLBACK mode: RSI spiked near overbought then falling
        rsi_pullback = ((recent["rsi"] > RSI_OB - 10).any()
                        and row["rsi"] < prev["rsi"]
                        and row["rsi"] > RSI_OS)
        # TREND mode: RSI above 50 and falling (strong bear, ADX > 30)
        rsi_trend    = adx_strong and row["rsi"] > 45 and row["rsi"] < prev["rsi"]
        # CONTINUATION mode: RSI deeply oversold + falling (strong bear, ADX > 30)
        rsi_oversold = adx_strong and row["rsi"] < RSI_OS + 5
        # MOMENTUM mode: RSI below 45 and falling regardless of ADX
        # β†’ NZD/USD: RSI 38.3 falling, W1+D1 bearish, EMA crossed β€” valid signal
        # β†’ No spike needed when RSI is already in bearish territory and moving down
        # β†’ D1 ADX must be strong (>30) to compensate for low H1 ADX
        rsi_momentum = (row["rsi"] < 45
                        and row["rsi"] < prev["rsi"]
                        and row["rsi"] > RSI_OS)   # not yet oversold (room to run)
        rsi_ok   = rsi_pullback or rsi_trend or rsi_oversold or rsi_momentum
        rsi_mode = ("oversold_cont" if rsi_oversold and not rsi_pullback
                    else "trend"    if rsi_trend    and not rsi_pullback
                    else "momentum" if rsi_momentum and not rsi_pullback
                    else "pullback")

    triggered = cross_ok and rsi_ok
    return {
        "triggered": triggered,
        "signal":    htf_bias if triggered else 0,
        "cross_ok":  cross_ok,
        "rsi_ok":    rsi_ok,
        "rsi":       round(row["rsi"], 1),
        "zone":      row["price_zone"],
        "rsi_mode":  rsi_mode,
        "adx":       round(row["adx"], 1),
    }




# Compatibility wrappers for newer scorer imports
def analyse_wtf_v2(df):
    return analyse_wtf(df)

def analyse_htf_v2(df):
    return analyse_htf(df)

def analyse_mtf_v2(df, htf_bias, instrument):
    return analyse_mtf(df, htf_bias, instrument)

def analyse_ltf_v2(df, htf_bias):
    return analyse_ltf(df, htf_bias)