Skip to content

Claude/expert trading bot dl5shu - #14

Open
prapatpk01 wants to merge 392 commits into
eliautobot:mainfrom
prapatpk01:claude/expert-trading-bot-dl5shu
Open

Claude/expert trading bot dl5shu#14
prapatpk01 wants to merge 392 commits into
eliautobot:mainfrom
prapatpk01:claude/expert-trading-bot-dl5shu

Conversation

@prapatpk01

Copy link
Copy Markdown

No description provided.

claude added 30 commits July 10, 2026 08:38
… on close

Root cause of the repeated false "Max Drawdown Reached" halts: the
paper futures simulation deducted margin straight from the USDT total
on open and never added it back correctly (margin returned on close
was recomputed at the exit price instead of the original entry
margin), and never realized trade P&L at all. fetch_balance()'s
"total" therefore dropped by the full margin amount the instant a
position opened — a $10k account opening a position needing $2k
margin looked like a 20% "loss" before the trade had even moved,
tripping check_drawdown() with nothing actually lost.

Now the paper connector tracks open positions (entry price, amount,
margin) per symbol+direction:
  - Opening a position moves margin from free -> used; total (free+used)
    is unchanged, exactly like a real exchange wallet.
  - Closing realizes P&L = (exit - entry) * amount (sign-flipped for
    shorts), credits back the ORIGINAL margin + P&L, and releases the
    used-margin lock. Supports partial closes (TP1 50%) by prorating
    the released margin.
  - fetch_balance() reports total = free + used per asset.

Verified with an open-then-close paper cycle: total balance stays at
$10,000 while margin is locked mid-trade (previously showed as a
"loss"), and correctly lands at $10,332 after a +$332 realized gain.

Also fixed direction inference for non-hedge-mode closes (WT/MCDX
strategies): pos_side=None always means "long" since non-hedge mode
never shorts — previously inferred from order side, which mislabeled
"sell to close long" as opening a short.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScBxsAhBoQbaLHf4xBnKjW
…start drawdown halt for this case

RiskManager now tracks a consecutive-losing-close streak: any close
with pnl < 0 extends it, any close with pnl >= 0 (win, partial TP,
BE-scratch) resets it. Hitting max_consecutive_sl (default 3) starts
a cooldown_hours (default 4h) window during which can_open() blocks
new entries — existing open positions keep being managed/closed
normally, only new entries are paused. The cooldown clears itself
automatically; no /start_bot needed (unlike the % max-drawdown halt,
which still exists separately as an emergency stop requiring manual
resume).

Wired into every position-close site that computes pnl: the fallback
hard SL/TP check in _tick(), and the Layer-7 PositionManager's
partial_tp and close actions. Added notify_cooldown_halt() to
Telegram so the trigger is visible immediately.

Configurable via MAX_CONSECUTIVE_SL and COOLDOWN_HOURS.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScBxsAhBoQbaLHf4xBnKjW
Railway logs showed the bot opening a BTC long on literally the first
scan after "Trading Bot Started" — before it had a chance to observe
live conditions across more than one cycle post-restart.

Add a warm-up counter (default 1 tick = one 5-minute scan): while
active, BUY/SELL signals are logged but not executed via
_maybe_notify, so no new positions open. Existing open positions (if
any survive a restart) are unaffected — only new entries are paused.
Counts down once per tick regardless of how many symbols/strategies
ran, so with the default WARMUP_TICKS=1 the very first scan after
start is observation-only and entries are allowed from the second
scan onward.

Configurable via WARMUP_TICKS.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScBxsAhBoQbaLHf4xBnKjW
Replaces the ad-hoc 9-layer pipeline with a modular, event-driven,
context-aware architecture that selects a genuinely different strategy
per market regime instead of one blended indicator score for every
condition.

