- signal_booster.py: _compute_rates() now also computes a per-symbol win
rate (not just system-wide/direction aggregates); trade_executor.py's
Kelly sizing prefers it when the symbol has enough closed-trade history.
Added _as_datetime() to normalize closed_at across backends/drivers
that return either a real datetime or a string from raw SQL.
- trade_executor.py: volatility-filter and Kelly-sizing exception handlers
now log at warning level with the actual exception instead of silently
swallowing failures that affect how much money a trade risks.
- risk_manager.py: compute_partial_tp_levels() now returns all 3 levels
its docstring always promised (TP1 25% + TP2 35% + 40% trailing
remainder) instead of silently dropping the last 40%.
- trade_executor.py: compute_volatility_adjusted_size() was dead code;
now applied as a multiplier on the Kelly-derived trade_size (using
max_risk_pct=100 to reinterpret it as "scale the already-sized trade"
rather than "% of a bankroll", which would always collapse to this
pipeline's $5 floor at its actual dollar scale).
- walk_forward.py: grid-search fallback (when every combo is too sparse
to trust) now picks the combo with the most trades/highest PnL instead
of always the grid's arbitrary first entry. Raised MIN_TRADES_PER_FOLD
5 -> 15 for a more defensible statistical minimum.
219 backend tests pass (+10).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- backtest_engine.py: positions now also exit on ATR/regime-adaptive
STOP_LOSS/TAKE_PROFIT (mirroring signal_service.py's close_stale_trades),
not just REVERSAL/TIME_LIMIT/END_OF_DATA -- backtest/WFO now exercises
the same exit rule live trading actually enforces.
- trade_executor.py: Kelly sizing dampens by 1/sqrt(same_direction_open+1)
to account for correlated risk across simultaneously open positions
(crypto altcoins move together); opposite-direction positions don't
dampen since they net against that risk.
- signal_scoring.py: correlation dampening between the 13 vote algorithms
now uses per-pair weighted coefficients (StochRSI~RSI high, MFI~RSI
moderate, etc.) instead of uniform 1/sqrt(count), so near-duplicate
signals get dampened harder than genuinely complementary ones.
- indicator_service.py: "volatile" regime threshold is now the 90th
percentile of a symbol's own recent ATR% history instead of one fixed
5% cutoff shared by every symbol (BTC vs. a naturally-volatile altcoin).
- trade_executor.py: fixed a phantom-read race in the eviction path where
two concurrent signals for the same user could both pass the
MAX_OPEN_TRADES check before either committed -- now locks the user row
first to serialize per-user trade-opening.
209 backend tests pass (+22).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backtest/walk-forward priced every fill at the exact candle close with zero
cost, making reported win rate/profit factor systematically more optimistic
than live trading. Added configurable taker-fee + slippage simulation
(defaults 0.1%/0.05% per fill) applied to every entry/exit, threaded through
walk-forward's grid search and both API endpoints.
sync_real_trades() hardcoded pnl=0 for every real trade needing it, silently
reporting break-even for real-money trades regardless of actual outcome.
Replaced with FIFO lot matching per (user, symbol, exchange), and fixed
orders.py to persist the exchange's actual average fill price instead of
the (always-None-for-market-orders) requested price, so there's real price
data to match against.
Also verified (and locked in with regression tests) that Divergence/SMC's
pivot-confirmation delay is already causally consistent between live and
backtest — no repaint, no look-ahead leak.
187 backend tests pass (+17).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The market-regime filter (suppress directional signals in choppy/sideways
conditions, downgrade STRONG signals in volatile ones) already existed,
but only as a manual post-classification check inside signal_service.py's
live-trading path. Backtest and walk-forward called signal_scoring.py's
classifier directly, bypassing it entirely — so backtest results
systematically overestimated trade frequency and risk exposure relative
to what live trading actually does.
Factored the filter into a shared _apply_regime_filter() in
signal_scoring.py and added an optional market_regime param to
_classify_signal_combined() (default None preserves existing behavior for
every other caller, e.g. MTF sub-votes). backtest_engine.py now precomputes
adx()/atr() alongside the other rolling-window indicators and computes
detect_market_regime() per candle with the same bounded-window formula
live trading uses (candle_service.py), applying the filter once a
threshold combo resolves a concrete signal type. signal_service.py now
passes its regime into the shared classifier instead of duplicating the
check.
Also found and fixed a second, independent regime system: close_stale_trades()
(SL/TP sizing for open trades) computed its own cruder ATR-percentile-only
regime bucketing, which could disagree with the ADX+BB+Choppiness+
Efficiency-Ratio classification used for entry filtering. It now reads
market_regime from get_indicators() — the same TTL-cached function the
live signal-generation loop already populates — so entry filtering and
exit sizing agree on what "volatile" or "choppy" means for a given symbol.
14 new tests (unit tests for _apply_regime_filter, backtest integration
tests proving the filter suppresses/allows trades by regime, and a
signal_service test confirming close_stale_trades sources its regime from
the shared cache). 170 backend tests passing, frontend build clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
market_structure() (SMC) and detect_divergence() were each precomputed
once over the ENTIRE multi-year backtest range and reused unchanged for
every candle, so every candle's score could see results derived from
years of future price data — a look-ahead bug that inflated both
single-run backtest and walk-forward results, undermining the very
overfitting check walk-forward exists to provide. A prior fix bounded
this to a per-candle trailing window, which closed most of the leak but
still rescanned pivots from scratch on every candle (O(window) per
candle), too slow to enable 15m/30m walk-forward runs.
The real fix: pivot detection is itself a bounded rolling-window scan
(each position only depends on a few bars on either side), so it can be
precomputed once for the whole dataset just like BB/RSI/MACD. Per candle,
_compute_scores_series now just advances a monotonic pointer over
already-known pivots to whatever is causally confirmable as of that
candle — O(1) amortized across the whole run instead of O(window) or
O(n) per candle. Added an optional precomputed_pivots param to
detect_divergence() (backward compatible) to reuse this for RSI/MACD
divergence too.
Net effect: 16,000 candles went from 16.1s to 1.7s (confirmed empirically,
on top of an earlier ~10x from fixing the raw O(n^2)), and scaling stays
linear at 32,000 candles (3.2s). Walk-forward's timeframe options are now
15m/30m/1h/4h/1d (up from 1h/4h/1d) since 15m at the 3-year default
lookback now costs roughly 30s instead of 5+ minutes. Also wired
walk_forward.py's grid search to actually reuse one computed score series
across all 27 parameter combinations per fold (it was recomputing full
classification for every combination despite the scoring/threshold split
added earlier). 156 backend tests passing (3 new: causal-score regression,
pivot-detection-runs-once, order-block-window-bounded).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every displayed timestamp previously relied on either the viewer's
browser-local timezone (toLocaleString/toLocaleDateString/toLocaleTimeString)
or raw UTC ISO-string slicing (.slice(0,10)/.slice(5,16)) — both wrong for
a Vietnam-based system, and the string-slicing approach could show the
wrong calendar date entirely near the UTC/GMT+7 day boundary. Added
frontend/src/utils/dateTime.ts with formatVN* helpers that explicitly
render in Asia/Ho_Chi_Minh regardless of the viewer's machine, and applied
them across AdminPage, OrderPanel's live clock, SignalPanel, ProfilePage,
AuditLogPage, and BacktestPage (including the new walk-forward fold/history
dates) — 10 display sites total.
Also consolidated the timeframe list (15m/30m/1h/4h/1d/1w/1M), which had
drifted into 4 different copies across BacktestPage, ProfilePage,
ChartToolbar, and AlertsPage, into a single frontend/src/utils/timeframes.ts
source of truth. Extended Walk-Forward's timeframe options from 1h/4h to
1h/4h/1d, and fixed a latent backend bug where backtest_engine.py's
tf_minutes map was missing "1d", silently defaulting to 30 minutes for
any daily-timeframe backtest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rolling train/test folds over 3 years of data auto-optimize the three
cheap-to-tune trading parameters (STRONG/BUY score thresholds, max hold
time) via grid search on each fold's train window, then evaluate purely
on the held-out test window. Stitching all out-of-sample results gives
an honest performance estimate uninflated by tuning against the same
data used to score it.
Split signal_scoring.py's expensive 13-algorithm scoring from its cheap
final threshold classification so grid search can replay many parameter
combinations without recomputing indicators each time. Moved the
backtest engine (fetch/precompute/simulate) out of the API layer into
app/services/backtest_engine.py so both /backtest/run and the new
walk-forward optimizer share one implementation instead of drifting
copies — same rationale as the earlier signal_service.py split (item h).
Also merges two long-diverged Alembic migration heads discovered while
adding the walk_forward_results table, so `alembic upgrade head` has a
single target again.
New: POST/GET/DELETE /walk-forward/* endpoints, a Walk-Forward tab on
the Backtest page (fold table, out-of-sample equity curve, run history).
19 new backend tests (153 total, all passing).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend: extract pure scoring logic from signal_service.py into signal_scoring.py (h),
add Redis-backed win-rate/PnL caching with graceful degradation (l), add Postgres
backup/restore scripts (n), move DB/encryption secrets to Docker secrets pattern (o),
fix RSI flat-price bug and MFI wraparound index bug (q, r). 134 backend tests passing.
Frontend: consolidate all API calls onto shared apiFetch with auto token refresh (i),
wire AnalyticsPage to the real /analytics/dashboard endpoint instead of fake random
data (j), migrate all pages and shared components to a Tailwind CSS design system (k)
fixing 3 mismatched color palettes found along the way. UI review also found and fixed
missing mobile table scroll wrappers, non-stacking grids, and a missing nav/logout bar
on ProfilePage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phat hien (p) khi viet test cho trade_executor: khi kiem tra hybrid
eviction, PnL cua TAT CA cac trade dang mo (o nhieu symbol khac nhau) bi
tinh bang current_price cua tin hieu dang xu ly, thay vi gia thuc cua
tung symbol. Fix bang cach lookup gia moi nhat theo tung
symbol/exchange/timeframe (batched query, cung pattern da dung dung trong
close_stale_trades), ap dung cho ca xep hang loser LAN gia dong lenh cuoi
cung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>