Commit graph

371 commits

Author SHA1 Message Date
AnimateDread
3855d4666a diag: the heartbeat could be outrun by the condition it watched for
It fired only on 4096-item boundaries once an era had already run 60s. Those
boundaries are all crossed in the first few chunks of pass 1, so an era that
became slow AFTER them printed nothing at all - which is precisely what
happened: 20 minutes, four pegged cores, zero heartbeats. I read that silence
as "the era loop is never reached" and went looking for a wedge above it. The
silence may simply have meant "past the last boundary".

A diagnostic whose trigger can be outrun by the condition it watches for is
worse than no diagnostic, because it produces confident wrong conclusions.

Now time-gated: checked every 256 items (the mask only keeps GetTickCount off
the hot path), prints when the era has run >60s and >30s since the last line,
up to 12 per era. Progress/phase for the panel is still published on every
call, before any gate.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 07:44:03 -04:00
AnimateDread
b461844767 fix: prebuild and era sized different windows; diag: Train() names its branch
TWO things, one incident.

1) THE BUG I SHIPPED IN 0c85c54. m_tuneStartTrainBar is declared, initialised
to 0, and NEVER ASSIGNED - the assignment existed before the God-class split
and the split dropped it, leaving a dead member. Harmless while nothing read
it; a real defect the moment 0c85c54 made StartLabelCachePrebuild() reset
dtStudied from it. Train() then computed the window as
max(StartTrainBar, floor) while the prebuild computed max(0, floor), where
StartTrainBar is the non-zero datetime OnChartEventHandler passes through from
the "New Bar" event. The two therefore disagreed about `bars`, so
EnsureBarCachesCapacity() saw a changed size at era start, wiped the caches,
and re-armed a full 38k-bar prebuild - instead of training. Restored the
assignment so both sides evaluate the identical expression.

2) THE REASON IT TOOK ALL NIGHT TO FIND. Train() is a state machine with six
early-return branches above the era loop and every one of them is silent. Four
charts burned a core each for 15 minutes with an empty journal: the pass
heartbeats (694b756) proved the era loop was never reached, no prebuild
completion line appeared either, and nothing external can see inside a single
MQL5 thread - per-thread CPU says "busy", file writes say nothing, and the VPS
has no debugger. That is an undiagnosable state, and it is the thing to fix,
not just the bug of the day.

ReportTrainStall() now names the branch Train() is taking whenever no era has
completed for 3 minutes, at most once a minute per signal, with the state that
decides the branch: run/prebuild/simOos/resume flags, era, dtStudied, and -
for the cache-invalidation branch specifically - BOTH bar counts, since two
sizings disagreeing is exactly what re-arms the prebuild forever. Silent on a
healthy run: an era completing resets the clock.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 07:32:08 -04:00
AnimateDread
783fd9e7a6 fix: the panel showed "100%" for the whole of pass 1
The simple panel derived its percentage from pass 2's counters:
(m_isTrainCursor+1) / max(m_isTrainQueueCount,1). During pass 1 those are 0
and 0, so the expression is (0+1)/max(0,1) = 100%. An era spends its first
pass scanning ~38k bars - minutes of work - and the panel reported that phase
as finished the entire time. Observed by the user as "started learning at 100%
of their era and are stuck there", and it actively misled the diagnosis: the
one number on screen said the opposite of what was happening.

The UI cannot fix this on its own - it can see pass 2's counters but has no
way to know which pass owns them. So each pass now PUBLISHES its own progress
and a short phase name through TrainHeartbeat (which every pass already calls
per item), and the panel just displays them: "learning (era 45, scan 34%)".
Published before the heartbeat's 4096-item journal gate, so the panel updates
continuously while the journal stays quiet.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 07:22:29 -04:00
AnimateDread
694b75686e diag: slow eras must explain themselves - heartbeat + era time split + pass-1 paint
The 23:42 restart left all four charts grinding ~25x slower than the 18:01
baseline (era lines in 86 seconds there; 20+ minutes of nothing here), and
NOTHING could say why from outside: pass 1 logs nothing, its status paint sat
inside the !wouldQueue branch so the IS sweep - 80% of the pass, processed
FIRST - painted nothing either, the VPS has no debugger for a thread stack,
and the hourly new-bar cache invalidation cancels and restarts an unfinished
era, so a slow era can stay invisible FOREVER. Externals gave: four chart
threads at ~95% pure user-mode compute, DLL pool idle, no file writes. That
narrows it to "MQL5-side per-item work in the era passes" and no further.

So training now explains itself:

- TrainHeartbeat: one line per 4096 processed items, only after an era has
  already run 60s, at most 6 lines per era - a healthy era stays exactly as
  quiet as before. Reports position and the cumulative split: feature-window
  builds vs net forward/backprop vs everything else. Hooked into all three
  passes.
- The era summary line gains "| ERA TOOK Ns (feature windows X, net fwd/back
  Y, other Z)" whenever an era exceeded 120s.
- Pass 1 paints its progress for QUEUED bars too, not just the OOS slice, so
  the panel shows "learning (era N)" instead of sitting on the idle writer's
  "Getting ready..." for the entire IS sweep. The label is throttled
  internally; painting per bar costs nothing.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 00:10:45 -04:00
AnimateDread
0c85c54a5b fix: a restart no longer loses the measured geometry or the training window
Terminal restart, 22:25: all four resumed models sat on empty windows with
enum 2:6 barriers. Three interlocking causes, all visible in one log excerpt:

1) THE PRE-SCAN WINDOW WAS SIZED BY THE SAVED WATERMARK. A resumed model's
dtStudied sits at its last studied bar, so Bars(dtStudied, now) ~ 0 and the
resumed-model MI pre-scan built a zero-bar "complete" label cache - logged as
"Buy: 0 | Sell: 0 | Neutral: 0". Train()'s own era start RESETS dtStudied to
the training-window rule before computing its window; the pre-scan did not.
The rule is now factored into TrainWindowStart() and both use it. The scan
also refuses to arm before SERIES_SYNCHRONIZED (it ran in the same second as
OnInit), and deployed models keep their watermark - for them it gates
inference recency, not a training window.

2) THE HORIZON LATCHED ON AN INDICATOR WARM-UP. ComputeBarrierHorizonBars ran
against a ZigZag with 0 calculated legs, fell back, and EnsureBarrierHorizon
latched fallback(32) x slMult x tpMult = 384 for the process lifetime. A
leg-starved horizon is now PROVISIONAL: re-resolved on the next rebuild, the
label cache wiped if it moved (labels from two horizons answer different
questions), and the geometry deriver refuses to run from it - a pair derived
over a warm-up window would get PINNED.

3) THE DERIVED GEOMETRY WAS NEVER PERSISTED. The .cfg is written at model
creation and at weights-reset - both BEFORE era 0 derives - so the measured
pair lived only in memory: every restart read back zeros, adopted nothing,
fell back to the enum barriers, and the era-0-only gate meant a resumed model
could NEVER re-derive. A full day of training on 3.33/1.62 resumed as 2:6.
Now: the settled pair is pinned to the .cfg the moment derivation completes
(one-shot, atomic write), and the derive gate accepts any model with no
pinned pair, not just era 0 - mid-run stability is carried by
m_geometryDerived itself, which never allows a second derivation.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 22:40:43 -04:00
AnimateDread
199726f651 fix: a one-sided era can no longer become the best checkpoint
Measured on HYBRID, era 29 of the first win-scored run: the model collapsed
to always-Buy and was crowned "new best selection score 67.1%". Under
win-based scoring that is not a coincidence - the always-call-the-drift-side
model IS the chance reference, so it scores exactly chance (P(winLong) ~ 67%
on SP500), while every honest two-sided era scores 63-66% because shorts win
less often against the drift. Raw score ranking therefore actively prefers
the degenerate model, every regression restores back to it, and live NMS
collapses its near-constant signal to ~25 trades per era - observed as
"hybrid barely trades".

bothSidesLive already blocked one-sided eras from DEPLOYING (tradeableOK,
371f8aa), but among not-yet-deployable eras the score alone ranked - the same
early phase the coverage credit was added for, failing the same way through a
different door.

The ranking key is now three lexicographic tiers: deployable > two-sided >
score. A one-sided era cannot displace a two-sided best regardless of score -
by construction its score is a property of the data's drift, not the model -
and a two-sided era displaces a one-sided best no matter how much lower it
scores. m_bestBothSidesLive is snapshotted with the checkpoint and reset with
the rest of the best-tracking state.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:19:44 -04:00
AnimateDread
9a7c37f334 fix: live trades now use the geometry the gate certifies; perf: BN kernels
Three changes, one theme: the trade placed, the trade graded, and the trade
computed are now the same trade.

1) GEOMETRY WIRE (correctness, the ranked #1 open issue). The measured barrier
pair reached the LABELS only - OpenParams still placed orders at the enum
geometry (2*ATR/6*ATR), so the deploy gate certified "reaches 1.62*ATR before
3.33*ATR above break-even" about trades the EA never placed. Published via
g_DerivedSlAtrMult/g_DerivedTpAtrMult (ConfidenceBridge, same same-tick
contract as the confidence globals, because OpenParams runs on the root signal
which has no pointer to the AI filter). Two writers: DeriveBarrierGeometry at
era 0, and the .cfg adoption a deployed model takes. Overrides both legs and
both Intelligent modes - the certificate is exact or it is nothing. TP is
ATR-anchored like the label, NOT risk-relative, so a floor-widened stop cannot
reshape the certified target.

2) BATCH NORM RUNS DEVICE-SIDE ON OPENCL. Four kernels in Network.cl -
forward, hidden gradient, gamma/beta accumulate, gamma/beta apply - each a
line-for-line transcription of the host implementation (NormalizeHost /
HiddenGradHost / StepGammaBeta) including every NaN guard, clamp, and the
exact moment-write ordering. The host copies remain the runtime for the DLL
and pure-MQL5 tiers and the reference the kernels must match.

Because this box has no OpenCL platform, the safety story is layered:
- shim validation: kernels compiled as C and driven against a fp64 host
  transcription over NaN-poisoned stats, NaN gamma, over-clamp inputs, the
  frozen path, both optimizers, 3 batches - ALL PASS, worst normalized diff
  0.132 vs tolerance 1.0
- in-situ self-check: each kernel is compared against its host twin ON FIRST
  USE on the real device (SelfCheckBn*), covering what the shim cannot - arg
  indices and buffer bindings. Any disagreement resyncs from the good copy,
  latches all BN kernels off process-wide, and training continues host-side.
  A transcription bug costs a warning and some speed, never a poisoned .nnw.
- sync discipline: BatchOptions is now a CBufferDouble with explicit
  authority tracking (m_bnDeviceAuthoritative). Checkpoints/saves pull
  read-only; restores/loads/resets push; a mid-batch handover drains the
  device gamma/beta accumulator into the host arrays so no sample is lost.

3) SMALL FIXES. Apply-kernel build failure now latches the dispatch path at
init (one warning instead of warning + failed Execute). Build tag bumped to
win-scoring-gpu-v1 - first tag change since expectancy-stop-v1 despite five
binary-changing commits.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:51:40 -04:00
AnimateDread
6f920fe378 perf: batch norm stops re-reading buffers it already has
Batch norm computes host-side while its neighbours are device-resident, so
every value it touches crosses the bus - and the cost is the BLOCKING SYNC per
crossing, not the bytes. Per sample it did four reads, two of which were exact
duplicates:

  feedForward        -> previous layer's Output
  calcInputGradients -> own Gradient, and the previous layer's Output AGAIN
  update/accumulate  -> own Gradient AGAIN