New engines (Layer 0-8):
  Layer 0  market_quality_engine.py  — hard gate: liquidity/noise/ATR/
           session/volume/tick-stability -> 0-100, vetoes untradeable
           markets before any directional analysis runs.
  Layer 1  macro_trend_engine.py     — thresholds updated to spec
           (90/70/45/20), adds allowed_direction() so macro is purely
           a direction gate and never picks entries.
  Layer 2  context_bias_engine.py    — unchanged (already score-based,
           no hard gate).
  Layer 3  regime_classifier.py      — NEW. Scores every candidate
           Primary Regime independently (Bull/Bear Trend, Range,
           Compression, Breakout, Reversal, Exhaustion, Transition)
           via EMA/ADX/ATR-percentile/Bollinger-width/structure/
           divergence/BOS/liquidity-sweep, winner-takes-all. A
           Secondary State (Low/Normal/High Volatility, Expansion)
           travels alongside it independently.
  Layer 4  regime_strategy_selector.py — rewritten to consume the new
           Primary Regime and map each one to exactly ONE strategy
           (Trend->TrendContinuation, Range->MeanReversion,
           Breakout->Breakout, Reversal->SwingReversal,
           Compression->BreakoutPrep [no entries], Exhaustion->
           ProfitProtection [manage-only], Transition->NoTrade;
           Momentum Expansion overrides trend-continuation when
           Secondary State is Expansion).
  Layer 5  strategy_engine.py        — NEW. Five concrete strategies,
           each with its own named-indicator entry/SL/TP/invalidation
           logic (no blended generic scoring): TrendContinuation (EMA
           pullback + HMA slope + ADX + RSI + volume), MeanReversion
           (RSI extreme + Bollinger + VWAP distance + S/R), Breakout
           (compression + ATR/volume expansion + BOS), SwingReversal
           (RSI divergence + CHOCH proxy + liquidity sweep +
           engulfing), MomentumExpansion (ROC + ATR expansion +
           volume + EMA slope).
  Layer 6  confidence_engine.py      — NEW. Trade-quality hard gate:
           blends the winning strategy's setup score with market/risk
           alignment and expert quality sub-scores into 0-100;
           <75 skip, 75-84 good, 85+ high confidence.
  Layer 7  expectancy_engine.py      — NEW. Trade-quality hard gate:
           rolling win rate/profit factor/avg R/Kelly fraction/
           Monte-Carlo stability per (regime, strategy) from the
           learning journal; skips even high-confidence setups with a
           historically negative edge. Passes through neutrally until
           20+ trades exist for that combination.
  Layer 8  dynamic_risk_engine.py    — NEW. Outputs a risk_multiplier
           (quality x confidence x expectancy x correlation x
           volatility-state) applied on top of the account's base
           risk-per-trade %, plus regime-aware TP1/TP2/trail
           parameters (trending regimes trail instead of a hard TP2;
           range/reversal take profit faster).

indicators.py: shared numpy toolkit (EMA/HMA/RSI/MACD/ADX/ATR/
Bollinger/ROC/swing-points/engulfing) used by Layers 0/3/5 so the new
code isn't duplicating indicator math per-file.

position_manager.py: register_position() now accepts optional
tp1_rr/tp2_rr overrides so Layer 8's per-trade plan actually takes
effect instead of a fixed constant.

bot.py: position sizing now applies Layer 8's risk_multiplier on top
of RISK_PER_TRADE_PCT; confidence-tier fallback sizing maps the new
Layer 6 level names (skip/good/high_confidence) to the legacy
WEAK/GOOD/HIGH_CONVICTION tiers.

ai_expert_strategy.py: analyze() now orchestrates the full Layer 0-8
flow end-to-end. DecisionEngine/EntryTimingEngine/MarketIntelligenceEngine
are no longer part of the active pipeline (kept in the repo, unused,
for backward compatibility) — ExpertAnalysisEngine is retained as a
supporting scorer feeding Layer 6's sub-scores, and PositionManager/
ExitEngine/AdaptiveLearningEngine/FeatureStore/ModelRegistry/
DriftDetector are unchanged downstream stages.

Verified: all 8 layers unit-tested individually and as a full pipeline
against both synthetic data and real BTC historical candles — regime
classification, strategy selection, and confidence gating all produce
sane, regime-appropriate output (e.g. breakout regime -> breakout
strategy, bull trend + expansion -> momentum expansion), with
confidence correctly gating below-threshold setups to HOLD.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScBxsAhBoQbaLHf4xBnKjW
New standalone strategy, separate from the AI Decision Engine pipeline:

  Entry LONG (all three required):
    - EMA12 crosses above EMA26 on 15m
    - Price closes above SMA50 on 15m (trend filter)
    - MACD(12,26,9) on 30m: line > signal AND histogram > 0

  Entry SHORT: mirrored (cross down, below SMA50, MACD 30m bearish)

  Exit LONG:  EMA12 crosses back below EMA26
              OR (MACD 30m turns bearish AND price pulls back to touch SMA50)
  Exit SHORT: mirrored

  TP/SL: 1:1 R:R, distance = ATR(14) on 15m (not specified in the
  request beyond the 1:1 ratio; ATR-based matches this repo's existing
  convention — tune via atr_mult/rr_ratio params if a different
  distance is wanted).

30m candles aren't fetched separately — resampled directly from the
15m series the strategy already receives, so no changes needed to the
bot's data-fetching loop. Select it via STRATEGY=ema_macd in Railway
Variables; production default (ai_expert) is unchanged.

Verified with synthetic reversal data: entry fires exactly on the EMA
cross + SMA50 + MACD alignment, and the later trend reversal correctly
triggers the EMA-cross exit.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScBxsAhBoQbaLHf4xBnKjW
…king bugs found along the way

