Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions dev/status/resistance-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ load-bearing; binary grade → searchable weight; kill the 5h armed-run wall).
bump + one full warehouse rebuild. Gate to build: lever (c)'s floor
surface first; build (f) only if the floor verdict shows the mechanism
wants real age structure (e.g. optimal floors regime-unstable).
**CODE LANDED 2026-07-19 (PR `feat/resistance-v2-age-bands`, default-off):**
schema `Res_hist` is now 80 band-major cells (4 age bands × 20 price
buckets, `Snapshot_schema.n_age_bands`/`n_hist_cells`); the pipeline
accumulates the 520-week histogram into age bands; `Resistance_supply`
collapses bands via four `config.band_weight_*` fields
(`[@sexp.default 1/1/1/0]`, Overlay_validator axes) at score time — default
weights `[1;1;1;0]` reproduce the pre-lever-f age-blind 130w histogram
bit-identically (pinned by `test_default_collapse_sums_recent_bands`). The
warehouse reader detects v3 (20-column) vs v4 (80-column) width and packs
v3 into the youngest band, so **existing v3 warehouses keep scoring
identically with NO rebuild** (`hist_bands_of_legacy`). The v4 warehouse
rebuild is DEFERRED pending the bundle verdict — no rebuild in this PR.
4. dedup-v2 warehouse deletable (v3 certified bit-identical:
`scenarios-2026-07-16-131756` baseline = Run D to 13 decimals).

Expand Down
78 changes: 69 additions & 9 deletions trading/analysis/weinstein/resistance/lib/resistance_supply.ml
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
open Core
open Weinstein_types

(* Age bands (lever f), youngest first, locked to [Snapshot_schema] band
boundaries [0-26w / 26-78w / 78-130w / 130-520w]. Bands 0..[_n_recent_bands-1]
union to the pre-lever-f trailing-130w histogram window; band 3 is the
130-520w extension, weighted 0 by default so scoring is unchanged. *)
let n_age_bands = 4
let _n_recent_bands = 3

type sketch = {
max_high_130w : float;
max_high_260w : float;
max_high_520w : float;
bars_seen : float;
hist : float array;
hist_bands : float array array;
anchor_close : float;
}

Expand All @@ -20,6 +27,10 @@ type config = {
insufficient_score : float;
heavy_resistance_bars : int;
moderate_resistance_bars : int;
band_weight_0_26w : float; [@sexp.default 1.0]
band_weight_26_78w : float; [@sexp.default 1.0]
band_weight_78_130w : float; [@sexp.default 1.0]
band_weight_130_520w : float; [@sexp.default 0.0]
}
[@@deriving sexp]

Expand All @@ -35,8 +46,49 @@ let default_config =
insufficient_score = 0.5;
heavy_resistance_bars = 8;
moderate_resistance_bars = 3;
(* No-op defaults (experiment-flag-discipline R1): the three 0-130w bands
weighted 1.0 and the 130-520w band 0.0 collapses the age-banded histogram
back to the pre-lever-f age-blind 130w histogram, bit-identically. *)
band_weight_0_26w = 1.0;
band_weight_26_78w = 1.0;
band_weight_78_130w = 1.0;
band_weight_130_520w = 0.0;
}

let _band_weights (config : config) =
[|
config.band_weight_0_26w;
config.band_weight_26_78w;
config.band_weight_78_130w;
config.band_weight_130_520w;
|]

(* Pack a legacy age-blind histogram (the v3 warehouse / pre-lever-f shape) into
the age-banded layout: all mass in the youngest band, the rest zero. With
[default_config] band weights ([1;1;1;0]) the collapsed effective histogram
equals [flat] exactly, so a v3 warehouse scores bit-identically. *)
let hist_bands_of_legacy flat =
let n = Array.length flat in
Array.init n_age_bands ~f:(fun b ->
if b = 0 then Array.copy flat else Array.create ~len:n 0.0)

(* Collapse the age-banded histogram into one effective per-bucket vector by
applying the per-band config weights: [effective.(k) = Σ_b w_b * bands_b.(k)].
Non-finite cells count as 0 (same guard the per-bucket scorer applies). *)
let _effective_hist ~(config : config) ~hist_bands =
let weights = _band_weights config in
let n_buckets =
Array.fold hist_bands ~init:0 ~f:(fun acc band ->
Int.max acc (Array.length band))
in
let eff = Array.create ~len:n_buckets 0.0 in
Array.iteri hist_bands ~f:(fun b band ->
let w = if b < Array.length weights then weights.(b) else 0.0 in
Array.iteri band ~f:(fun k c ->
let c = if Float.is_finite c then c else 0.0 in
eff.(k) <- eff.(k) +. (w *. c)));
eff

