Claude/expert trading bot dl5shu - #14
Open
prapatpk01 wants to merge 392 commits into
Open
Conversation
… 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.
…ic reversal exits
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.