New strategy (STRATEGY=hma_macd_roc):
  TF30M HMA10/20 cross gate -> 3-bar confirmation window requiring
  MACD(12,26,9) cross + ROC(9) sign agreement, all evaluated only on
  closed 30m bars (resampled from the 15m series, dropping any
  still-forming last bucket). Entry = AND logic, exit = OR logic on
  the same three signals reversing. SL = ATR(14,30m) x1.5, TP = same
  distance (1:1). Implemented as an explicit state machine
  (WAITING/LONG_CONFIRMATION/SHORT_CONFIRMATION) matching the spec's
  pseudocode exactly, including sticky MACD confirmation vs.
  re-evaluated-every-bar ROC confirmation, and opposite-gate/HMA-flip
  setup invalidation.

Two real bugs found and fixed while building this (both affect the
live bot under HEDGE_MODE=true, the production default):

1. _strategy_map lookups in bot.py used the raw pos_info["strategy"]
   key, which carries a ":L"/":S" hedge-mode suffix (e.g.
   "AIExpert(BTC/USDT:USDT):L") that _strategy_map's keys (plain
   strategy.name) never had. tick_open_position() and
   record_closed_trade() therefore silently never ran in hedge mode —
   AIExpertStrategy's partial-TP/break-even/Layer-8 position
   management and the learning journal have been dead code in
   production. Added _resolve_strategy_inst() to strip the suffix on
   fallback lookup; fixed all three call sites.

2. ema_macd_strategy.py returned a SELL/BUY Signal to represent
   "close this position" — but in hedge mode a SELL Signal always
   OPENS a new short (independent of any existing long), so its exit
   path would have opened the wrong position instead of closing the
   right one. Moved exit logic to tick_open_position() (now that fix
   #1 makes it actually run), which always closes whichever position
   is genuinely open regardless of hedge mode. Also fixed its 30m
   MACD resample to drop an unclosed trailing bucket, and its
   position_manager import path (was "..strategies.position_manager",
   should be "..engines.position_manager" — also present in the new
   strategy, both fixed).

bot.py: added opt-in margin-based position sizing (signal.metadata
sizing_mode="margin", margin_pct) alongside the existing risk-based
default — margin = balance x margin_pct, notional = margin x
leverage, verified against the spec's worked example (Balance=1000,
5% -> Margin=50, 20x -> Notional=1000). Risk-based sizing (used by
ai_expert/ema_macd) is unaffected.

Verified end-to-end: entry fires with correct 1:1 SL/TP and margin
sizing metadata, exit fires exactly once per position (a repeated-exit
bug from _reset() not clearing _open_position was caught and fixed
during testing), and a fresh entry can open again afterward.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScBxsAhBoQbaLHf4xBnKjW
User reported the strategy entered 1-2 bars late relative to the HMA
cross point (traced on real SOL data: gate at bar 187, entry not
confirmed until bar 189). That delay was the multi-bar confirmation
window requiring a fresh MACD cross after the HMA gate fired.

Replaced the WAITING/LONG_CONFIRMATION/SHORT_CONFIRMATION state
machine with a single-bar AND-gate: the moment a 30m bar closes with
HMA10/HMA20 crossing AND MACD already on the matching side of its
signal line AND ROC(9) agreeing in sign, enter immediately. MACD/ROC
now need to already agree, not freshly cross, on the same bar as the
HMA gate — verified this fires with zero lag on the same SOL data
(idx=173 short entry, same bar as the HMA cross).

Exit logic (OR across HMA/MACD/ROC reversal, evaluated via
tick_open_position) is unchanged.
Comparison strategy against ema_macd, per user request — same EMA/SMA50
core (EMA12 cross EMA26, candle opens above/below SMA50) but with the
30m MACD confirmation layer removed entirely; everything evaluated on
15m only. Exit on EMA12 crossing back OR price closing on the wrong
side of SMA50. 1:1 R:R via ATR(14) distance.

Exit path lives in tick_open_position() (not a SELL/BUY Signal from
analyze()) for the same hedge-mode-safety reason as ema_macd and
hma_macd_roc: a SELL signal in hedge mode always opens a new short
rather than closing an existing long.

Wired into run_bot.py via STRATEGY=ema_sma.
User flagged the SL/TP lines as too tight on the example charts —
BTC's stop was only ~0.135% of price (ATR x1.0 on a single 15m bar is
naturally small). Widened the default atr_mult from 1.0 to 2.0; R:R
stays 1:1.
User asked for ema_sma to use the same TP/SL as ai_expert instead of
its own flat ATRx2 1:1. Replaced entry SL/TP with ai_expert's Layer 5
formula (structure-based swing SL + 0.3xATR buffer, widened by ATRx1.5,
capped at 3xATR, 1.2R target) — same as entry_timing.py's _compute_sl_tp
minus the regime multiplier scaling (no regime classifier here, so it
always uses the TREND-regime default of 1.5x).