type result = {
score : float;
recent_weighted_bars : float;
Expand Down Expand Up @@ -110,11 +162,12 @@ let _insufficient ~(config : config) =
}

let _scored ~(config : config) ~(sketch : sketch) ~breakout_price =
let effective = _effective_hist ~config ~hist_bands:sketch.hist_bands in
let k_min =
_first_bucket ~n_buckets:(Array.length sketch.hist)
_first_bucket ~n_buckets:(Array.length effective)
~anchor:sketch.anchor_close ~breakout:breakout_price
in
let weighted, max_bucket = _recent_supply ~config ~hist:sketch.hist ~k_min in
let weighted, max_bucket = _recent_supply ~config ~hist:effective ~k_min in
let quality = _quality_of ~config ~sketch ~breakout_price ~max_bucket in
let score =
(* When the histogram holds mass at/above the breakout it speaks for
Expand Down Expand Up @@ -145,12 +198,19 @@ let is_virgin ~(sketch : sketch) ~breakout_price =
&& Float.is_finite breakout_price
&& Float.(breakout_price >= sketch.max_high_520w)

(* Closing-basis "new high ground": no weekly bar in the trailing histogram
window sits at/above the current close (every [hist] bin is 0). Robust to
the own-week-high artifact that makes [is_virgin] unsatisfiable on a
close-anchored breakout price (see .mli — AXTI 2026-01-06). A non-finite bin
fails the [= 0.0] test, so no explicit finiteness check on [hist] is needed. *)
(* Closing-basis "new high ground": no weekly bar in the trailing 130-week
histogram window (age bands 0..[_n_recent_bands-1], which union to that
window) sits at/above the current close — every recent-band bin is 0. The
130-520w band is ignored, keeping this predicate bit-identical to its
pre-lever-f 130w semantics (and to a v3 warehouse whose mass all lands in
band 0). Weight-independent by design — a structural emptiness test, not a
scoring measure. Robust to the own-week-high artifact that makes [is_virgin]
unsatisfiable on a close-anchored breakout price (see .mli — AXTI
2026-01-06). A non-finite bin fails the [= 0.0] test, so no explicit
finiteness check on the bins is needed. *)
let is_clear_of_supply ~(sketch : sketch) =
_sketch_is_finite sketch
&& Float.(sketch.bars_seen > 0.0)
&& Array.for_all sketch.hist ~f:(fun count -> Float.(count = 0.0))
&& Array.for_alli sketch.hist_bands ~f:(fun b band ->
b >= _n_recent_bands
|| Array.for_all band ~f:(fun count -> Float.(count = 0.0)))
65 changes: 53 additions & 12 deletions trading/analysis/weinstein/resistance/lib/resistance_supply.mli
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,34 @@ type sketch = {
max_high_260w : float; (** Same, 260 weekly bars. *)
max_high_520w : float; (** Same, 520 weekly bars. *)
bars_seen : float; (** True weekly-bar count available (capped 520). *)
hist : float array;
(** Trailing-130w histogram: [hist.(k)] counts weekly bars whose
mid-price lies in [anchor * 2^(k/n), anchor * 2^((k+1)/n)) where
[n = Array.length hist] buckets span one doubling. *)
hist_bands : float array array;
(** Age-banded histogram (lever f): [hist_bands.(b).(k)] counts weekly
bars of age band [b] (0..{!n_age_bands}-1, youngest first —
[0-26w / 26-78w / 78-130w / 130-520w]) whose mid-price lies in
[anchor * 2^(k/n), anchor * 2^((k+1)/n)) where [n] is the per-band
bucket count. {!analyze} collapses the bands into one effective
histogram via the [config] band weights at score time. A v3 warehouse
/ age-blind histogram maps to the youngest band with the rest zero —
see {!hist_bands_of_legacy}. *)
anchor_close : float;
(** The raw close the histogram was anchored at (the sketch row's own
close). *)
}
(** One symbol-day's sketch cells, as stored in the warehouse columns. *)

val n_age_bands : int
(** [n_age_bands] is the number of {!sketch.hist_bands} age bands (4). Locked to
[Snapshot_schema.n_age_bands] (the pure scoring layer cannot depend on the
snapshot layer, so the two are pinned equal by test). *)

val hist_bands_of_legacy : float array -> float array array
(** [hist_bands_of_legacy flat] packs a legacy age-blind histogram (the v3
warehouse / pre-lever-f 130-week shape) into the {!n_age_bands}-band layout:
all mass in the youngest band, the remaining bands zero. Under
{!default_config} band weights ([1;1;1;0]) the effective histogram
{!analyze} collapses equals [flat] bit-for-bit, so a v3 warehouse scores
identically to before lever f. *)

type config = {
proximity_decay : float;
(** Multiplicative weight decay per bucket above the breakout: supply [j]
Expand All @@ -54,6 +72,21 @@ type config = {
(** Grade derivation: max single-bucket count at/above the breakout that
classifies as [Heavy_resistance] (mirrors v1). *)
moderate_resistance_bars : int; (** Same for [Moderate_resistance]. *)
band_weight_0_26w : float; [@sexp.default 1.0]
(** Lever f: score-time weight for the 0-26w age band. Defaults to 1.0
(the pre-lever-f no-op). *)
band_weight_26_78w : float; [@sexp.default 1.0]
(** Score-time weight for the 26-78w age band. Default 1.0. *)
band_weight_78_130w : float; [@sexp.default 1.0]
(** Score-time weight for the 78-130w age band. Default 1.0. *)
band_weight_130_520w : float; [@sexp.default 0.0]
(** Score-time weight for the 130-520w age band. Defaults to 0.0 so the
band is inert; the three 0-130w bands at weight 1.0 reproduce the
pre-lever-f age-blind 130w histogram exactly. Each band weight is an
[Overlay_validator] axis (a real [config] sub-field), so the age decay
is searchable without a warehouse rebuild (experiment-flag-discipline
R1/R2). The four fields carry [@sexp.default]s so a config sexp
written before lever f (without them) still round-trips. *)
}
[@@deriving sexp]
(** [@@deriving sexp] so this config can be nested (as an [option]) inside
Expand All @@ -64,7 +97,8 @@ type config = {
val default_config : config
(** No-op-adjacent defaults: decay 0.7, saturation 8 bars, floors 0.4 / 0.25 /
0.1, [min_history_bars = 0], insufficient score 0.5, grade thresholds 8 / 3
(v1 parity). All searchable; none hardcoded at use sites. *)
(v1 parity), age-band weights 1 / 1 / 1 / 0 (collapses to the pre-lever-f
age-blind 130w histogram). All searchable; none hardcoded at use sites. *)

type result = {
score : float;
Expand All @@ -85,9 +119,12 @@ val analyze : config:config -> sketch:sketch -> breakout_price:float -> result
(** [analyze ~config ~sketch ~breakout_price] scores the overhead supply a
breakout at [breakout_price] faces.

Score composition (plan §D5):
- recent component: proximity-weighted histogram mass at/above the breakout,
saturated at [saturation_bars];
Score composition (plan §D5). The age bands are first collapsed into one
effective per-bucket histogram by the [config] band weights
([effective.(k) = Σ_b w_b * hist_bands.(b).(k)]); the rest scores that
effective histogram exactly as before lever f:
- recent component: proximity-weighted effective-histogram mass at/above the
breakout, saturated at [saturation_bars];
- horizon floors: ONLY when the histogram holds no mass at/above the
breakout but a max-high proves overhead exists, the score falls back to
[recent_far_floor] / [stale_mid_floor] / [stale_old_floor] by the age of
Expand Down Expand Up @@ -126,10 +163,14 @@ val is_virgin : sketch:sketch -> breakout_price:float -> bool

val is_clear_of_supply : sketch:sketch -> bool
(** [is_clear_of_supply ~sketch] is [true] iff the sketch is finite,
[bars_seen > 0], and EVERY [hist] bin is [0] — i.e. zero measured overhead
mass: no prior (finalized) weekly bar in the trailing 130-week histogram
window has a high above the current close whose mid-price
([(high + low) / 2]) falls at or above it. The histogram producer gates on
[bars_seen > 0], and EVERY bin of the {b 0-130w age bands} (bands [0..2],
which union to the trailing-130w window) is [0] — i.e. zero measured
overhead mass: no prior (finalized) weekly bar in the trailing 130-week
histogram window has a high above the current close whose mid-price
([(high + low) / 2]) falls at or above it. The 130-520w band is ignored so
this predicate keeps its exact pre-lever-f 130w semantics (and a v3
warehouse, whose mass all lands in band 0, gives the same answer);
weight-independent by design. The histogram producer gates on
[weekly_high > anchor] and buckets by the mid-price (see
[Snapshot_pipeline.Resistance_sketch] and its .mli — the source of truth).
This is exactly the recent-overhead mass {!analyze} scores as its histogram
Expand Down
Loading
Loading