Nothing writes the previous layer's Output between the forward and backward
passes, and nothing writes this layer's Gradient between the gradient pass and
the weight-update pass - backPropOCL runs those as two separate top-to-bottom
loops and only the first writes gradients. So the repeats are removable and
the result is bit-exact: the same values, read once instead of twice.

Each cache is armed by its producer and disarmed by feedForward, so a consumer
whose producer did not run this sample falls back to reading the buffer rather
than using the previous sample's data. That is not hypothetical - a batch norm
at layer 1 never gets calcInputGradients called at all, the same asymmetry
backPropOCL already documents for a layer-1 LSTM, so its gradient cache is
never armed and it takes the fallback every time.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:16:08 -04:00
AnimateDread
899e0c66ca perf: mini-batch apply becomes a kernel - 8 weight-matrix transfers per batch become 0
Market builds cannot import a DLL, so OpenCL is the tier paying clients run.
It was several times slower than the CPU DLL, and the dominant reason was a
host-side optimizer step I shipped with the mini-batch work in 274630f.

ApplyAccumToBlock read the weights, the accumulator and both Adam moments back
over the bus, stepped them in MQL5, and wrote four buffers out - eight full
weight-matrix transfers per batch PER WEIGHT BLOCK, each a blocking sync. At
TRAIN_BATCH_SIZE 8 that is roughly one entire weight matrix crossing the bus
per training sample. It was host-side for a good reason (one optimizer
implementation shared by all four tiers instead of four that can drift), and
that reason turned out to cost the product's own compute tier.

- ApplyAccumAdam / ApplyAccumMomentum in Network.cl: flat, elementwise, and
  they zero the accumulator themselves so there is no separate clear dispatch
  and no way to leave it dirty via an early return
- ApplyAccumOnDevice dispatches them; the host step stays as the reference and
  as the implementation for DirectML, the CPU DLL and pure-MQL5
- failure latches OFF process-wide with one warning rather than a failed
  Execute per batch, since a kernel that did not build will not build later
- m_applyKernelsOk is tracked SEPARATELY from m_batchKernelsOk: without the
  accumulation kernels a device cannot batch and must drop to per-sample
  updates, whereas without these it batches normally and merely pays the
  transfers. Conflating them would turn a missing optimisation into a changed
  optimizer

The kernel runs fp32 where the host step ran fp64. That is the OpenCL tier
becoming self-consistent, not a regression: its device buffers are already
fp32 (CBufferDouble::m_data_f) and its unbatched optimizer already ran in
fp32, so the batched path was the odd one out. DLL tiers keep fp64 throughout.

Validated: no OpenCL platform exists on this box, so the kernel source is
syntax/type checked as C against a shim and driven for 4000 steps. It clears
the accumulator, and it is scale-invariant - displacement 1.199336 at |g|=1
versus 1.199333 at |g|=1e-4, matching the figure DirectML/batch_accum_check
produced for the fixed CPU_UpdateWeightsAdam. It reproduces the corrected Adam,
not the pre-371f8aa one.

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 17:09:52 -04:00
AnimateDread
5cef0947f4 fix: the deploy gate was benchmarking a win rate against a label frequency
The gate rests on an invariant stated at ExpertSignalAIBase.mqh:199 - under a
driftless walk P(touch +k before -m) is m/(m+k), and break-even for a k:m trade
is ALSO m/(m+k), so "beats chance" and "is profitable" are the same test.

That invariant needs reward >= risk, and the measured geometry no longer
satisfies it. With target 1.62*ATR and stop 3.33*ATR, break-even is 67.3%, but
both-won bars were stripped out of Buy and Sell so the label base rate read
37.5%. chancePrecPct is max(BuyTotal,SellTotal)/bars, so the gate was clearing
models nearly 30pp short of break-even: 42% "directional precision" is +4 sigma
against 37.5% and loses money on every single trade. Live since 217b9bc.

Root cause is that label agreement stopped being the same question as trade
profitability. Buy implies winLong, but the converse fails on every both-won
bar, and the label can only name one of two directions that both pay.

So stop asking the model whether it matched a label and start asking whether
its trade paid:

- cache winLong/winShort per bar beside the label, under the same validity
  flag; published from the barrier walk before the collapse to 3 classes
- dirPrecPct now counts wins on the side actually called
- chancePrecPct is max(P(winLong), P(winShort)), MEASURED - the textbook
  m/(m+k) would credit SP500's drift to the model
- the NMS "what would I have made" pair, the live-fired precision, and the
  IS/OOS cumulative win rates all move to the same test. IS and OOS are read
  side by side as the overfitting signal, so measuring one in wins and the
  other in agreement would put a fixed gap between them that has nothing to do
  with generalization
- the confidence threshold is FITTED on wins too, so the operating point
  maximises what the gate grades
- per-class label-agreement precision is still computed and logged; it is the
  right diagnostic for class separation, just not for a deploy decision
- era line renamed dir-precision -> win-rate, chance -> chance=break-even

Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 16:00:51 -04:00
AnimateDread
ce5265488e fix: both-won bars were labelled "do not trade" - resolve by first touch
Removing the min-reward:risk raise let the MEASURED geometry come back with
the target NEARER than the stop (SP500 H1: target 1.62*ATR at q50 of
favourable, stop 3.33*ATR at q75 of adverse). That reopened a branch the
code called unreachable: price can reach +target and -target inside one
horizon, winning in BOTH directions, and those bars fell through to Neutral.

Neutral has only three producers, both-lost is unreachable (you cannot touch
-3.33 without crossing -1.62 first, which wins the short), and timeouts logged
at 1.0% of Neutral - so ~27% of ALL bars were being handed to the model as the
abstain class when a trade either way would have collected its target. The
cleanest positives in the sample, labelled "do not trade", while the fitted
confidence threshold was being asked to find selectivity in what was left.

Resolved by FIRST TOUCH: the target reached earlier is the trade that would
have closed first. Same forward window, no extra lookahead. Same-bar ties stay
Neutral - OHLC cannot order two touches, and unlike an intrabar stop tie there
is no pessimistic side to fall to, so a guess would inject a coin-flip
direction into the target.

Also:
- count both-won and its same-bar tie subset in the prebuild line, so the
  share is measured rather than inferred from arithmetic on a log line
- scope the timeout counter to IS, matching the tally it is reported as a
  percentage OF; it was incremented over the whole scan and divided by an
  in-sample denominator
- clear m_lastBarrierTimedOut at the top of the walk with the excursions, not
  at the bottom - the two early returns published the previous bar's verdict
- mark the pass-1 label line PROVISIONAL. It prints the enum fallback because
  geometry can only be derived from excursions that do not exist yet, and it
  reads exactly like a config change that failed to take effect

FORCES RETRAIN. Both build variants compile 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:35:42 -04:00
AnimateDread
19dfb91108 feat: fitted directional confidence threshold - selectivity gets a mechanism
The training loss and the selection metric wanted different things and only
the second one knew it. Logit-adjusted cross-entropy has no term for "how
often should I trade", so the head calls a direction on 87-91% of bars. The
selection metric is precision x coverage credit, saturating at the coverage
floor - above the floor extra calls earn NOTHING and only precision counts.
So selection wanted few good calls, the loss produced many mediocre ones, and
all selection could do was pick the least-bad era out of what it was handed.
Nothing pushed the model toward selectivity.

This gives the decision RULE the policy instead of distorting the loss (which
is estimating class probabilities correctly, and a probability estimate should
not be bent to encode a trading policy - Elkan 2001: estimate, then choose the
operating point separately). AdjustedSignalFromSoftmax now abstains unless the
winning direction's softmax margin over its best rival clears a fitted
threshold. Margin, not the winning probability: the latter moves with overall
calibration rather than with how close the decision actually was.

Fitted on IS, applied to OOS and live. Pass 2 already forward-passes every IS
sample, so the margin histogram is harvested there for free (primary
occurrences only, so the oversampled replay queue cannot skew the operating
point); the fit runs at the end of pass 2, BEFORE pass 3, so the deploy gate
grades the thresholded model on bars the threshold never saw. Fitting on
pass 3's own predictions would be choosing the operating point on the data
being graded - the best-of-N error corrected in five other places here.

Objective: maximise IS directional precision subject to still clearing the
SAME coverage floor the deploy gate uses (base rate x 0.25, re-derived
locally so the two cannot drift apart). Swept top-down in one pass; ties go
to the LOWER threshold, since equal precision for less coverage is strictly
worse. Under DIR_CONF_MIN_FIT_CALLS (200) it runs unthresholded rather than
on a guess.

The threshold is part of the MODEL, not the run: captured with
Net.CaptureWeights(), restored with the weights at both restore sites, and
appended to the .cfg under the same length-guard convention so a deployed
model reloads at the operating point its gate actually cleared. A pre-2026-08-09
.cfg reads 0.0, which is exactly the behaviour it was trained under.

Per-era line now prints "@margin>=X.XX" next to coverage, so a coverage drop
can be attributed to the operating point rather than guessed at.

Both build variants compile 0 errors / 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 15:04:37 -04:00
AnimateDread
217b9bc9bf feat: remove Min_Risk_Reward_Ratio - a guess was overriding a measurement
The barrier geometry is derived from the instrument's own excursion
distribution (stop at q75 of adverse travel, target at q50 of favourable),
and then a 1:2 floor was applied on top, raising the target to twice whatever
the stop happened to be. On SP500 H1 that pushed the target to 6.66*ATR,
reached on 3.3% of bars inside the horizon - so the label became "almost
never a win" and every topology was trained to predict an event that
essentially does not occur. A measured target has to stay measured.

The ratio never bought what it was believed to buy. A reward:risk floor does
not create expectancy; it trades hit rate against payoff at a break-even the
geometry already fixes - which this project has separately MEASURED (payoff
0.92 -> 5.72 with expectancy flat). What it did buy was two outages: four
consecutive Market validation rejections for "no trading operations" when it
rejected 100% of setups, and the label corruption above.

Removed:
- the input and the RISK_REWARD_RATIO enum (deleted, not left dangling - a
  live enum with no input behind it is the shape of the stale-.set incident
  that trained ~250 eras on the wrong target)
- the forced target raise in the label geometry
- the rrOK eligibility gate in the barrier-geometry scan, so every unclamped
  pairing now competes on the measurement alone. Clamping stays disqualifying
  for its own unrelated reason.
- the reward < minRR*risk veto in OpenParams

Kept: g_TradeRewardRiskRatio still computed and still bridged to Kelly sizing
in MoneyIntelligent - the ratio as a SIZING input was always the sound use.
Risk stays bounded where it actually is - account risk % and CRiskBudget.

The low-reachability warning survives but is re-aimed: with nothing inflating
the target, a target the market rarely reaches can only mean the horizon is
truncating the excursions the geometry is derived from.

Both build variants compile 0 errors / 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:51:59 -04:00
AnimateDread
371f8aaecd fix: the Adam second moment was never Adam - all four tiers
Root cause of the B=32 regression, and it predates F4 entirely. Every Adam
kernel stored v already square-rooted and then fed that stored value back in
as if it were the variance:

    v_new = sqrt(b2 * v_old + (1 - b2) * g^2)