Wired in the same engines.position_manager.PositionManager ai_expert
uses for post-entry management: TP1 @ 0.6R closes 50% + moves SL to
break-even, TP2 @ 1.2R closes the rest (TP1_RR/TP2_RR env vars, same
defaults). The strategy's own EMA-reversal / SMA50-close exit still
runs as a discretionary early-close check after PositionManager passes
on a given tick, unchanged from before.

Verified against real BTC/XAU 15m data: TP1 partial-close + BE and TP2
full-close both fire correctly alongside the existing signal-based
exit.
Clarified: closing should stay driven by EMA cross / SMA50 (as the
strategy always did), not ai_expert's TP1/TP2 R:R-based partial-close
mechanics from the previous commit. Removed the PositionManager
wiring from tick_open_position() entirely — it's back to pure
EMA-reversal / SMA50-close signal exit.

Kept ai_expert's SL/TP price-level formula (structure-based swing SL
+ ATRx1.5 buffer, capped 3xATR, 1.2R target) from the previous commit,
since that request stands — those levels now only serve as the
hard-stop safety net checked by bot.py's risk-manager fallback, not
as an active close trigger.

Verified against real BTC 15m data: entries and closes now only ever
fire on EMA cross / SMA50 close, no partial-TP events.
… order-failed reports

Code review (8-angle) surfaced two confirmed bugs in ema_sma/ema_macd/
hma_macd_roc that would silently disable a strategy on any of its
first few trades:

1. Each strategy latches an internal "position open" flag the instant
   analyze() emits an entry signal, before bot.py has confirmed the
   order actually executed. Nothing ever cleared it if execution was
   later rejected (MAX_POSITIONS reached, low balance, portfolio-heat
   gate, size rounds to 0, order exception) — a single rejected signal
   permanently stuck the strategy on HOLD for that symbol.

2. The same flag was never reset when a position closed via the
   risk-manager's hard SL/TP fallback (bot.py's _tick loop) instead of
   the strategy's own tick_open_position() — a routine occurrence any
   time price gaps through the stop before the EMA/SMA/HMA/MACD/ROC
   exit condition fires. _on_position_closed only resets state through
   record_closed_trade(), which none of the three strategies
   implemented.

Fixes:
  - Added record_closed_trade() to all three strategies, resetting
    their open-position flag on ANY close path (bot.py's
    _on_position_closed already calls this generically via hasattr).
  - Added cancel_pending_entry() to all three strategies, and wired
    bot.py._cancel_pending_entry() into every rejection point between
    analyze() and a confirmed order (can_open false for both long and
    short/hedge paths, low balance, zero-sized position, portfolio
    gate, order exception).

Also fixed a related bug the review surfaced in the same code path:
_execute_signal wrapped the Telegram notify_order() call in the same
try/except as the actual order placement, so a Telegram-side failure
right after a successful order was misreported as "Order failed" and
prematurely unlocked the strategy while a real position was already
open. Moved the notify call outside the order try/except with its own
error handling, so a notify failure can never be confused with an
order failure again.

Verified: all three strategies import cleanly, record_closed_trade()
and cancel_pending_entry() both reset internal state correctly.
…ry (30m)

New strategy per spec — TF30m, no counter-trend trades allowed:

Trend gate (both must agree, or no trade at all):
  - SMA30: candle opens above -> uptrend, opens below -> downtrend
  - MACD(12,26,9): line>signal and hist>0 -> uptrend, line<signal and
    hist<0 -> downtrend

Entry (only in the confirmed direction):
  - EMA5 crosses EMA10 the matching way
  - price within 1.5xATR of SMA30 — beyond that the setup fails outright
    (no chase). Since cross_up/cross_down are one-shot transition events,
    a failed setup naturally waits for price to pull back AND a fresh
    EMA5/10 cross before re-evaluating, with no extra state needed.

Exit = EMA5/10 cross reversal, via tick_open_position() (hedge-mode-safe,
same pattern as the other new strategies — a SELL Signal in hedge mode
always opens a short, never closes a long). SL/TP = ATR(14,30m)x1.5, 1:1
R:R, kept only as the hard-stop safety net.

Implements record_closed_trade()/cancel_pending_entry() from the start
(the two hooks added to the other three new strategies in the previous
commit to fix the permanent-lockout bugs the review surfaced), so this
one launches without that class of bug.

Verified against real BTC/XAU 15m data: trend-confirmed entries, EMA
cross exits, and the "too far from SMA30 -> fail, wait for pullback +
fresh cross" case all fire correctly.

Wired into run_bot.py via STRATEGY=trend_confirm.
… trend

Fixes the exact case flagged from a live chart: after a long closed via
EMA5/10 crossing down, price kept falling and SMA30+MACD confirmed a
downtrend a couple bars later — but no short ever fired, because the
downward cross had already happened (and expired, being a one-shot
transition) before the trend confirmed. That's correct per spec (a
stale cross from before confirmation must never count), but the
strategy had no explicit trend-state tracking to guarantee it, and had
no rule against re-entering repeatedly within the same still-confirmed
trend after a pullback exit.

Added:
  - self._trend_state ("up"/"down"/None), updated every 30m bar from
    the SMA30+MACD gate. Any change — a flip, or a conflict clearing
    back to confirmation — is a fresh trend-confirmation event.
  - self._traded_this_trend, reset to False on every trend-confirmation
    event, set True the moment a trade actually opens. While True, no
    further entries are evaluated for that trend streak, even after the
    position closes and price pulls back for a fresh EMA5/10 cross —
    "close and wait for the next trend confirmation," not "close and
    look for the next cross."

Verified against real BTC/XAU 30m data: a genuine trend flip (down ->
up -> down) correctly resets the gate and allows a second short; the
original flagged case (stale cross before confirmation) still
correctly produces no entry.
Per request: if the EMA5/10 cross that would trigger an entry happened
up to 3 bars BEFORE the trend actually confirmed, still take it once
the trend confirms — don't require the cross and the confirmation to
land on the exact same bar. This is exactly the case a live chart
flagged: the cross that closes one position (e.g. EMA5 crossing below
EMA10 while exiting a long) is very often the same cross that, once
SMA30+MACD catch up and confirm the new trend a bar or two later,
should also be the entry trigger for the opposite direction.

Added self._last_cross_up_ts/_last_cross_down_ts, updated every 30m
bar regardless of trend state. On entry, a cross within
cross_grace_bars (default 3) of the confirming bar now counts, not
just a same-bar cross.

While implementing this, found and fixed the actual reason the
original case never worked: analyze() returned early whenever a
position was open, before ever computing indicators — so cross events
that happened WHILE HOLDING (like the very cross that triggers the
exit) were never recorded at all, leaving the grace window with
nothing to look back on. Restructured so indicator/cross/trend
tracking always runs on a new 30m bar regardless of open-position
state, and gave tick_open_position() its own independent bar tracker
(self._last_exit_bar_ts) instead of sharing analyze()'s, since the two
now both need to track "new bar" independently.

Verified against real BTC 30m data: the exact flagged case (long exits
via EMA cross at bar 137, downtrend confirms at bar 139) now correctly
enters short at bar 139, citing "1 bar(s) before trend confirmed."
… zero-line read

Per a live chart showing a cluster of EMA5/10 crosses all within a
single still-uptrending swing — the pure cross-based exit was closing
(and immediately re-evaluating) on every one of those, churning
through a move that never actually left the SMA30 side it entered on.

Exit LONG:  candle opens below SMA30 (30m)  [was: EMA5 crosses below EMA10]
Exit SHORT: candle opens above SMA30 (30m)  [was: EMA5 crosses above EMA10]
Entry still uses the EMA5/10 cross (unchanged) — only the exit trigger
moved to SMA30, so a position now rides out chop and only closes once
price genuinely opens on the wrong side of the trend line.

Also switched the MACD half of the trend gate to match the user-
supplied Pine "MACD 4C" script: trend is now read from the raw MACD
LINE's sign (>0 uptrend, <0 downtrend), not macd_line vs signal_line +
histogram as before.

Verified against real BTC/XAU 30m data: trades now hold through the
kind of chop shown in the flagged chart (e.g. BTC entry at bar 139
now holds until bar 335, vs. exiting within a few bars previously) and
only close on an actual SMA30 break.
…-trend lock

Two changes per request:

1. Exit is now OR logic instead of SMA30-only: close on EITHER a candle
   opening on the wrong side of SMA30 OR an EMA5/10 cross reversal,
   whichever fires first (still underneath the ATR-based hard SL/TP
   stop as the ultimate safety net).

2. Removed self._traded_this_trend — previously a trend streak could
   only produce one trade total, even after closing early. Now: after
   ANY close, the very next bar where the trend gate reads confirmed
   (same direction or a fresh flip) is immediately eligible for a new
   entry via the normal EMA5/10 cross + distance check. "Close,
   reconfirm the trend, find the next entry" — no artificial cooldown.

Verified against real BTC/XAU 30m data: both exit reasons (SMA30 break
and EMA cross) now appear in the trade log, and re-entries follow
immediately within the same still-confirmed trend rather than waiting
for a trend flip.
…y gate

Four changes to the live Layer 1-6 pipeline per request:

1. Trend stage (early/mid/late), not just direction — added to both
   MacroTrendResult (4H, Layer 1) and ContextBiasResult (1H, Layer 2).
   4H: ADX level/trajectory + price extension from EMA20 in ATR units.
   1H: RSI position relative to the trending zone (55-75 bull /
   25-45 bear = mid; beyond = late; short of = early). "n/a" when the
   bias itself is neutral — stage only applies to an actual trend.