That recursion has a fixed point at v ~= b2 = 0.999 for ANY gradient below
unit scale, so the denominator stops tracking the gradient and Adam degrades
into plain SGD with lr = lt. Measured against the shipped WarriorCPU.dll
(batch_accum_check.cpp, TestOptimizerScaleInvariance), 4000 steps of a
constant gradient: 3285x less displacement at |g|=1e-5 than at |g|=1, where
a scale-invariant optimizer gives the same distance for both. After the fix
all six magnitudes read 1.199 and v tracks |g| exactly.

It hit conv/LSTM specifically because they sit behind a batch-norm with
running variance ~2.6e+05, so their gradients arrive divided by ~500 - deep
in the degraded regime - while the dense stack near the loss stayed in the
working one. In situ on SP500 H1: lstm1 dW/W 2.62/10.0/7.14% -> 0.024/0.022/
0.003%, conv1 decaying to 0.000% by era 30. NeuronBatchNorm.mqh already
squared v back for gamma/beta and its comment named the kernels as wrong,
which is exactly why gamma/beta kept training while the stages behind froze.

Persisted .nnw needs no migration - v keeps its std-dev meaning.

Also, the two ways F4 exposed it, both mine:

- No LR compensation for B fewer steps per era. sqrt(B) for adaptive methods
  (Krizhevsky 2014; Granziol et al. 2022), applied once in
  InitialEtaForOptimizer(). Linear scaling (Goyal et al. 2017) is for SGD.
- Plateau patience denominated in eras, so raising B made the ladder 32x more
  impatient in its only unit. PAI converged at era 41 on ~49k updates where
  the same config had been finding new bests at era 1028.
  TrainPlateauPatienceEras() stretches it by the same sqrt(B).

TRAIN_BATCH_SIZE 32 -> 8 so the patience stretch stays affordable (8 -> 23
eras per stage, not 8 -> 45). Both helpers are identities at B=1.

Deploy gate: DEPLOY_MIN_SIDE_RECALL_PCT (10%) folded into tradeableOK. The
perceptron reported Sell:0% recall in all 41 eras, cleared the floor on Buy
alone at 36.6% vs 34% chance, deployed, and sprayed buy arrows. Folded into
the ranking key rather than checked at deploy time so a one-sided era cannot
become best-so-far in the first place.

Deinit: the arrow purge now runs BEFORE ExtPanel.Destroy(), an unbounded
CAppDialog teardown that sat ahead of it - the same ordering inversion the
rule there exists to prevent. CONV was force-terminated 4.8 s into OnDeinit
(vs ~1.1 s for the three that finished) having reached none of its cleanup,
so its arrows stayed on the chart. Steps are now timed in the log.

PurgeChart's verification rescan filtered on OBJ_ARROW, the same blind spot
as the bulk delete, so "persisted 10 ... cleared 0" passed silently. It now
walks every object type and reports the object counts when both are zero.

Both build variants compile 0 errors / 0 warnings; both DLLs rebuilt.
FORCES A RETRAIN (already forced by N1) and both DLLs must ship with the .ex5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 14:02:35 -04:00
AnimateDread
3c8d67254b chore: update binary files for WarriorCPU and WarriorDML components 2026-08-09 12:43:49 -04:00
AnimateDread
0c01dc279b feat: mini-batch gradient accumulation (F4), front-end-aware capacity budget (F6), split Wyckoff categoricals (N1)
Completes the 2026-08-09 training audit. FORCES A RETRAIN of every
Wyckoff-enabled config (N1 re-keys the fingerprint), and BOTH DLLs must be
redeployed alongside the .ex5 - they carry new exports.

F4 - mini-batch accumulation, TRAIN_BATCH_SIZE=32. Training was pure online
SGD (one weight update per bar), which is the mechanical source of the
era-to-era whipsaw every downstream guard was built to cope with. The O(n^2)
outer product is native - AccumulateWeightGrad / AccumulateWeightGradConv /
AccumulateBufferInto in Network.cl, WarriorCPU and WarriorDML - while the
optimizer step is host-side MQL5 shared by all tiers (ApplyAccumToBlock), so
there is one Adam/SGD implementation instead of four that can drift.
  - the LSTM needs no outer-product kernel (WeightsGradient already holds the
    sample's full dW) but could NOT simply be left un-zeroed between samples:
    CPU_LSTMSeqBackward/DML_LSTMSeqBackward memset it on entry. Hence a
    separate accumulator plus an elementwise add.
  - batch-norm gamma/beta accumulate in host arrays, not new BatchOptions
    slots - BN_OPT_STRIDE is baked into every persisted .nnw.
  - scoped to pass 2; online learning keeps immediate updates. Every save /
    checkpoint / scoring boundary flushes, scaling by the real sample count.
  - degrades to per-sample updates (one log line) on a tier that cannot
    accumulate, so old devices and DLL-free builds are unaffected.
  - verified offline: DirectML/batch_accum_check.cpp drives the real exports
    against an independent reference; at B=1 the accumulator matches the
    shipped unbatched kernel's own gradient to 1.1e-16. Math only - the
    in-situ check remains the per-layer dW/W report on a real era.

F6 - ComputeFirstLayerWidth budgeted against the RAW input width even where a
conv/LSTM front end had already reduced it, so an LSTM's dense stack was
charged for 1,280 inputs when it receives 64. Confirmed from the deployed
.cfg files: CONV, LSTM and HYBRID were all pinned at the 16-unit floor. Now
budgeted against the front-end output and capped at it (never fan out), with
the derivation reordered so both stages settle first.

N1 - EventCode/EventPhase/StructuralPhase are signed categoricals packing
direction and Wyckoff stage into one scalar across a sign discontinuity. Split
into direction + [0,1] magnitude, the same convention the base OHLC block uses.
Information-preserving; 13 readings now occupy 16 inputs.

Compiled clean (0 errors, 0 warnings); both DLLs rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 11:48:03 -04:00
AnimateDread
274630f802 fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):

- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
  which is 15-bit - provably non-uniform on every full-history era over 32,768
  queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
  ceiling (the normal state of a non-regressing plateau) - the ladder was just
  a 24-era countdown. Restarts now overshoot to 5x the ceiling
  (PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
  window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
  real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
  Adam moments, so the optimizer immediately pushed back toward the rolled-back
  state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
  zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
  untouched) on every mid-run restore, every boosted restart, and the
  deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
  so the selection metric the checkpoint ranking and deploy gate read is a pure
  function of the checkpoint instead of partly measuring BN drift. Defensive
  unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
  the OOS continual-learning simulation stay adaptive by design.

Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00
AnimateDread
ee48381cbd fix: NMS gates the TRADE, not just the arrow - one arrow is now one trade
NmsLiveAccept() appeared in exactly one place: wrapped around DrawObject().
It never touched dPrevSignal, and dPrevSignal is what LongCondition() /
ShortCondition() / SignedAIConfidence() read. So a declustered bar lost its
arrow and still opened a position.

Measured on SP500 H1 2026-08-09: CONV called a direction on 64% of bars,
so the ~500 bars visible on screen held ~320 decisions - and ~40 arrows
were drawn. Roughly one arrow per eight positions the EA would take.

And the survivors are not a random eighth. Rule 2 of the declustering
keeps the HIGHER-CONFIDENCE side of a cluster, so the visible set is
systematically the best member of each run. A chart showing the best of
every eight decisions and hiding the rest reads far better than the model
is - the same best-of-N selection error already corrected in the geometry
scan, the indicator tuner, the lag profile and the deploy gate, this time
on the display layer, where it is most likely to mislead the person
deciding whether to trade.

Fixed by neutralising dPrevSignal when NMS rejects, rather than adding a
"may trade" flag consulted at each read site: that leaves exactly ONE
definition of what the model decided this bar, so the arrow, the panel's
"Current signal", the confidence feeding sizing/SL/TP/trailing, the
refresh tally and the order itself cannot drift apart again.

Also reports the consequence instead of hiding it. Every OOS counter on
the era line still scores every directional call - a population ~8x larger
than what now trades - so the line carries a second figure:

  | TRADED (declustered) NN% on N calls (edge +Npp)

replaying the identical rule over pass 3 (which walks OOS bars oldest to
newest, the same order the live sweep sees). Its cursors are separate
members from the live ones so a training pass can never disturb the live
chart's declustering.

Deliberately NOT switched into selectionScore yet. Declustering cuts
coverage from ~64% of bars to ~8%, well under
MIN_COVERAGE_FRACTION_OF_BASE_RATE, which would make every checkpoint
undeployable overnight - the minRR collision and the recall-floor catch-22
twice over. The floor gets re-derived from these measurements first.

Compiles clean: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:22:31 -04:00
AnimateDread
c690a73901 docs: remove outdated note from AutoTuneIndicators comment
The comment previously included "; see note" which is no longer applicable. Removed to keep the input description concise and accurate.
2026-08-09 09:51:27 -04:00
AnimateDread
fcb69d8efb research: test whether across-time structure exists at all
The one hypothesis the shipped diagnostics do not cover. The EA prints it
itself: the MI measure is "marginal (one feature at a time) and per-bar",
so a floor reading "cannot rule out one that only exists in combination or
across time". The instrument for across-time structure is the sequence
model, and until bfc1da9 it read its window backwards - so the hypothesis
has never been tested, not once.

Asks the DATA rather than retraining the LSTM. A topology comparison in
MT5 confounds "can order be exploited" with learning rate, init, capacity,
class weighting, batch-norm state and a single OOS slice, and costs hours
per run on a CPU-only box.

Three configurations, identical model / CV / null:
  A  entry bar only         joint across FEATURES, one bar
  B  window, chronological  joint across features AND time
  C  window, lag order shuffled INDEPENDENTLY PER SAMPLE

B-A: does history help at all.  B-C: does temporal POSITION carry
information - the actual hypothesis.

C is the design. A single global shuffle tests nothing, because any model
relearns a permuted column layout; per-sample shuffling kills the
lag<->column correspondence while preserving the exact multiset of values
in the window. It is the time-order analogue of the shuffled-label null.

VALIDATED WITH BOTH CONTROLS, because an untested research script is worse
than none:
  planted order-only label (monotone up/down):
      A -4.13pp   B +85.68pp   C -4.13pp   B-C +89.81pp
      C collapses to exactly A - an order-destroyed window carries
      precisely as much as one bar. Correct.
  planted order-free label (window sum):
      A +22.23pp  B +65.62pp   C +65.44pp  B-C +0.18pp
      history helps (+43pp over one bar) and the test attributes none of
      it to order. Correct.

Metric is directional precision against max(P(Buy),P(Sell)) - the same
metric and zero-skill benchmark the EA's deployability gate uses, so a
result reads directly against an era line.

Two p-values, because the analytic one lies: barrier labels are massively
autocorrelated, so the binomial p is optimistic and is reported only for
comparability. The BLOCK-permutation p (block = horizon, Phipson & Smyth)
is the one to believe - same correction test_volume.py already applies.

Purged, embargoed walk-forward CV (H bars each side), because without the
embargo the barrier horizon leaks across the split - the mistake that
scored +0.53 R on an "honest" holdout once already.

normal_upper() mirrors NormalUpperTail() in Training.mqh so the two agree
to 6dp.

Needs <SYM>_<TF>_rates.csv from a WARRIOR_EXPORT_FEATURES build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:42:49 -04:00
AnimateDread
922484e8d9 feat: expose the AD/Wyckoff parameters; default the indicator tuner off
AutoTuneIndicators now defaults to FALSE, and the 33 AD/Wyckoff parameters
it used to search are now inputs.

WHY THE DEFAULT FLIPPED - not because the search is broken. It is correct,
and its own Sidak gate is what proves it: 324 candidates per model on
SP500 H1, "no improvement" on all four topologies (0.00236 -> 0.00236 on
the AD configs, 0.00370 -> 0.00370 on PAI), winner rejected at p=1.0000.
It cannot do better here by construction - it ranks candidates by MARGINAL
MI, and the headline MI is 0.00370 nats against a shuffled null of
0.00379 +/- 0.00061 (p=0.4975), so every candidate is a noise draw and the
maximum over N of them is noise too. The cost is 45-56 min per model in
one synchronous call with no yield, and it was the amplifier for the
handle leak fixed in 33f106d. The EA's own report says it plainest: "no
per-feature indicator retuning will help."

THE INPUT STAYS. TuneIndicatorsByFilter is one function of twelve in
AIBase/AutoTune.mqh; the other eleven are the MI/lag/excursion/geometry
diagnostics that produced every verdict this project relies on, and they
run regardless of this flag. Removing the input invites removing the file.

WHY THE INPUTS WERE NEEDED. All 33 were literals in CADIndicatorTuner's
constructor with no input of any kind, while MA/RSI/MACD/Ichimoku have had
their periods exposed from the start. On the AD configs those indicators
contribute 28 of 64 features per bar. Survivable while the tuner searched
them; indefensible with it off, where they would freeze at values nobody
chose.

CONSOLIDATED 33 -> 18. volClimax/volHigh/rangeClimax/rangeSignificant/
stVolRatio/atr were duplicated verbatim across CumulativeDelta, Wyckoff
Events, Failed Structure and Bar Inversion - the same constants restated
3-4 times. One concept, one input. They are SEEDS: each fans out to the
indicator's own struct field, so with the tuner on it retains full
per-indicator freedom to move them apart. Same contract as PeriodMA.

NO RETRAIN. Every default is byte-identical to the literal it replaces,
and the fingerprint's new ADP token is appended ONLY on deviation
(MACD/Ichimoku/BN/XA convention), gated on the AD features being enabled.
At defaults the token is absent, so every model on disk keeps its filename
and stays loadable. Without that guard, merely EXPOSING these parameters
would have re-keyed every config and forced a from-scratch retrain of all
four topologies for a change that alters no number anywhere.
All-or-nothing rather than per-input, so the token can never encode a
partial picture of what the features were built from.

Compiles clean: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:37:21 -04:00
AnimateDread
1df305431d feat: gate deployment on the null of the MAXIMUM, not the per-era null
EDGE_MIN_SIGMAS is a PER-ERA test and the deployed model is the MAXIMUM
over every era a run ranks. A 2-sigma one-sided test passes on noise with
probability 0.0228 per era, so over N eras the chance at least one clears
it is 1-(1-0.0228)^N: 34% by era 18, 80% by era 70, 93% by era 112. The
gate was near-certain to open on a long run whatever the data held.

It did. HYBRID deployed 2026-08-08 at dir-precision 35.5% vs 34% chance -
+1.5pp, best of 112 eras whose per-era values wandered 30%..35.5%. At the
call counts these runs produce that is p_family 0.92..0.9999.

Every OTHER best-of-N decision here already carries this correction, and
every one REJECTS on this data: the barrier-geometry winner (null of the
maximum over 6, p=0.3902), the indicator tuner (Sidak, p=1.0000), the MI
lag profile (null of the maximum over 21 lags). The one decision that
ships a model to a live account had none.

BestCheckpointSurvivesSelection() re-tests the checkpoint that is about to
deploy:
    z        = (precision - chance)/SE,  SE = sqrt(p0(1-p0)/n)
    p_single = P(Z >= z)
    p_family = 1 - (1-p_single)^N
against DEPLOY_FAMILY_WISE_ALPHA. It uses the checkpoint's OWN
snapshotted precision/chance/call-count, not the latest era's, because
the model that ships is the one that has to clear the bar.

N counts CANDIDATE eras (coverage measurable, at least one directional
call) - an era that called nothing directional could never have become
the best, so counting it would make the gate stricter than the search
that actually happened.

Conservative on purpose: consecutive eras share OOS bars and differ by
one gradient step, so they are nowhere near N independent draws and the
true family-wise error is below this bound. This gate decides what trades
real money and the house posture is reject-unless-demonstrated.

Effect at 2900 directional calls / N=112: required edge goes 1.76pp ->
2.92pp. A real edge clears it; +1.5pp does not.

Applied to BOTH automatic paths - the plateau ladder's stage-3 deploy and
the m_trainingComplete assignment - which must stay identical or the flag
persisted into the .nnw disagrees with the decision to stop, and a reload
runs inference on a model the ladder refused.

NOT applied to the two operator paths (era-cap deploy, panel Deploy
button). Those stay the operator's call; ReportSelectionGateVerdict()
logs the verdict beside them so an authorised deploy can never later be
misread as a validated one.

NormalUpperTail() is A&S 26.2.17 (|err| < 7.5e-8), self-contained rather
than pulling in Math\Stat. Verified against reference values to 6dp:
Q(1.645)=0.049985, Q(1.96)=0.024998, Q(3.0)=0.001350. Its locals are
ntB1..ntB5 because AI\Network.mqh line 79 does "#define b1 AdamBeta1".

Compiles clean: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:01:04 -04:00
AnimateDread
33f106d99d fix: the indicator re-init leaked a terminal handle per candidate
This is what killed CONV and LSTM on 2026-08-07. Terminal journal:

  19:19:40  6664 x "VirtualAlloc failed in large allocator"
  19:19:40.829  expert Warrior_EA (SP500,H1) removed      <- CONV
  19:29:55  2048 x "VirtualAlloc failed in large allocator"
  19:29:55.359  expert Warrior_EA (SP500,H1) removed      <- LSTM

50ms and 71ms after each printed its "logit adjustment" line, i.e. the
instant era 0 tried to allocate its training queues. They did not hang -
MT5 shot them for running out of memory.

ReInitADIndicators() re-Create()s every enabled indicator and released
nothing. The comment above it asserted "CiCustom.Create() already
releases its old handle"; MQL5's CIndicator::Create is

    m_handle = IndicatorCreate(symbol, period, type, num_params, params);

a plain overwrite, whose only success-path IndicatorRelease is in
~CIndicator. IndicatorRelease appeared nowhere in this codebase.

That function is the indicator tuner's inner loop. AutoTuneIndicators
scored 324 candidates per model on SP500 H1, so ~324 x 6 orphaned
terminal-side instances, each holding a full-history buffer set -
ADWyckoffEventStream is 14 buffers x ~38k bars x 8 bytes = ~4.3 MB each.
Gigabytes. Confirmed in the shutdown teardown, where a single surviving
expert still held 70 x ADWES, 53 x ADWyckoffEventStream, 23 x
WFS(48,3,1.80,1.10), 19 x ADMovingAverage, 18 x WYSB(162). Those
parameter sets are the tuner's candidate grids.

Release is UNCONDITIONAL, not gated on the handle having changed: MT5
refcounts instances by (symbol, period, params), so re-creating with
IDENTICAL params returns the SAME handle with the count incremented -
the "23 x WFS(48,3,1.80,1.10)" pattern. Either way Create() added one
reference and we hold one handle, so one release is owed.

Released AFTER the re-creates, never before: dropping the terminal's
last reference first would tear the instance down, so an identical-params
Create() would rebuild it from scratch instead of reusing the live one -
turning a refcount bump into a full recalculation over all history, 324
times over.

PAI was unaffected because it has no AD/Wyckoff indicators enabled (35
candidates, built-ins only). HYBRID survived on luck: CONV and LSTM died
2 and 12 minutes before its own sweep finished, freeing the memory.

Compiles clean: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 15:42:03 -04:00
AnimateDread
bfc1da9de1 fix: the sequence models were reading the window backwards
BuildFeatureWindow() replaces eight hand-rolled copies of the same loop
and feeds the window OLDEST BAR FIRST. Every copy fed it newest-first,
because MQL5 timeseries indices run backwards and `r + b` with b ascending
walks into the past.

Harmless for PAI and CONV - a dense layer learns a weight per position
either way, a conv learns time-mirrored kernels. Not harmless for the
recurrent stacks:

  - LSTM_SeqStepForward reads `inputs + t*Iw`, so step t is block t.
  - It writes output[] only when t == steps-1: the visible output IS the
    last hidden state.
  - c_t = f*c_{t-1} + i*g decays toward the start of the sequence.
    lstm_seq_flowcheck.cpp measured block 0's influence on the output at
    1.2e-2 of block T-1's, at the shipped forget bias of 1.0.

So the bar being PREDICTED sat at the far end of the decay and the output
was handed to the OLDEST bar in the window - the exact inverse of what the
window is for. ~80x backwards on LSTM and HYBRID, on all three tiers
(OpenCL kernel, CPU DLL, pure-MQL5 inference), which is why it never
surfaced as a backend discrepancy.

This does not create edge - the MI diagnostics read at the noise floor
(p=0.4975) with a working positive control. It makes the one hypothesis
those diagnostics explicitly do NOT cover testable: they are marginal and
per-bar, and state they "cannot rule out one that only exists in
combination or across time". The sequence model is the instrument for
across-time structure and it has been crippled, so that hypothesis has
never been honestly tested.

Fingerprint gets an unconditional |WIN:2 - the vector keeps its shape and
its features, so a stale .nnw would load cleanly and run a model fitted to
one ordering against the other, silently. Re-keying every config is the
point, not collateral damage. FORCES A FULL RETRAIN.

Also: the now-relative bar caches are re-keyed on the two live paths.
EnsureBarCachesCapacity() was only ever called from training paths, but
once m_trainingComplete is set ScheduleTrainingIfNeeded() routes every bar
to RefreshConvergedSignal() and Train() is never re-entered - so nothing
cleared the feature cache again for the life of the process. A chart that
trained to convergence kept replaying the rows computed for the last
training era's bar grid: the live signal froze at its convergence-time
value, and OnlineLearnStep() backpropped those stale features against
freshly resolved labels. Backtests were never affected (an inference-only
process never allocates the arrays, so every read recomputes).

Compiles clean: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 18:28:44 -04:00
AnimateDread
da54639996 feat: expectancy stop - halt when the measured result says the strategy loses
The daily (4%) and total (8%) rules bound how FAST an account can lose. Nothing
noticed WHETHER it was losing. A negative-expectancy signal traded at 1% inside
that envelope breaches no rule and still arrives at zero - it just takes longer,
with every limit green the whole way down. That is the realistic way this EA
destroys an account, and no existing guard could see it.

THE ARITHMETIC THIS ENFORCES. Expected value per trade is p*TP - (1-p)*SL - cost.
With no directional edge p equals SL/(SL+TP), which is also the break-even rate,
so the payoff terms cancel exactly and EV = -cost. Expected P&L is -(trades) x
cost: strictly negative, proportional to activity. Measured here: directional
precision 23-24% against a 25% break-even, flat across every confidence tier,
with 58 points of spread on SP500. Sizing, stop placement and trailing move
variance around that mean; none of them changes its sign.