2. Weighted 15m/1h/4h combined direction score, via the existing
   BaseStrategy.compute_mtf_bias() (weights 1/2/3, higher TF weighted
   more) — now actually wired into the pipeline as
   metadata["mtf_combined"] (pct, label, aligned_1h_4h). Used in
   RegimeStrategySelector to break the direction tie when 4H macro
   alone reads neutral ("both") and the regime has no lean of its own:
   |mtf_pct| >= 15 now picks long_only/short_only instead of leaving
   direction fully unconstrained.

3. [SCAN] log line (bot.py) now shows 4H bias/stage, 1H bias/stage,
   an aligned ✓/✗ flag, and the combined mtf% — previously only 4H
   bias/score was visible, 1H context was computed but never surfaced
   anywhere.

4. Loosened Layer 6's real trade-quality gate (ConfidenceEngine — the
   entry_timing.py/decision_engine.py checklist described in an old
   docstring is dead code, never imported by ai_expert_strategy.py,
   left untouched). skip_threshold 75->45, high_threshold 85->65
   (env-overridable: AI_EXPERT_CONFIDENCE_SKIP/_HIGH), matching
   "3-4 of ~7 weighted components confirming" instead of requiring
   most of them.

Verified against real BTC/XAU data: stage classification produces all
three values (early/mid/late) across real bull/bear swings, mtf_combined
correctly computed and visible in metadata, and entries now pass at
confidence scores (55-71) that the old 75 threshold would have skipped
— no crashes across ~2 full backtests.
Per request: a shared, timeframe-agnostic trend direction/stage engine
using exactly 4 checks (down from the more elaborate per-engine logic
macro_trend_engine.py/context_bias_engine.py each grew independently):

  1. EMA20 vs EMA50 alignment       — raw direction
  2. ADX level + 5-bar trajectory   — trend strength, rising or fading
  3. RSI(14) position                — overbought/oversold extremity
  4. EMA20 slope over the last 4 bars — is the move still actively
     happening right now, or has it stalled? A stale EMA20/50 order
     from an already-stopped move no longer counts as bull/bear.

Stage: early (ADX<20) / mid / late (RSI extreme, or ADX high but no
longer rising) / n/a (neutral bias). Not yet wired into ai_expert or
any strategy — standalone for now, visualized on real 1H BTC/XAU/SOL
data (bull=green/bear=red/neutral=gray, shade intensity = early->late).
…check

Per feedback that direction still wasn't accurate: a single bar's read
was enough to flip a check's vote, so one noisy wick or RSI blip could
flip the whole trend label.

  - EMA fast/slow default changed from 20/50 to 12/26.
  - Every one of the 4 checks (EMA align, ADX +DI/-DI dominance, RSI
    zone, EMA12 bar-to-bar slope direction) is now evaluated at each
    of the last 3 bars individually, and only "votes" for a direction
    if it read the SAME way on all 3 — a flickering check contributes
    nothing that bar instead of forcing a guess.
  - Bias is only bull/bear when at least 3 of the 4 checks confirmed
    (3-bar-consistent) AND all confirmed checks agree with each other;
    otherwise neutral. Stricter, but the label only moves once multiple
    independent signals agree and hold — matches the accuracy
    complaint at the cost of a few more "neutral" bars during chop.
  - Stage (early/mid/late) extremity checks (ADX<20, RSI>=70/<=25,
    ADX>=35-and-flat) also now require holding across the same 3 bars,
    not just the current one.

Verified against real BTC/XAU/SOL 1H data and re-rendered the 3 example
charts: choppy stretches now correctly read "neutral" instead of
flipping bull/bear every bar.
…votes

Per request: replace the AND-vote/persistence-gate design with a
continuous weighted score, symmetric bands as specified:

  76-100 strong_bull   45-55 sideway   0-24  strong_bear
  66-75  bull                          25-34 bear
  56-65  early_bull                    35-44 early_bear

Each of the same 4 checks now produces its own continuous 0-100
sub-score (50 = neutral) instead of a binary vote:
  EMA12/26 spread   30% weight — % spread, tanh-scaled
  ADX + DI direction 25% weight — strength (capped 50) signed by +DI/-DI
  RSI(14)            25% weight — used directly, already 0-100/50-center
  EMA12 bar-slope    20% weight — bar-to-bar % change, tanh-scaled

The composite is computed at each of the last 3 bars and averaged —
this is where the "check backwards 3 bars" requirement from the
previous iteration carries over, now as smoothing instead of a hard
AND-gate: a single noisy bar gets diluted into a 3-bar average rather
than either fully confirming or fully vetoing the read.

Verified against real BTC/XAU/SOL 1H data: full band range appears
across all three symbols (including the rare strong_bull/strong_bear
extremes), score tracks price swings smoothly bar-to-bar without the
discrete-label jumpiness of the previous version. Re-rendered the 3
example charts with a trend-score panel under the candles.
…F lag)

User flagged the score wasn't catching BTC's trend fast enough,
especially on 15m/30m. Root cause: the EMA spread/slope checks were
scaled by raw %-of-price with fixed sensitivity constants tuned
loosely around 1H behavior — the same real move is a much smaller %
on a 15m bar than a 1H bar, so on lower timeframes the score barely
left "sideway" even during a genuine trend.

  - Both EMA checks now normalize by ATR instead of price: spread =
    (ema12-ema26)/ATR, slope = (ema12[t]-ema12[t-1])/ATR. This keeps
    the score equally responsive whether it's fed 15m, 30m, 1H, or 4H
    candles, since it reacts to the move's size relative to the
    instrument's own current volatility rather than a fixed %.
  - Replaced the flat 3-bar average with a recency-weighted blend
    (50/30/20% from most to least recent) — still dilutes single-bar
    noise but no longer lags a full bar-count behind a real move.

Verified against real BTC 15m/30m/1H: score range widened from ~23-64
to ~10-87, strong_bull/strong_bear bands now appear regularly instead
of being nearly unreachable, and the 15m chart's score now correctly
drops into strong_bear during the observed crash instead of stalling
around 30. Re-rendered the 15m/30m BTC charts to confirm visually.
Found the actual weighting bug behind the "TF30m looks off" feedback:
the ADX sub-score used sign(+DI - -DI) as a hard +1/-1 multiplier, so
the instant +DI and -DI crossed by even a hair, the entire ±adx_val
contribution snapped to the opposite side — up to a ~50pt swing in
that sub-score, ~12+pts in the composite at 25% weight, in a single
bar, before often reverting the very next bar. Traced on real BTC 30m
data: bar 1491's adx sub-score jumped from ~25 to 73.5 then back to
~30 within 3 bars while ema/rsi/slope moved smoothly the whole time.

Replaced the hard sign() with tanh(0.08 * (pdi-mdi)) — a near-tie
+DI/-DI now contributes near-neutral instead of snapping fully to one
side, while ADX magnitude still scales confidence smoothly. Direction
and strength blend continuously instead of direction being a coin-flip
discontinuity riding on top of a magnitude.

Verified on real BTC 30m data: the same window that previously spiked
73.5->30 now reads a smooth 45->53->51->48 climb-and-settle. Score
range and band distribution otherwise unchanged (~9-85, all 7 bands
represented). Re-rendered the BTC 30m chart to confirm visually.
Per request — category-weighted instead of 4 flat-weighted checks,
5 total sub-checks (1-2 per category):

  Trend    50%: EMA12/26 spread (25%) + ADX/DI direction (25%)
  Momentum 30%: RSI(14) (15%) + EMA12 bar-slope (15%)
  Volume   20%: volume vs its own 20-bar average, signed by that bar's
                close-vs-open (new) — a high-volume bar confirms
                whichever way it closed; an average-or-below-volume bar
                contributes near nothing regardless of direction.

The 4 existing checks (ema/adx/rsi/slope) are unchanged internally,
just regrouped and reweighted into the 3 categories; only the volume
check is new. Same 3-bar recency-weighted blend (50/30/20) and ATR
normalization as before.

Verified against real BTC 15m/30m data: score range and band coverage
unchanged (~11-87, all 7 bands), volume sub-score swings independently
per-bar as intended (e.g. high volume down-bar -> near 0, high volume
up-bar -> near 100, low volume either way -> near 50), category
breakdown now included in TrendResult.detail for visibility.
Re-rendered the BTC 15m/30m example charts.
…ness

Per feedback that the score was too jumpy/unstable. Added 2 checks
that both move more gradually than the existing 5, diluting fast-check
noise by construction:

  Trend    +Price vs SMA50 (ATR-normalized) — a much slower anchor
           than EMA12/26 spread or ADX, which both react within a
           handful of bars.
  Momentum +MACD(12,26,9) histogram / ATR — smoother than raw RSI or
           EMA12 slope since it's already a smoothed difference-of-EMAs.

Category weights stay Trend 50% / Momentum 30% / Volume 20% as before;
each category's weight is now split evenly across 3 checks instead of
2 (16.7% each for trend, 10% each for momentum), volume unchanged at
20%. 7 checks total.