So every closed position now reports its result in R (net profit over money
actually at risk) and the running mean is tested against zero. Above the
configured minimum sample, if mean + sigma*SE < 0, new entries stop.

  - SIGNIFICANTLY below, not merely below. A run of losers is ordinary variance
    even for a profitable system; halting on the raw mean would be the same
    act-on-noise error the MI gates exist to prevent. Using the standard error
    means a wide spread simply demands more trades before the rule can fire.
  - NET of swap and commission (ResolveClose already sums all three). Deliberate
    and load-bearing: when the edge is zero, cost IS the expectancy, so a gross
    version would measure a strategy nobody can trade.
  - Reported in R so symbols, lot sizes and balances share one scale and one
    mean. Trades without a stop are not scored rather than assigned a guessed R.
  - LATCHED across restarts, like the daily halt and for the same reason: a
    latch a reattach clears is not a latch. Clearing it means deleting the risk
    state file, deliberately, after looking at why.

State is appended to the risk file length-guarded, so files written before this
still load and start their sample at zero rather than misreading.

Defaults 40 trades / 2 sigma; ExpectancyMinTrades = 0 disables it.

This does not make the strategy profitable and is not meant to. It stops paying
tuition on one the results say is losing, and does it on measurement rather than
on a drawdown limit finally being reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:20:00 -04:00
AnimateDread
b3b7e7bceb fix: excursion window must not depend on the barrier it sizes
DIRECTION IS NOT THERE, and this run is what establishes it. Three symbols:

  raw ASYMMETRY   clears on all three (p=0.0199 / 0.0050 / 0.0050)
  norm ASYMMETRY  collapses on all three (p=0.3433 / 0.5075 / 0.2736),
                  USDCAD landing BELOW its own null
  RANGE control   strengthens to 3-5x its null everywhere

Divide sigma out and the apparent directional signal vanishes entirely. What
cleared was volatility leaking through an unnormalised difference. Note this
would have passed any replication test: three instruments at p=0.005 is exactly
the evidence one would accept before committing to a rebuild, and the confound
reproduces perfectly. Replication was never going to catch it - only the
normalisation could.

Two defects of mine, both surfaced by the same run.

1. THE GEOMETRY DERIVATION WAS DIVERGING, NOT CONVERGING. It produced a
   14.57*ATR stop and a 29.14*ATR target that only 5.7% of bars ever reach.
   Excursions were measured over the barrier horizon; the horizon scales with
   the target; the target is a quantile of the excursions - so target ->
   horizon -> excursions -> target ran away, and "settled" only because the
   horizon ladder caps at 384 bars. A saturated runaway, which the iteration
   guard could not catch because it watches for OSCILLATION.
   Fixed at the root: excursions now accumulate only over m_swingMedianBars -
   the UNSCALED median ZigZag leg, a property of the instrument that owes
   nothing to the barrier. The barrier walk still runs the full horizon,
   because that is how long the trade is held; only the MEASUREMENT used to
   size the barrier is confined to a geometry-independent window.
   (The Min_Risk_Reward_Ratio warning fired correctly and is what flagged it -
   the diagnostic worked while the derivation behind it did not.)

2. THE CONFOUND VERDICT WAS UNREACHABLE. `sizeCleared && !asymCleared` was
   tested first and is true whenever size clears - i.e. always - so the branch
   that NAMES the volatility confound never printed; all three symbols showed
   the generic size-not-direction message instead. Verdict chain rewritten with
   the specific case first, and the dangling elses my first patch introduced
   removed.

FORCES A FULL RETRAIN (the excursion window changes every derived barrier).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:57:23 -04:00
AnimateDread
32ffeb99f3 fix: normalise the asymmetry target - the raw one is confounded by volatility
Three symbols ran the excursion test. RANGE/UP/DOWN cleared on all three;
raw ASYMMETRY cleared on EURUSD and USDCAD at p=0.0050 and not on SP500
(p=0.1045). That looked like the first directional signal this project has
found. It probably is not, and the test as built could not tell.

(up-dn) IS NOT SCALE-FREE. If sigma is predictable - and RANGE clears at ~4x its
null on every instrument - and the directional part is symmetric noise eps, then
up-dn ~ sigma*eps, so a large sigma pushes the value into BOTH outer terciles. A
pure volatility predictor scores positive MI against a 3-bin (up-dn) while
carrying no directional information at all. Crucially that confound REPLICATES,
so reproducing on two instruments is not evidence against it - and the effect
sizes fit it: asymmetry runs 1.3-1.6x its null where RANGE runs ~4x, and carries
~0.1% of the target's entropy against RANGE's ~0.9%. That is the shape of a
leaked fraction of the volatility signal, not an independent one.

So add (up-dn)/(up+dn): bounded in [-1,+1], volatility divided out, and the only
target a directional claim may rest on. The verdict now separates the cases and
NAMES the confound when raw clears while normalised does not, instead of
reporting the raw line as a finding.

Two bugs of mine in the same block, both caught by output rather than review:

  - The derived-geometry line had a MISORDERED argument list: it printed
    "stop 25.00*ATR (q3 of adverse travel)" - the quantile percentage as the
    multiple and the multiple as the quantile. Real values were 2.61 stop /
    8.03 target. A 25*ATR stop is absurd on its face, which is why it was seen.
  - THE STOP QUANTILE WAS BACKWARDS, and this one changes labels. It was 0.25
    "so ordinary noise does not reach it", but q25 means 75% of bars EXCEED the
    stop - hit three times in four. The printed reachability said exactly that
    ("stop on 75.0% of bars"). Now 0.75. A quantile is a threshold, not a rate.
    This is the entire reason reachability is measured and printed rather than
    assumed.

Also raises BARRIER_DERIVE_MAX_PASSES 3 -> 5: SP500 did not settle in 3 (stop
still moving ~14% per pass) while EURUSD and USDCAD converged on pass 2. And
bounds both quantile indices with MathMin(..., n-1) so q=1.0 cannot run off the
end of the sorted array.

The geometry from the previous run is NOT usable and the asymmetry result is
unresolved, not established. Both are decided by the next run.

FORCES A FULL RETRAIN (the stop quantile changes every label).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:04:13 -04:00
AnimateDread
a7701f032b feat: derive the ATR multiples from measured excursions - no hardcoded geometry
The barrier was still two constants. SL_Mode/TP_Mode left the Inputs tab in
3482b6c, but the fallback was a hardcoded 2:6 and the geometry scan only ever
chose from a hardcoded grid {2,3} x {2,3,4,6,8,10}. Picking the least-bad of
eleven guesses is not deriving anything.

WHY THE SCAN WAS THE WRONG INSTRUMENT, now measurable rather than argued. It
ranks pairings by how predictable their OUTCOME is - a question about direction.
The excursion test (2c78f3b) ran on SP500 H1 and direction is the one thing
absent: ASYMMETRY p=0.0846, against RANGE/UP/DOWN all at p=0.0050, with RANGE
scoring 0.01345 vs a 0.00343 null - 4x, where the barrier label sits at 1.01x.
Hence the scan failing its own gate on every run, and its "winner" wandering
2:8 -> 3:8 -> 2:8 -> 2:4 across four runs of the same data. Excursion SIZE is
strongly measurable, so derive the geometry from that instead.

  stop   = q25 of measured ADVERSE travel   (ordinary noise does not reach it)
  target = q50 of measured FAVOURABLE travel (reached ~half the time, by
           construction, inside the horizon)

Continuous, in ATR units, superseding the enum multiples. Reachability ("target
on X% of bars, stop on Y%") and the implied break-even are printed so the choice
is auditable rather than trusted.

FIXED-POINT ITERATION, not one-shot. ComputeBarrierHorizonBars scales the
horizon with the target (first-passage time grows with the band) and the
excursions are measured OVER the horizon, so target -> horizon -> excursions ->
target is a real loop - deriving once sizes the target from travel measured
under the PREVIOUS horizon. Re-measures until the multiples move <5%, capped at
3 passes, and says so if it does not settle.

Does NOT create expectancy, and the log says as much: chance precision equals
break-even at every geometry (m/(m+k) on both sides). It buys a target the
market reaches and a stop that survives noise. Where Min_Risk_Reward_Ratio
forces a target the market rarely reaches, it WARNS rather than overriding -
the ratio is the user's risk policy, so the honest move is to state its cost.
That is the collision that once rejected 100% of setups.

Pinned in the .cfg as doubles appended AFTER this morning's two ints, so .cfg
files written earlier today still load (their length guard finds no doubles) and
a model that carries them was trained on them and never re-derives.

Also fixes a message from e5ceed6 that claimed "this model resumed from disk"
unconditionally - it printed above a "seeding era 0" line on a brand-new model,
because the branch fires whenever the cache is not built, which is equally true
before a fresh model's first prebuild. A diagnostic that misreports its own
trigger is worse than one that says nothing: it gets quoted back as evidence.

FORCES A FULL RETRAIN (labels change).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:06:25 -04:00
AnimateDread
2c78f3b90d diag: is "optimal SL/TP" learnable? Score the features against excursions
Proposed direction: train the net to predict entry/SL/TP that maximise return
and minimise drawdown, rather than to classify direction. Before rebuilding a
head, measure whether the target is learnable at all.

That question splits into two that behave nothing alike:
  HOW FAR price travels (MFE/MAE) - essentially volatility, and volatility
    clustering is about the most robust regularity in markets.
  WHICH WAY it goes first (the asymmetry) - direction, which is what every
    noise-floor verdict in this project has been about.
Expectancy comes ONLY from the second. The first buys position sizing and
drawdown control - worth having under prop-firm limits, but not an edge: exit
management on RANDOM entries already moved the payoff ratio 0.92 -> 5.72 with
expectancy FLAT.

Crucially this is NOT already answered. Every MI figure here scored the
triple-barrier label, i.e. one specific question at one fixed geometry. A
noise-floor result there says nothing about whether excursion MAGNITUDE is
learnable - different target, different answer.

Four targets, and the verdict is the CONTRAST, printed explicitly because the
dangerous misreading of "UP clears" is "we can predict profitable trades":
  RANGE (up+dn)  - realised volatility, included as a POSITIVE CONTROL that
                   SHOULD clear. Every prior verdict here lacked a control
                   expected to pass; a range target at the floor indicts the
                   measurement, not the market.
  UP / DOWN      - MFE / MAE.
  ASYMMETRY      - up-dn, the only one that can pay.

Collected inside the walk the label already does (one max, one min per bar).
The early-out when both barriers resolved is GONE: it would have truncated the
excursions at whichever bar tripped the last barrier, making the measurement a
function of the CURRENT SL/TP - the circularity this is trying to escape. The
loop was already bounded by the horizon, so only the average cost moves.

Discretised into 3 EQUAL-FREQUENCY bins, so every downstream piece (block
permutation, null, p-value) is reused unchanged. Equal-frequency because MFE is
fat-tailed and fixed-width bins would put nearly every row in bin 0; it also
pins H(Y) at ln(3)=1.099 for all four, making them comparable to each other and
to the barrier label's ~1.02 instead of confounded by class balance.

Two bugs fixed in this code before it ever ran, both of which would have
produced a plausible quiet wrong answer rather than an error:
  - TripleBarrierLabel early-returns on invalid ATR/close BEFORE the point the
    accumulators were reset, so one bar's excursions would be cached under
    another bar's index. Cleared at the top now, ahead of every return.
  - An unresolvable bar is still flagged as labelled but carries excursions of
    exactly 0. Under equal-frequency binning a block of identical zeros drags
    the lowest cut onto zero and a third of the sample lands in one
    uninformative bin - a depressed score that reads as "not predictable", a
    false negative in the direction that would wrongly kill the idea. Rows
    where both excursions are zero are dropped; price cannot travel zero both
    ways over a whole horizon.

Read-only diagnostic. No topology or label change: no retrain of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:22:41 -04:00
AnimateDread
3482b6c238 feat: entry/SL/TP stop being inputs - the barrier geometry is measured
Three enums left the Inputs tab. They were three things a user had to pick and,
in the tester, three more axes for a genetic optimization to overfit.

Entry_Multiplier is pinned to MARKET. Its pending modes place the entry at a
LEVEL while the rest of the pipeline measures from the bar open - the exact
mismatch that manufactured the +0.097 R "retail fade" result later retracted as
a fill artifact. This codebase's fill model cannot honestly simulate a pending
entry, so it is no longer offered.

SL_Mode/TP_Mode become a STARTING pair. ReportBarrierGeometryScan now ADOPTS its
winner instead of printing "set SL_Mode/TP_Mode to X and retrain":

  - only when it clears the family-wise gate from 04ee2e1 (beat the null of the
    MAXIMUM, not merely the incumbent). This is why that gate had to land first:
    without it, removing the inputs would hand a noise-picked geometry direct
    control over the training target with no human in the loop - strictly worse
    than the input it replaced. On SP500 H1 today it does NOT clear (p=0.1463),
    so 2:6 is what you get - now chosen by measurement rather than assumed.
  - only at m_eraCount == 0. Relabelling a partly-trained net moves the target
    out from under weights already fitted to the old one.

THE GEOMETRY LEFT THE WEIGHTS-FILENAME HASH, because it is now measured. Same
rule that moved the horizon and the derived topology values out: a filename
keyed on a measured quantity changes the moment the measurement does - a few
more bars shift which pairing wins - and the EA then looks for a file that does
not exist, starts from era 0 and orphans a trained model silently. It is PINNED
IN THE .cfg instead: appended at the end (the only backward-safe change),
length-guarded like the 2026-07-30 derived pair, and ADOPTED on load rather than
compared, so a trained model keeps the barriers it actually learned and never
re-measures.

Two traps closed while wiring it, neither of which announces itself:

  - m_barrierHorizonResolved latches the horizon ONCE PER PROCESS. Adopting 2:8
    (wants ~192 bars) after it settled for 2:6 (128) would label the new target
    against the old ceiling - the truncation fixed in 168422f, where every model
    learned "target within 128 bars" while the EA holds to SL/TP. It lands in
    Neutral, not in the timeout counter watching for it. Unlatched on adoption,
    along with the label cache the old barriers filled.
  - the .cfg adopt runs at init, before the horizon latches and before any label
    is computed, so a resumed model has its pinned pair in place first. Verified,
    not assumed.

FORCES A FULL RETRAIN: the fingerprint change orphans every existing .nnw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:39:30 -04:00
AnimateDread
9e1c72aacc fix: make the indicator tuner actually measure, and gate what it installs
ROOT CAUSE of the zero spread measured on SP500 H1 2026-08-07 (all 17 candidates
returned exactly 0.00359 nats): the tune loop re-inits the indicators and then
scores, with no RefreshData() between.

ReInitADIndicators() does its part - Create() builds a NEW handle carrying the
new parameters, and the feature cache is flagged stale so features really are
recomputed. But BufferTempDataCompute() reads the CIndicatorBuffer objects, and
only Refresh() copies data out of a handle into those. So every candidate was
scored on values still held from the PREVIOUS handle. My earlier guess in the
diagnostic ("suspect the feature cache") was wrong: the cache invalidation works.

Two things land together, because neither is safe alone:

1. RefreshData() after the re-init, so a candidate is scored on its own features.
2. A SELECTION GATE on the install. bestScore is a MAXIMUM over candidates, and
   the maximum of N draws from a null beats its incumbent almost every time - so
   "it beat the incumbent" installs noise. This selector is the highest-stakes of
   the three found in this audit because it ACTS: it overwrites the user's
   configured indicator settings and forces BuildFreshTopology(), so the network
   then trains on whatever the noise picked. Fixing (1) without (2) would have
   made a dormant bug actively harmful.

The gate draws the winner's own permutation null once, then corrects the p-value
for having chosen it out of N with Sidak: p_family = 1 - (1-p)^N. Sidak rather
than the max-of-N resample used by the geometry scan because each candidate here
has a DIFFERENT feature set, so their draws cannot be pooled; Sidak needs only
the one null. Exact under independence, mildly anti-conservative under positive
dependence - stated in the comment rather than hidden. A rejected winner restores
the configured settings, which best[] cannot do since the descent mutates it.

Also reports the least-ready tunable handle's BarsCalculated(). IndicatorCreate()
calculates asynchronously, so if the spread is STILL zero the handles simply are
not done and the tuner needs to yield between candidates rather than score them
back to back - a state machine like the label prebuild. That distinction is now
readable from the log instead of requiring another guess.

No input, topology or label change: no retrain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:31:06 -04:00
AnimateDread
e5ceed6466 fix: MI diagnostics never ran on a resumed model - the stated intent was never achieved
A comment above the diagnostic branch says it "runs even when the sweep does
not: on a resumed model ... tying it to that gate meant the only way to see the
answer on a running model was to delete the model."

It does not. Moving the diagnostic out of the tuner's gate left it behind
m_labelCachePrebuilt, which has the same effect: the eager label pre-scan runs
only on a FRESH start, because a net loaded from disk labels lazily per bar. So
on a resumed model the flag is false forever and the whole MI block - headline,
positive control, alignment scan, lag profile, geometry scan, winner test, and
the auto-tune line - silently never runs.

Measured on SP500 H1 2026-08-07: attached at era 271, still nothing by era 314,
zero MI lines in the day's log, and the only "label cache pre-built" entry
predates the attach. It also explains the shape of every capture on 08-05/06:
each one came directly after a weights reset. The situation the comment was
written to eliminate is exactly the situation that persisted.

So drive the pre-scan when it is the only thing missing. Safe on a trained net:
its one fresh-net side effect, pushing the output-layer bias toward the dominant
class, is already gated on m_eraCount == 0, and the advance gate in Train() sits
ABOVE if(!m_trainRunActive), so the era loop keeps its state - training pauses
for the scan (~1s at 38k bars) and continues from where it was, not from 0.
Announced only on a start that actually armed, since StartLabelCachePrebuild()
returns unarmed when history is not ready and is retried per bar event.

NOT sampled from the lazily-filled cache instead: BuildMiSample skips bars with
no cached label, so that would score whichever subset training happened to have
visited - a biased subsample presented as a measurement, which is the failure
this diagnostic exists to catch.

Also corrects a claim in 0d58923's comment. It argued four consecutive "no
improvement" runs were ~1-in-100,000 evidence the indicator tuner is inert, by
multiplying 5.6% across four runs. They are not independent trials: the MI
scorer is deterministic and all four covered nearly the same bars, so an
incumbent that is the maximum on this data is the maximum on every run. One
~1-in-18 observation with three correlated repeats, ~5.6% - unremarkable. The
same independence assumption that made the uncorrected lag profile star four
lags. The candidate-spread line stands: it settles inert-vs-live directly.

No input, topology or label change: no retrain. Training in flight stays valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:08:01 -04:00
AnimateDread
0d5892357b diag: report the indicator tuner's candidate spread - "no improvement" is ambiguous
Auditing the other best-of-N scans after cccf94f turned up a third instance of
the same pattern, and this one is worse than the two already fixed: the geometry
scan and the lag profile PRINT a row, whereas TuneIndicatorsByFilter INSTALLS
its winner (Unflatten + ReInitADIndicators) and the caller then calls
BuildFreshTopology(), so an unguarded maximum changes the feature vector the
network trains on.

It has no null of any kind. But before adding one, the logs say something a
noise-driven best-of-N cannot: 2026-08-05/06, four consecutive runs, 17
candidates each, every one "no improvement" with start and best identical to
4dp. The maximum of 17 draws from a noise distribution beats its incumbent
about 94% of the time, so 4/4 is on the order of 1 in 100,000.

Two readings fit and they want opposite responses:
  - INERT: trial scores come back identical to the incumbent because the
    parameter change never reaches the scored features (suspect the feature
    cache surviving ReInitADIndicators), so `sc > bestScore` can never fire.
    That is a dead code path, and gating it would be decorating a corpse.
  - LIVE and correctly finding nothing: then it needs the family-wise gate.

The current log line cannot separate them, so add the number that can: the span
of the candidate scores, with an explicit ZERO SPREAD callout naming the likely
cause. Also widened the MI figures from 4dp to 5dp - at this scale 4dp rounds
the entire effect away.

No gate yet, deliberately: measure which failure this is, then fix that one.

Read-only diagnostic. No input, topology or label change: no retrain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:59:10 -04:00
AnimateDread
cccf94f9ca fix: correct the lag profile across lags too - it contradicted itself
3271f1e tested each of ~21 lags against its OWN null at alpha 0.05 and starred
whatever cleared. That is about one false positive per run before any signal
exists, and because neighbouring lags share nearly their entire feature window
the false positives arrive in CLUSTERS that read like a hump.

It did exactly that on SP500 H1, twice in one afternoon on identical data:

  13:55  nothing clears at any lag       headline MI p=0.4478
  16:22  k6/k10/k12/k16 starred,         headline MI p=0.8756, observed
         "information survives to lag 16"   BELOW its own null mean

Same 31 features, same 2009 samples, same 287 blocks, cross-asset absent in
both - so this was not two different measurements. Non-replication on identical
data is the signature of an uncorrected multiple comparison, and acting on the
second run would have pinned the lookback to 17 off noise.

Galling detail: 04ee2e1 had just added exactly this correction to the
barrier-geometry scan one function below. The rigorous bar went on the report
with 6 candidates and the naive one stayed on the report with 21.

So the lag profile now uses the same construction as the geometry winner test:
one draw from every lag, keep the largest, repeat; a lag clears only by beating
that distribution. Draws centred leave-one-out to match how the observed excess
is centred. Independence across lags overstates the spread of the maximum
(neighbours share their window), so it errs toward rejecting.

Also: the positive branch now says to re-run before acting, because one run of
this report has demonstrably not been a result; and MI_LAG_MAX_PROFILE caps the
retained-draw matrix rather than trusting a derived m_historyBars.

Read-only diagnostic. No input, topology or label change: no retrain, and a
training run already in flight stays valid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 16:28:57 -04:00
AnimateDread
04ee2e113a fix: gate the barrier-geometry winner on a family-wise null, not its own
The scan ends by printing "set SL_Mode/TP_Mode to <winner> and retrain".
That advisory fired on `bestExcess > cfgExcess * 1.5` - a ratio between two
numbers, with no test that either is distinguishable from zero.

bestExcess is a MAXIMUM over the eligible candidates. The maximum of several
draws from a null sits well above any single draw from it, so a max-shaped
statistic tested against a single-candidate null crowns a winner on noise
almost every time. On SP500 H1 the winner is 2:8 at +0.00081 nats - and the
lag profile committed in 3271f1e measures the pure-noise swing on this exact
data at +/-0.0004, peaking at +0.00042 with nothing clearing its own null at
any lag. The advisory was one ratio away from talking us into relabelling and
retraining all four topologies to chase that.

So build the null OF THE MAXIMUM: retain every candidate's permutation draws,
take one draw from each candidate, keep the largest, repeat. The winner must
beat that distribution.

- draws centred LEAVE-ONE-OUT, so a draw is centred by a mean excluding it -
  exactly how the observed score is centred. Centring a draw by a mean that
  contains it shrinks it toward zero and would deflate the null.
- only ELIGIBLE candidates enrol: the family the max was taken over is the
  family to correct for, and a clamped or sub-minRR pairing can never win.
  rrOK hoisted above the draws for this.
- draws per candidate 20 -> MI_GEOMETRY_PERMUTATIONS (40): they now have to
  resolve an upper tail, which is where 20 draws are thinnest.
- MI_GEOMETRY_ALPHA 0.05, stricter than the lag profile's: a wrong lookback
  costs input width, a wrong geometry costs a full retrain from era 0.

Independence across candidates overstates the spread of the max (the real
candidates share features and overlapping label windows), so the gate errs
toward rejecting - the safe direction when passing costs a retrain.

Read-only diagnostic. No input, topology or label change: no retrain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 14:04:16 -04:00
AnimateDread
3271f1ea93 diag: MI feature-lag profile - close the blind spot in every MI verdict so far
BuildMiSample samples features from ONE bar. So every "MI is at the noise
floor" result this codebase has produced - including yesterday's p=0.18 on
SP500 H1 - described the ENTRY BAR's 31 features only, while the network is
fed 20 bars of them. If information lived at lag 7 and not lag 0, the report
would have said "no signal" while the model could still learn. The diagnostic
we have been making decisions on had a blind spot exactly the width of the
input vector.

Adds a FEATURE-side offset to BuildMiSample, which is not the same thing as
the existing labelBarOffset and is not interchangeable with it. Shifting the
LABEL changes which trade is predicted, so at any non-zero offset the
features sit inside the labelled window and the score is lookahead - that is
precisely what the alignment scan measures and correctly reports (4.7x more
knowable 5 bars into a 128-bar window). Shifting the FEATURES keeps the label
pinned to the entry bar, so every row stays causal.

ReportFeatureLagProfile() then scores k = 0..historyBars against the same
block-permutation null and reports the deepest lag that clears it - the
lookback the data supports, versus the 20 that was picked by hand and never
measured. The null is redrawn PER LAG: finite-sample MI bias moves with the
realised class counts and bin occupancy, and different rows survive the
validity checks at each lag, so one shared floor would be right for lag 0 and
wrong everywhere else. Draw count is reduced accordingly (40, not 200) since
cost is draws x historyBars; this figure decides a lookback, never a trade.

MiShiftPad now also covers historyBars, keeping the fixed-pad invariant that
makes two builds comparable row by row.

Read-only - no input, topology or label change, so no retrain. Both builds
0/0. Build tag lag-profile-v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 13:51:39 -04:00
AnimateDread
8c5ea639ee feat: extend ADWyckoffEventStream with new range-lifecycle parameters and update related features 2026-08-02 17:08:48 -04:00
AnimateDread
8ccbddb051 Add new research scripts for trading strategy analysis
- Implemented sqx_audit.py to audit StrategyQuant X trade lists, focusing on performance metrics and cost analysis.
- Created sqx_portfolio.py to evaluate portfolio performance based on uncorrelated components and their impact on risk and return.
- Developed swing.py to analyze cost ratios across different holding periods and assess swing trading structures.
- Introduced test_management.py to investigate the effectiveness of exit rules on random entries and their impact on expectancy.
2026-08-02 12:25:20 -04:00
AnimateDread
f1b7dcf7f3 fix: correct MI sample alignment and improve BN weight diagnostic report
The MI sample builder used `MathAbs(labelBarOffset)` as a padding, causing rows from offset and non-offset builds to be paired with a double shift. This broke the positive control, failed the 5× gate, and voided all reported mutual‑information figures. Replace with the fixed `MiShiftPad` constant to ensure builds enumerate the same set of bars and row-k alignment is preserved.

Add `BatchOptionsTotal()` to `CNeuronBatchNormOCL` and split the packed BN weight array in the learning report into separate norms for the outgoing dense matrix, gamma, beta, running statistics, and Adam moment buffers. This turns an ambiguous single‑norm reading into precise diagnostics that distinguish weight divergence from scaling issues.
2026-08-02 08:12:47 -04:00
AnimateDread
5f647ba5db fix: improve error messages and suppress false sharing-violation logs
- BufferDouble: replace hardcoded "DirectML/CPU-DLL" with dynamic backend name
  and add buffer index/element count to all error prints for easier debugging.
- NetPersistence: distinguish missing file from transient lock by probing
  FileIsExist before logging, eliminating false "sharing violation" warnings
  when no saved model exists on first run.
2026-08-02 01:09:18 -04:00
AnimateDread
3459db887b infra: bid/ask M1 bars and an order engine that makes the fill bug unavailable
Three results today were invalidated by the same family of error: a price level
used as an entry while the outcome was measured from somewhere else. The defence
is not vigilance - it is an interface where the mistake cannot be expressed.

bidask.py  M1 bars carrying SEPARATE BID and ASK OHLC, built from the tick
  stream in one parallel pass. Every existing bar file stores MID, which is fine
  for measuring returns and useless for simulating orders, because no order ever
  executes at the mid. Mid bars force the spread to be bolted on afterwards as an
  average - the approximation that let today's artifacts through. With both sides
  carried, the spread is whatever it actually was, including the overnight and
  news blowouts an average hides. Fails loudly if ask < bid.

fills.py  owns the ENTIRE trade lifecycle; no test may open a position any other
  way. Longs enter at the ask and exit at the bid, shorts the reverse. A buy stop
  triggers on the ask and fills at the trigger price OR the bar's open if the bar
  gapped past it, which is where real slippage comes from. Limits fill on the
  opposite side and a gap is capped in their favour.

  The invariant that was violated: THE OUTCOME CLOCK STARTS AT THE FILL BAR. The
  fill index IS the start index - they are the same variable and cannot diverge.
  That is what went wrong before, and it is now unrepresentable rather than
  merely discouraged.

  Same-bar ambiguity is REPORTED, not assumed away: every result carries the
  fraction of trades decided by a bar containing both barriers, alongside fill
  rate and unresolved rate. A resolution-limited result now says so itself.

M1 rather than raw ticks is a deliberate, stated bound: 513M ticks per symbol is
~8 GB packed and four symbols will not fit in memory, while M1 keeps it at ~500 MB
and cuts the residual ambiguity 60x versus H1. It is an approximation with a
visible error bar, not an exact simulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 00:08:30 -04:00
AnimateDread
82cdbb40df research: Wyckoff context REPLICATES on a second, independent trigger
Applies the same five-trace context score from c998d65 to a completely
different entry - the LPS retest instead of the shakeout - as a check that the
dose-response was not specific to one setup.

  shakeout trigger   slope +0.0464 R/trace   t +2.16   8/8 cells positive
  LPS trigger        slope +0.0425 R/trace   t +3.18   7/8 cells positive

Two unrelated triggers, 31,000 non-overlapping trades, same magnitude to within
10%, same sign in 15 of 16 cells. This is the strongest confirmed result in the
whole programme: reading past structure to qualify current structure has real,
measurable predictive content. Villahermosa is describing something that exists.

AND IT IS STILL NOT ENOUGH. The buckets:

    1 agree  -0.287     3 agree  -0.151
    2 agree  -0.206     4 agree  -0.157

Context is a MODIFIER worth ~+0.045 R per agreeing trace. Every trigger it can
modify starts between -0.15 and -0.33, and only five traces exist, so full
confluence still lands short of break-even. Reaching zero would need ~7.

One cell goes positive - EURUSD H1 at 4 agreeing traces, +0.14 on n=133 - and it
is 1 of 40, on the smallest bucket, in the instrument with the lowest costs. That
is the exact shape of the three artifacts already retracted today, so it is
recorded as noise unless a fill-correct simulator says otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:57:30 -04:00
AnimateDread
c998d655ee research: Wyckoff CONTEXT has real predictive content - 8/8 positive slopes
Earlier tests fired on the shakeout alone, which is not the method. Book 2 2.3
treats it as the third of four cumulative traces and reads the structure's own
history first. This scores all of them, oriented to the shakeout's direction:

  1  Phase A test location (upper vs lower half of the structure)
  2  Phase B test location
  2b STRUCTURAL FAILURE - after the Phase B test, did price fail to reach the
     opposite extreme
  4  effort/result on the shakeout bar (close position + volume vs range average)
  7.1 higher-timeframe context - is the larger move in the shakeout's favour

Conditioning on agreement shrinks the sample and multiplies the ways to slice
it, so the test is NOT 'find the combination that works'. It is the one
pre-specified prediction the books make and mining does not: expR must rise
MONOTONICALLY with the number of agreeing traces. One slope, no threshold to
tune, no best cell to pick.

POOLED (16,234 non-overlapping trades):

    0 traces  -0.332      3 traces  -0.114
    1 trace   -0.214      4 traces  -0.117
    2 traces  -0.184

  slope +0.0464 R per agreeing trace, t +2.16
  per-symbol slopes POSITIVE IN ALL 8 CELLS (p ~ 0.004 on sign alone)

So the context logic is real and measurable - it is not folklore. But the base
trade is in too deep a hole for it to matter: full confluence still returns
-0.117, and reaching break-even would need ~7 agreeing traces when only 5 exist.

The useful reading is that context is a MODIFIER worth about +0.05 R per trace,
which is only interesting when bolted to a trigger whose base expectancy is
already near zero. The shakeout's is not, because its structural target sits
4-6R away and is rarely reached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:56:05 -04:00
AnimateDread
9a4ae635e0 research: the two canonical Wyckoff trades tested whole - both negative
Completes the programme on both books. Entries are MARKET ORDERS at a bar's
open throughout, so the fill-timing artifact that invalidated the last round is
designed out rather than remembered. Benchmark is analytic: entry, stop and
target fixed at entry means a driftless market gives expR = 0 exactly.

1. SPRING / UPTHRUST (book 1 ch.18, the event 'all Wyckoff operators wait for').
   Pierce of a COMPRESSION-QUALIFIED range edge, close back inside, stop beyond
   the shakeout extreme, target the far side of the range.

   30 cells across 4 symbols x M15/H1/H4. Reward-to-risk averages 4-6:1, so the
   break-even win rate is only 15-20%, and it still loses nearly everywhere:
   M15 all four symbols -0.15 to -0.24 with 0/4 folds positive. Best cell is
   EURUSD H1 climactic-volume +0.302 at t +2.27, which over 30 cells is inside
   the family-wise band.

   The books' volume requirement was applied - climactic (>1.5x range average)
   and quiet (<0.8x) shakeouts scored separately. Neither rescues it.

2. LPS / LPSY, the test-after-breakout, and book 2's A/B (5.7.1, 5.8.3): it
   claims the retest should be awaited at the VOLUME PROFILE level, not the
   price edge. Same breakout, same stop, same 2R target, only the location
   differs:

     retest at            typical expR
     A price edge         -0.041 .. -0.315
     B value-area edge    -0.128 .. -0.413
     C range VPOC         -0.129 .. -0.506

   24/24 cells negative, and A > B > C in ALL EIGHT symbol/timeframe
   combinations. That monotone ordering is not noise, and it inverts the book's
   recommendation. Mechanism is adverse selection: the VPOC sits deep inside the
   old range, so a retest that reaches it is disproportionately a breakout that
   has already failed. The deeper the level you wait at, the more your fills are
   selected against you.

   Practical consequence: the volume profile is real (levels beat distance-matched
   placebos at z +3 to +7.8) but using it to LOCATE ENTRIES makes this trade
   worse, not better.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:34:35 -04:00
AnimateDread
79f108ab84 research: RETRACTION - the retail-fade edge was a fill-timing artifact
The +0.097 R EURUSD result in c9b489e is wrong. So is the +0.108 R pooled edge
in c2dd9eb and the selection model in 821f16d, which used the same labels.

THE BUG. The mirror test entered at the retail trigger price e2 - a STOP order
level - but started the outcome race at the OPEN of the fill bar. Price at that
open is on the far side of e2 by construction; that is why the order is a stop
order. So the race began before price had reached the entry, which handed the
fade a free run toward its target and pushed its stop further away than it
really was. Retail's side carries the same bias with the sign reversed, so the
DIFFERENCE - which is exactly how 'edge' was computed - was inflated twice over.

Found by generalising the trigger to swing-extreme breakouts, per the user's
suggestion. That version returned +0.7 R at t +110, which is not a result, and
it has the identical structure: enter at a level, measure from the bar open.

WITH AN HONEST INTRABAR FILL (first M5 bar that actually trades at the entry),
every EURUSD cell inverts:

                  bar open   honest fill
  H1  pin          +0.0953      -0.0096
  H1  pin          +0.0559      -0.0573
  H1  inside       +0.0612      -0.0395
  M15 pin          +0.0407      -0.0135

The 4/4 walk-forward held because the bias was present in every fold. A
walk-forward validates against regime change, not against a broken fill model.

The tell was there and I walked past it: sweep_entry() was the ONE test that
modelled the fill properly, and it was the ONE test that came out negative. When
one arm of a suite disagrees with the rest, check what it does differently before
believing the majority.

So the standing conclusion returns to what it was: retail setups are close to a
coin flip that pays the spread, and there is nothing in them to harvest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:17:55 -04:00
AnimateDread
821f16df8b research: model-selected retail fades hold up out of sample (after killing a big leak)
Reframes what the net is asked. Direction is the one thing the data says is
barely predictable; but the fade of retail pin/inside setups has a MEASURED
gross edge of ~0.139 R and a cost of spread/stop that varies 5x across
instruments, sessions and vol regimes. So the answerable question is selection:
spend the edge only where it survives.

  label     realised R of the fade (what actually pays)
  features  28, all knowable at entry, incl. the volume-profile levels
  split     CHRONOLOGICAL train/val/test; threshold picked on val, frozen for test

TEST BLOCK 2022-08 .. 2026-07, 63,901 candidate trades, keep top 10%:

  mean R  +0.1005  vs  -0.0616 for taking everything
  by year +0.140 +0.075 +0.098 +0.105 +0.105   -> 5/5 positive
  with the COST FEATURE REMOVED: +0.0800, still 5/5 positive

That last control matters: the model is not merely learning 'skip wide
spreads'. Something in the setup geometry, session and level structure carries
signal beyond the cost.

HONEST SIGNIFICANCE. The naive t of +8.06 is not believable - with an 8-day
horizon these trades overlap heavily and thousands share one price path. On a
strictly non-overlapping subset (163 independent trades) it is +0.123 R at
t +1.61; without the cost feature, +0.257 at t +3.45. The overlap filter applies
the H1 horizon to M15 trades too, so 163 is a conservative floor and the true
independent count is higher. Suggestive, not settled.

THE LEAK THIS RUN NEARLY SHIPPED. First version scored +0.53 R on the held-out
block, t +54. Bar-derived features were read at the FILL bar i2, but the order
fills intrabar and the outcome race starts at the first M5 bar inside i2 - so
i2's close, tick count and realised volatility are not knowable at entry. The
model was seeing how the bar it entered on turned out. Second lookahead of this
hunt (the first was worth +0.15 R in the sweep test).

RULE: a clean chronological split does NOT protect against lookahead. The split
was honest and the features were not. Any feature indexed at the entry bar must
be re-derived from the bar before it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:15:00 -04:00
AnimateDread
c9b489e5d8 research: the retail fade DOES clear cost on EURUSD - correcting the earlier verdict
The previous commit pooled four instruments with very different spread-to-stop
ratios and concluded the edge never beats the cost. That was too broad. Per
cell (48 cells, M5 path, k=1), 8 clear - and they are not scattered:

   EURUSD  H1  pin     spread/stop 0.042   edge +0.160  cost 0.063  -> +0.097 R
   EURUSD  H1  pin                 0.045        +0.127        0.067     +0.060
   EURUSD  H1  inside              0.047        +0.123        0.060     +0.063
   EURUSD  H1  inside              0.050        +0.102        0.062     +0.040
   EURUSD M15  pin                 0.074        +0.141        0.100     +0.042
   EURUSD M15  pin                 0.072        +0.140        0.099     +0.041

Every clearing cell is on the tightest-spread instrument. XAUUSD carries the
same gross edge (+0.09 to +0.135) and never clears, because its cost is 3x.
That is the mechanism predicting where the effect should survive and being
right - the opposite of the stop-run case, which inverted.

WALK-FORWARD, 4 chronological folds: 6 of 8 hold at >=3/4. EURUSD H1 pin short
side is +0.116 / +0.061 / +0.143 / +0.067 across 23 years, 4/4.

WIDENING THE STOP still says what it said: EURUSD H1 pin goes +0.078 (m=1) ->
+0.028 -> +0.009 -> +0.017 -> -0.003 (m=5). The gross edge collapses ~15x while
the stop widens 5x, so this is NOT drift - it is reversion inside roughly one
setup-risk of a stop order filled at a local extreme. It is only tradeable at
the tight stop, which is exactly where cost bites hardest.

WHAT IS NOT MODELLED, and it decides this: commission and stop slippage. Gross
edge is ~0.139 R = ~2.4 pips on a 17.3-pip stop, against 0.75 pips of spread.
That leaves ~1.6 pips of headroom for commission plus slippage before it is
gone. A demo forward test measuring both is the next step, not more history.

Also fixes a LOOKAHEAD found in the sweep-entry test: the protective stop was
anchored to the low of the very bar that filled the limit order, which is not
known until that bar closes. It was worth ~+0.15 R - larger than any real effect
here - and it inflated the placebo equally, which is how it was caught. With it
removed, buying at retail stop levels is no better than buying at an arbitrary
level the same distance away: the 'stops are a farmable magnet' claim fails its
own control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:33:11 -04:00
AnimateDread
c645c98f31 chore: keep the reference PDFs out of the repository
40 MB of third-party copyrighted books sit in references/ so the research
scripts can read them. Untracked is not the same as safe: one broad
`git add -A` puts them in history permanently and on a public remote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:18:18 -04:00
AnimateDread
c2dd9eb6aa research: retail setups ARE anti-predictive - and the edge dies with the cost
Tests the user's thesis directly: if price is unpredictable, trade against the
people predicting it badly. Implements the three mechanical setups from 'How To
Day Trade Forex For Profit' ch.5 with their DOCUMENTED stop rules, so retail
stops are located exactly rather than by proxy.

THE MIRROR TEST. Retail's trade and its exact mirror, priced under identical
rules. Both sides pay the same spread and suffer the same same-bar tie
convention, so those cancel in the difference and double in the sum:

    edge = (mirror - retail)/2      cost = -(mirror + retail)/2

  pin     EDGE +0.108 R   COST +0.143 R
  inside  EDGE +0.068 R   COST +0.140 R
  engulf  EDGE -0.001 R   COST +0.095 R

So pin-bar and inside-bar setups really are anti-predictive - the first
confirmed directional edge in this project. Engulfing is a pure coin flip whose
loss is entirely the spread, i.e. money already gone to the broker.

Stable across three conventions: H1 bars pessimistic ties, M5 path pessimistic,
M5 path optimistic. Re-walking the barriers on M5 CUT the cost (0.195 -> 0.143)
and RAISED the edge (0.078 -> 0.108), so the coarse-bar convention was masking
the effect, not manufacturing it.

THEN THE TEST THAT KILLS IT. Cost in R is spread/stop-distance, so widening the
stop divides it. If the edge is directional drift it survives. Fade expR by stop
multiple (pin, k=1, 122k trades):

    m=1.0  cost 0.146  expR -0.045     implied edge +0.101
    m=1.5  cost 0.097  expR -0.067                  +0.030
    m=2.0  cost 0.073  expR -0.065                  +0.008
    m=3.0  cost 0.049  expR -0.051                  -0.002
    m=5.0  cost 0.029  expR -0.040                  -0.011

The edge decays exactly as fast as the cost, then inverts. It was never drift:
it is reversion against a stop order filled AT a local extreme, and it lives
within one bar-range of the entry - the same short-horizon reversal the tick-flow
work already measured, meeting the same fate.

Also in this commit, the volume-profile claims from Wyckoff 2.0:
  MAGNET   all 10 tests positive vs a distance-matched placebo, z +3.0 to +7.8,
           family-wise bar 2.79 - but the effect is +0.15 to +0.59pp on a ~74%
           base rate, i.e. ~0.01 R.
  REACTION naked VPOC and VPOC reject +0.71 to +0.86pp (z to +5.65); value-area
           edges null or negative; HVN/LVN marginal.
  80% RULE dead. 27.8% traversal against a 29.0% martingale benchmark. Acceptance
           nearly DOUBLES the raw rate (14.1% -> 27.8%) and the benchmark doubles
           with it - every bit of the apparent improvement is geometry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:18:01 -04:00
AnimateDread
8e6525d819 research: Wyckoff's law of cause and effect is real but SUBLINEAR
The 1:1 range projection is the target rule both books recommend (book 1 ch.8
discards point-and-figure counting as too subjective and keeps the vertical
projection). Tested as a complete trade on 4 symbols x M15/H1: enter on the
range breakout, stop at the far side of the range, target k x risk.

A driftless market gives P(win) = 1/(1+k) and expR = 0 at EVERY k, so the
benchmark here is analytic - no permutation null needed. Result: expR sits on
that benchmark at every k on every symbol. Target placement does not move
expectancy, which is what a martingale already said.

But the law itself is measurable, and it is not 1:1. Regressing
log(MFE) on log(range height) with log(ATR) as a FREE regressor (a shared ATR
denominator correlates the errors and biases the exponent towards the
hypothesis, so it cannot be used to argue against it):

  b = 0.10 .. 0.92, centred ~0.6
  b = 1 rejected in 5 of 8 at >2sd, never significantly above 1
  b = 0 rejected in 7 of 8

So a bigger cause does produce a bigger effect - sub-proportionally. The 1:1
projection systematically over-reaches after a large consolidation and
under-reaches after a small one. Median travel in risk-multiples falls
monotonically across height quartiles in 8 of 8 runs.

Also adds the volume-profile machinery the second book is built on and which
nothing in the EA has: tick-level volume-at-price on a fixed absolute grid,
per-session VPOC/value area by the standard Market Profile walk, naked VPOCs,
and HVN/LVN from a rolling causal composite.

Two biases are left in deliberately, both against the hypothesis: a bar
spanning stop and target books the loss, and spread is charged on entry and
both barriers. Unresolved trades are marked to market at the horizon rather
than discarded - discarding them deletes slow winners and manufactures a
false deficit at large k.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:36:14 -04:00