Verified against real BTC 15m/30m data: score still covers the full
band range (~12-86 on 30m) but changes more gradually bar-to-bar.
Re-rendered the BTC 15m/30m example charts.
Full rebuild per new spec — replaces the 7-check Trend/Momentum/Volume
category system with a simpler, differently-weighted 4-check entry
score plus an explicit pass/fail gate:

  EMA12/26 spread   30pts — (ema12-ema26)/ATR, tanh-scaled direction
  MACD(12,26) hist  30pts — histogram/ATR, tanh-scaled direction, with
                            a DYNAMIC ATR-based penalty: once the
                            histogram is stretched beyond ~1xATR from
                            zero, up to 60% of its points get deducted
                            the further it goes — an already-extended
                            MACD signal is chasing a move that mostly
                            happened, not confirming a fresh one.
  ROC(9)            20pts — 9-bar rate of change, tanh-scaled
  ADX(14) rising    20pts — ADX has no direction of its own (pure
                            strength gauge), so it doesn't vote
                            independently — it AMPLIFIES whichever
                            direction the other 3 checks already lean,
                            scaled by how much ADX rose over the last
                            3 bars. Falling ADX contributes nothing.

Entry gate: score > 70 -> long entry ready, score < 30 -> short entry
ready (TrendResult.entry_ready), matching the requested ">70 pass to
entry" threshold, mirrored for shorts.

Same 3-bar recency-weighted blend (50/30/20) as before. Had to recalibrate
roc_sensitivity from an initial guess of 4.0 to 50.0 after checking real
ROC(9) magnitude on BTC 30m data (~0.75% mean abs, up to ~5.5%) — the
original constant left ROC almost never moving off 50.

Verified against real BTC/XAU 30m data: score spans the full 0-100
range, entry_ready fires correctly at both thresholds, sub-score
breakdown visible per check. Rendered new example charts marking
score>70/<30 entry points directly on the candles.
… %B)

Per request to balance the score with a few more logics. Rescaled the
original 4 checks to make room, added 4 new ones — 8 total, still 100
points:

  EMA12/26 spread    20pts (was 30)
  MACD hist          20pts (was 30, same dynamic ATR extension penalty)
  ROC(9)             15pts (was 20)
  ADX(14) rising     15pts (was 20, still directionless/amplifying-only)
  Volume vs 20-bar avg 10pts (new) — signed by that bar's close-vs-open
  Price vs SMA50       10pts (new) — slow anchor, damps fast-check noise
  HH/HL swing structure 5pts (new) — over a 20-bar lookback, ATR-normalized
  Bollinger %B          5pts (new) — small mean-reversion counterweight,
                        the only check that pulls toward neutral at
                        volatility extremes instead of chasing them

Entry gate unchanged: score > 70 -> long, < 30 -> short.

Found and fixed a real bug while adding the structure/Bollinger
checks: slicing with a negative *stop* bound (e.g. `i+1` when i=-1
resolves to stop=0) silently returns an empty array instead of "up to
and including bar i" — converted to an absolute positive index before
slicing for both new checks.

Verified against real BTC/XAU 30m data: all 8 sub-scores populate
correctly (no more silently-empty windows), full 0-100 range still
reachable, entry_ready still fires at both thresholds. Re-rendered the
BTC/XAU 30m example charts.
Decision: use trend_confirm as-is for now, keep ai_expert as-is. This
engine was a standalone exploration (direction+stage classifier, then
reworked several times into a weighted 0-100 composite score with an
entry-pass gate) built and iterated on purely via example charts —
never imported by ai_expert_strategy.py, bot.py, or any strategy file.
Confirmed no repo code references it before removing.
…n scan logs

Previously the [SCAN] log line reused ai_expert's format (macro/context/mtf
fields), which don't exist on TrendConfirmStrategy's metadata — every field
just printed "?". Now TrendConfirmStrategy attaches its own trend_confirm
metadata (sma_trend, macd_trend, confirmed side, entry_status) on every
analyze() return, and bot.py's _log_scan routes to a dedicated formatter
showing SMA/MACD reads, confirmed long/short, and which entry check
(EMA cross / distance-to-SMA30) passed or is still waiting.
Previously every restart wiped RiskManager._positions, PortfolioEngine
state, and each strategy's _open_position/_open_entry — pure in-memory
state with no persistence. If a position was still genuinely open on
the exchange when the bot restarted, the bot had no way to know: no
hard SL/TP fallback, no portfolio heat accounting, and the strategy
would try to open a duplicate on its next signal instead of managing
the existing one.

Adds BinanceConnector.fetch_positions() (ccxt fetch_positions, paper
mode returns [] since there's no exchange-side state to reconcile
against) and TradingBot._reconcile_positions(), called once before the
first tick of every (re)start. For each live position found, registers
it in RiskManager (default percentage-based SL/TP, since the original
levels were never persisted) and PortfolioEngine, and calls a new
attach_existing_position() hook on the owning strategy so
tick_open_position()/record_closed_trade() resume managing it instead
of treating it as untracked. Implemented for trend_confirm, ai_expert,
ema_sma, ema_macd, and hma_macd_roc. Sends a Telegram notification
(notify_reconciled_position) when a position is recovered this way.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants