From 27d1c8debe4ec63f72f1b6602e399a38adfd82db Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 25 Aug 2026 17:48:09 -0700 Subject: [PATCH 1/5] DEM-PRODUCER-PLAN (reactor): the planned R:R is a verified provider fact; goldens regenerated once, itemized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEM-GOV §9 DEM-PRODUCER-PLAN (owner-authorized 2026-08-25; afi-governance #55; §9 determinations D-2/D-4/D-5 in #56). Merge AFTER afi-core dem/plan-core and afi-config dem/plan-config. Source: - uss/cpjMapper.ts: the submitted plan survives as uss.plan (afi.trade-plan.v1, decimal strings, validated against the governed contract) or is refused at ingest (typed error → 422 trade_plan_invalid). TV/MarkitTick untouched. - enrichment/tradePlanVerification.ts (NEW): the D-DEM-5(6) law — every submitted level within [L − W, H + W] of the fetched window; plan geometry from prices only (D-DIR-3 by construction); R:R to the nearest target, 4 dp; refusal = TradePlanVerificationError (NodeConfigurationError). - providers/adapters/technicalLocalAdapter.ts: the producer — verifies the plan against ITS fetched candles and emits technical.plan (declared absences: no plan → no block; no stop/target → no R:R). - pipeline/nodes/laneView.ts: viewTechnical projects plan verbatim. - server.ts: 422 trade_plan_unverifiable (unwrapping NodeExecutionError.cause), 422 trade_plan_invalid; both persisted:false. - types/TradePlan.ts (NEW), types/UssLenses.ts, uss/ussValidator.ts: types. Registries/pins: the three fixture trees mirror afi-config (mapping 1.1.0, config, three implementationVersion moves, registration hash 5cb9b7a4…); registryLoader/officialArtifactPins pins rotated (pluginSetHash 36f911f4…). Goldens: the two plan-bearing CPJ fixtures re-authored inside the demo envelope (old levels were unverifiable by the law this slot lands); 12 goldens regenerated ONCE; every moved path itemized in INTENTIONAL_DIFFS.md (risk 0.9 → {0.5 via the provider's rr 1.4286 | 0.2 via the declared floor}; executionSummaryHash flips executed → degraded on the two goldens where the floor is the first fired default; identity-hash class on all 12). test/oracle/support/goldenDiff.mjs (NEW): the committed per-path differ. Tests: verification KATs (band, geometry, absences, error class), carrier (decimal strings, refusals), producer (role split, D-DIR-3 negative test, refusal, kernel floor, envelope-covers-fixtures), error-table rows for both 422s (no record), seam suites re-oracled to the composition reference with the mapping resolved by mappingRef from the fixture registry, golden scorerInput captured at the rubric call. 756/756; typecheck; compiled-build proofs (unavailable 503; Mongo persistence 10/10; oracle equivalence 7/7; SIGTERM) green locally. No scoring-law value moves. Sealed records untouched. Co-Authored-By: Claude Fable 5 --- src/enrichment/tradePlanVerification.ts | 160 +++++++++++++++ src/pipeline/nodes/laneView.ts | 4 + .../adapters/technicalLocalAdapter.ts | 17 ++ src/server.ts | 45 +++- src/types/TradePlan.ts | 74 +++++++ src/types/UssLenses.ts | 8 + src/uss/cpjMapper.ts | 119 ++++++++++- src/uss/ussValidator.ts | 8 + .../mappingPerStrategyResolution.test.ts | 86 +++++--- test/guardrails/uwrRuntimeProfile.test.ts | 32 ++- test/guardrails/uwrStampSemantics.test.ts | 19 +- .../reactorEvidencePersistence.mjs | 7 +- .../reactorEvidenceUnavailable.mjs | 3 +- test/oracle/INTENTIONAL_DIFFS.md | 48 +++++ .../afi-analysis-technical--2.0.0.json | 13 +- .../afi-merge-enriched-view--1.1.0.json | 6 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 11 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.1.0.json | 99 +++++++++ .../fixtures/cpj/cpj-blofin-perp-long.json | 12 +- .../fixtures/cpj/cpj-coinbase-spot-sell.json | 4 +- .../cpj-blofin-perp-long.builtin.json | 129 +++++++++--- .../cpj-blofin-perp-long.registry.json | 129 +++++++++--- .../cpj-blofin-perp-neutral.builtin.json | 32 +-- .../cpj-blofin-perp-neutral.registry.json | 32 +-- .../cpj-coinbase-spot-sell.builtin.json | 107 +++++++--- .../cpj-coinbase-spot-sell.registry.json | 107 +++++++--- .../goldens/enriched/tv-long.builtin.json | 34 +-- .../goldens/enriched/tv-long.registry.json | 34 +-- .../goldens/enriched/tv-neutral.builtin.json | 32 +-- .../goldens/enriched/tv-neutral.registry.json | 32 +-- .../goldens/enriched/tv-short.builtin.json | 32 +-- .../goldens/enriched/tv-short.registry.json | 32 +-- test/oracle/oracleErrorTable.test.ts | 29 +++ test/oracle/oracleGoldensEnriched.test.ts | 11 +- test/oracle/oracleInvariance.test.ts | 14 +- test/oracle/support/goldenDiff.mjs | 145 +++++++++++++ .../afi-analysis-technical--2.0.0.json | 13 +- .../afi-merge-enriched-view--1.1.0.json | 6 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 11 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.1.0.json | 99 +++++++++ .../afi-analysis-technical--2.0.0.json | 13 +- .../afi-merge-enriched-view--1.1.0.json | 6 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 11 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.1.0.json | 99 +++++++++ test/pipeline/officialArtifactPins.test.ts | 4 +- test/pipeline/registryLoader.test.ts | 4 +- test/pipeline/support/froggyMapping.ts | 54 +++++ test/pipeline/tradePlanCarrier.test.ts | 85 ++++++++ test/pipeline/tradePlanProducer.test.ts | 127 ++++++++++++ test/pipeline/tradePlanVerification.test.ts | 194 ++++++++++++++++++ 56 files changed, 2064 insertions(+), 380 deletions(-) create mode 100644 src/enrichment/tradePlanVerification.ts create mode 100644 src/types/TradePlan.ts create mode 100644 test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json create mode 100644 test/oracle/support/goldenDiff.mjs create mode 100644 test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json create mode 100644 test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json create mode 100644 test/pipeline/support/froggyMapping.ts create mode 100644 test/pipeline/tradePlanCarrier.test.ts create mode 100644 test/pipeline/tradePlanProducer.test.ts create mode 100644 test/pipeline/tradePlanVerification.test.ts diff --git a/src/enrichment/tradePlanVerification.ts b/src/enrichment/tradePlanVerification.ts new file mode 100644 index 00000000..dfe4b029 --- /dev/null +++ b/src/enrichment/tradePlanVerification.ts @@ -0,0 +1,160 @@ +/** + * DEM-PRODUCER-PLAN — the technical lane's trade-plan verification law + * (DEM-GOV D-DEM-5(6); §9 DEM-PRODUCER-PLAN, owner-authorized 2026-08-25). + * + * "No producer may emit as its own output a value whose sole origin is an + * assertion by the party who submitted the signal, unless that value is + * verifiable against observed market data and is validated as such by the + * producer that emits it … and refused when it cannot be checked." + * + * THE LAW (pure; no I/O; reads NO submitted side — plan geometry is derived + * from the submitted PRICES only, so DIR-GOV D-DIR-3 is honored by + * construction): + * envelope L = min(low), H = max(high) over the fetched window; W = H − L + * (W > 0 required — a flat window cannot verify anything); + * band every submitted level (entry bound(s), stop, each target) must + * lie within [L − W, H + W]: one observed-range width around the + * observed window. Breakout entries above the range and limit + * entries below it are lawful; a level a full range-width away + * from anything the market printed is not verifiable; + * geometry (only when a stop AND at least one target are submitted) every + * target lies on the same side of the stop; the entry lies + * strictly between the stop and every target. For an entry + * RANGE the conservative bound is used (nearest the targets — + * smallest reward, largest risk); + * R:R |firstTarget − entry| / |entry − stop| where firstTarget is the + * target nearest the entry; rounded half-up to 4 decimals. + * Any violation REFUSES the determination (TradePlanVerificationError — a + * NodeConfigurationError: no retry, pipeline abort, no score, no record). + * + * Declared producer absences (D-DEM-5(4)(b)): no stop or no target → no + * `rrToFirstTarget` (nothing to verify: no R:R was claimed); an entry range + * with no stop/target geometry → no `entryPrice`. + * + * This is profile math of the reference technical lane (MATH-GOV): it + * promotes nothing into afi-math and sets no scoring-law value. + */ +import { NodeConfigurationError } from "../pipeline/nodeSdk.js"; +import type { AfiCandle } from "../types/AfiCandle.js"; +import type { TechnicalPlanFacts, TradePlanV1 } from "../types/TradePlan.js"; + +export class TradePlanVerificationError extends NodeConfigurationError { + readonly code = "trade_plan_unverifiable" as const; + readonly reason: string; + constructor(reason: string) { + super(`trade plan unverifiable against the fetched candles: ${reason}`); + this.name = "TradePlanVerificationError"; + this.reason = reason; + } +} + +const DECIMAL_STRING = /^[0-9]+(\.[0-9]+)?$/; + +function refuse(reason: string): never { + throw new TradePlanVerificationError(reason); +} + +function parsePrice(label: string, raw: unknown): number { + if (typeof raw !== "string" || !DECIMAL_STRING.test(raw)) { + refuse(`${label} is not a decimal-string price (${JSON.stringify(raw)})`); + } + const value = Number(raw); + if (!Number.isFinite(value) || value <= 0) refuse(`${label} is not a positive finite price (${raw})`); + return value; +} + +/** Round half-up to 4 decimals (positive inputs only). */ +export function roundRr(value: number): number { + return Math.round(value * 1e4) / 1e4; +} + +export function verifyTradePlan(plan: TradePlanV1, candles: readonly AfiCandle[]): TechnicalPlanFacts { + if (!plan || typeof plan !== "object" || plan.schema !== "afi.trade-plan.v1" || !plan.levels) { + refuse("plan is not an afi.trade-plan.v1 object"); + } + if (candles.length === 0) refuse("no fetched candles to verify against"); + + let low = Number.POSITIVE_INFINITY; + let high = Number.NEGATIVE_INFINITY; + for (const c of candles) { + if (!Number.isFinite(c.low) || !Number.isFinite(c.high) || c.low <= 0 || c.high < c.low) { + refuse("fetched window carries a malformed candle"); + } + if (c.low < low) low = c.low; + if (c.high > high) high = c.high; + } + const width = high - low; + if (!(width > 0)) refuse("fetched window is flat (zero observed range)"); + const bandLow = low - width; + const bandHigh = high + width; + const inBand = (label: string, price: number): number => { + if (price < bandLow || price > bandHigh) { + refuse( + `${label} ${price} lies outside the observed band [${bandLow}, ${bandHigh}] ` + + `(window low ${low}, high ${high}, ${candles.length} candles)` + ); + } + return price; + }; + + // --- entry (single price or range) --- + const entryRaw = plan.levels.entry; + let entryLow: number; + let entryHigh: number; + if (typeof entryRaw === "string") { + entryLow = entryHigh = inBand("entry", parsePrice("entry", entryRaw)); + } else if (entryRaw && typeof entryRaw === "object") { + entryLow = inBand("entry.min", parsePrice("entry.min", (entryRaw as { min?: unknown }).min)); + entryHigh = inBand("entry.max", parsePrice("entry.max", (entryRaw as { max?: unknown }).max)); + if (entryLow > entryHigh) refuse(`entry range is inverted (min ${entryLow} > max ${entryHigh})`); + } else { + refuse("entry is neither a price nor a range"); + } + + // --- stop / targets (each verified against the band whenever present) --- + const stopPrice = + plan.levels.stopLoss !== undefined + ? inBand("stopLoss", parsePrice("stopLoss", plan.levels.stopLoss)) + : undefined; + const targets: number[] = []; + for (const [i, tp] of (plan.levels.takeProfits ?? []).entries()) { + targets.push(inBand(`takeProfits[${i}].price`, parsePrice(`takeProfits[${i}].price`, tp?.price))); + } + + const facts: TechnicalPlanFacts = { + entryLow, + entryHigh, + targetCount: targets.length, + envelopeLow: low, + envelopeHigh: high, + barCount: candles.length, + }; + if (entryLow === entryHigh) facts.entryPrice = entryLow; + if (stopPrice !== undefined) facts.stopPrice = stopPrice; + + // --- geometry + R:R only when a stop AND a target were submitted --- + if (stopPrice === undefined || targets.length === 0) return facts; + + const above = targets.filter((t) => t > stopPrice).length; + const below = targets.filter((t) => t < stopPrice).length; + if (above > 0 && below > 0) refuse("targets lie on both sides of the stop"); + if (above === 0 && below === 0) refuse("a target equals the stop"); + const longGeometry = above > 0; + // Conservative entry bound: nearest the targets. + const entryPrice = longGeometry ? entryHigh : entryLow; + if (longGeometry) { + if (!(stopPrice < entryPrice)) refuse(`stop ${stopPrice} is not below the entry ${entryPrice}`); + for (const t of targets) if (!(t > entryPrice)) refuse(`target ${t} is not above the entry ${entryPrice}`); + } else { + if (!(stopPrice > entryPrice)) refuse(`stop ${stopPrice} is not above the entry ${entryPrice}`); + for (const t of targets) if (!(t < entryPrice)) refuse(`target ${t} is not below the entry ${entryPrice}`); + } + let firstTarget = targets[0]; + for (const t of targets) if (Math.abs(t - entryPrice) < Math.abs(firstTarget - entryPrice)) firstTarget = t; + const risk = Math.abs(entryPrice - stopPrice); + const reward = Math.abs(firstTarget - entryPrice); + facts.entryPrice = entryPrice; + facts.firstTargetPrice = firstTarget; + facts.rrToFirstTarget = roundRr(reward / risk); + return facts; +} diff --git a/src/pipeline/nodes/laneView.ts b/src/pipeline/nodes/laneView.ts index 5abf022f..78ddd47b 100644 --- a/src/pipeline/nodes/laneView.ts +++ b/src/pipeline/nodes/laneView.ts @@ -88,6 +88,10 @@ export function viewTechnical( // AR-GOV D-AR-3: the regime is projected AND read by the adapter (unlike // the four context-only fields above/below it). atrRegime: payload.atrRegime, + // DEM-PRODUCER-PLAN: the lane's verified trade-plan facts ride the view + // verbatim so the registered mapping can bind them (technical.plan.*); + // omitted (not null) when the lane emitted none. + ...(payload.plan !== undefined ? { plan: payload.plan } : {}), }; } diff --git a/src/providers/adapters/technicalLocalAdapter.ts b/src/providers/adapters/technicalLocalAdapter.ts index a2906607..d2941749 100644 --- a/src/providers/adapters/technicalLocalAdapter.ts +++ b/src/providers/adapters/technicalLocalAdapter.ts @@ -18,6 +18,8 @@ import type { computeTechnicalEnrichment } from "../../enrichment/technicalIndic import type { AfiCandle } from "../../types/AfiCandle.js"; import { NodeConfigurationError } from "../../pipeline/nodeSdk.js"; import type { CategoryResult, ProviderAdapter, ProviderAdapterContext } from "../types.js"; +import { TradePlanVerificationError, verifyTradePlan } from "../../enrichment/tradePlanVerification.js"; +import type { TradePlanV1 } from "../../types/TradePlan.js"; export interface TechnicalLocalAdapterDeps { resolvePriceSource: typeof getDefaultPriceSource; @@ -80,6 +82,21 @@ export function createTechnicalLocalAdapter(deps?: TechnicalLocalAdapterDeps): P const candles = toAfiCandles(rawCandles); const technical = d.computeTechnical(candles); + // DEM-PRODUCER-PLAN (D-DEM-5(6)): a submitted trade plan is verified + // against THIS fetched window by THIS producer before any of its + // levels becomes a fact; an unverifiable plan refuses the determination + // (no retry, no score, no record). Reads only uss.plan and the candles — + // never facts.direction (DIR-GOV D-DIR-3) and never analyst config. + const plan = (ctx.signal as { plan?: unknown }).plan; + if (plan !== undefined && plan !== null) { + if (!technical) { + throw new TradePlanVerificationError( + `no computable indicator window (${candles.length} candles fetched, kernel floor not met)` + ); + } + technical.plan = verifyTradePlan(plan as TradePlanV1, candles); + } + ctx.logger.info("technical enrichment computed (provider adapter)", { priceSource, symbol, diff --git a/src/server.ts b/src/server.ts index dd12d22a..0985ff36 100644 --- a/src/server.ts +++ b/src/server.ts @@ -32,6 +32,7 @@ dotenv.config(); import express, { Request, Response } from "express"; import { validateUsignalV11 } from "./uss/ussValidator.js"; +import { TradePlanVerificationError } from "./enrichment/tradePlanVerification.js"; import { mapTradingViewToUssV11, type TradingViewAlertPayload, @@ -112,6 +113,26 @@ function respondWithFailure(res: Response, err: unknown, context: string): Respo persisted: false, }); } + // DEM-PRODUCER-PLAN (D-DEM-5(6)): a submitted plan the technical lane could + // not verify against its fetched candles is an honest 422 — no score, no + // record. The executor wraps the fatal node error (NodeExecutionError) with + // the original as `cause`; unwrap one level. + const cause = (err as { cause?: unknown } | null)?.cause; + const unverifiable = + err instanceof TradePlanVerificationError + ? err + : cause instanceof TradePlanVerificationError + ? cause + : undefined; + if (unverifiable) { + console.warn(`⚠️ ${context}: trade plan refused`, { reason: unverifiable.reason }); + return res.status(422).json({ + error: unverifiable.code, + message: unverifiable.message, + reason: unverifiable.reason, + persisted: false, + }); + } console.error(`❌ ${context}:`, (err as Error)?.message ?? String(err)); return res.status(500).json({ error: "internal_error", @@ -662,16 +683,28 @@ app.post("/api/ingest/cpj", async (req: Request, res: Response) => { // ✅ STEP 2: Map CPJ → USS v1.1 with strict symbol validation const mappingResult = mapCpjToUssV11(rawPayload, resolution.triple); - // Check for symbol normalization failures + // Typed mapping refusals: symbol normalization, or (DEM-PRODUCER-PLAN) a + // submitted trade plan that does not conform to afi.trade-plan.v1. if (!mappingResult.success) { - console.error(`❌ Symbol normalization failed:`, mappingResult.error); + const mappingError = mappingResult.error!; + if (mappingError.type === "trade_plan_invalid") { + console.warn(`⚠️ Trade plan refused at ingest:`, mappingError); + return res.status(422).json({ + error: "trade_plan_invalid", + message: "Submitted trade levels do not form a valid afi.trade-plan.v1", + reason: mappingError.reason, + details: mappingError.details, + persisted: false, + }); + } + console.error(`❌ Symbol normalization failed:`, mappingError); return res.status(422).json({ error: "symbol_normalization_failed", message: "Could not normalize symbol to canonical BASE/QUOTE format", - symbolRaw: mappingResult.error!.symbolRaw, - symbolNormalizedAttempt: mappingResult.error!.symbolNormalizedAttempt, - reason: mappingResult.error!.reason, - details: mappingResult.error!.details, + symbolRaw: mappingError.symbolRaw, + symbolNormalizedAttempt: mappingError.symbolNormalizedAttempt, + reason: mappingError.reason, + details: mappingError.details, }); } diff --git a/src/types/TradePlan.ts b/src/types/TradePlan.ts new file mode 100644 index 00000000..524dea14 --- /dev/null +++ b/src/types/TradePlan.ts @@ -0,0 +1,74 @@ +/** + * afi.trade-plan.v1 — the governed trade-plan contract + * (afi-config/schemas/provenance/v1/trade-plan.schema.json), as carried on the + * canonical USS signal at `uss.plan` by the CPJ→USS mapper (DEM-GOV §9 + * DEM-PRODUCER-PLAN, owner-authorized 2026-08-25; determination D-4). + * + * NUMBER POLICY: every price/quantity is a DECIMAL STRING (`^[0-9]+(\.[0-9]+)?$`), + * never a float — the afi.hash.v1 input-hash law rejects raw non-integers, and + * the contract fixes strings. The runtime validates the block against the + * governed schema (validateTradePlanV1) at ingest; the technical lane verifies + * the LEVELS against the candles it fetched before any of them becomes a fact + * (D-DEM-5(6)). + */ +export interface TradePlanTakeProfitV1 { + price: string; + sizePct?: string; +} + +export interface TradePlanPriceRangeV1 { + min: string; + max: string; +} + +export interface TradePlanLevelsV1 { + entry: string | TradePlanPriceRangeV1; + stopLoss?: string; + takeProfits?: TradePlanTakeProfitV1[]; +} + +export interface TradePlanV1 { + schema: "afi.trade-plan.v1"; + signalId?: string; + levels: TradePlanLevelsV1; + leverageHint?: string; + venueHint?: string; + marketTypeHint?: "spot" | "perp" | "futures"; + notes?: string; +} + +/** + * The technical lane's VERIFIED plan facts (`technical.plan` on the lens + * payload; projected verbatim into the enriched view). Every price here was + * checked against the fetched candle window (tradePlanVerification.ts); a + * plan that cannot be checked never produces this block — the determination + * refuses instead (D-DEM-5(6)). + * + * Declared producer absences (D-DEM-5(4)(b), recorded in the technical + * plugin manifest's description): the whole block is absent when the signal + * carries no plan; `stopPrice` / `firstTargetPrice` / `rrToFirstTarget` are + * absent when the plan carries no stop or no target (no R:R claim to + * verify); `entryPrice` is absent for an entry RANGE whose conservative bound + * cannot be chosen (no stop/target geometry). + */ +export interface TechnicalPlanFacts { + /** Lower / upper submitted entry bound (equal for a single entry price). */ + entryLow: number; + entryHigh: number; + /** The entry price the R:R is computed at: the submitted price, or for a + * range the CONSERVATIVE bound (nearest the targets — smallest reward, + * largest risk). */ + entryPrice?: number; + stopPrice?: number; + /** The submitted target nearest the entry. */ + firstTargetPrice?: number; + /** Number of submitted take-profit targets. */ + targetCount: number; + /** |firstTarget − entry| / |entry − stop|, rounded half-up to 4 decimals. */ + rrToFirstTarget?: number; + /** The verification band's source: the fetched window's extreme low/high. */ + envelopeLow: number; + envelopeHigh: number; + /** Fetched candles the envelope was computed over. */ + barCount: number; +} diff --git a/src/types/UssLenses.ts b/src/types/UssLenses.ts index 4013029b..bcc8105b 100644 --- a/src/types/UssLenses.ts +++ b/src/types/UssLenses.ts @@ -9,6 +9,7 @@ * * @module UssLenses */ +import type { TechnicalPlanFacts } from "./TradePlan.js"; /** * Base lens interface. @@ -60,6 +61,13 @@ export interface TechnicalLensV1 extends UssLens { * every atrRegime classification auditable against these bytes. */ atrPercentile?: number; + /** + * DEM-PRODUCER-PLAN: the lane's VERIFIED trade-plan facts (the submitted + * afi.trade-plan.v1 levels checked against this window, D-DEM-5(6)). + * Absent when the signal carried no plan; an unverifiable plan refuses + * the determination instead of producing this block. + */ + plan?: TechnicalPlanFacts; }; } diff --git a/src/uss/cpjMapper.ts b/src/uss/cpjMapper.ts index ea0772ad..54163b67 100644 --- a/src/uss/cpjMapper.ts +++ b/src/uss/cpjMapper.ts @@ -20,6 +20,9 @@ import { type SymbolNormalizationResult, SymbolNormalizationError, } from "./symbolNormalizer.js"; +import { validateTradePlanV1 } from "../evidence/provenance/schemaValidation.js"; +import { toCanonicalDecimalString } from "../evidence/provenance/hashProjection.js"; +import type { TradePlanV1 } from "../types/TradePlan.js"; /** * CPJ to USS mapping result @@ -27,13 +30,96 @@ import { export interface CpjMappingResult { success: boolean; uss?: UssV11Payload; - error?: { - type: "symbol_normalization_failed"; - symbolRaw: string; - symbolNormalizedAttempt?: string; - reason: SymbolNormalizationError; - details?: string; - }; + error?: CpjMappingError; +} + +export type CpjMappingError = + | { + type: "symbol_normalization_failed"; + symbolRaw: string; + symbolNormalizedAttempt?: string; + reason: SymbolNormalizationError; + details?: string; + } + | { + /** DEM-PRODUCER-PLAN: the submitted levels do not form a valid + * afi.trade-plan.v1 — refused at ingest (422), never dropped. */ + type: "trade_plan_invalid"; + reason: string; + details?: string[]; + }; + +/** Thrown inside buildTradePlan; converted to the typed error union by the mapper. */ +class TradePlanInvalid extends Error { + readonly details?: string[]; + constructor(reason: string, details?: string[]) { + super(reason); + this.details = details; + } +} + +/** + * A CPJ number → the contract's decimal string (plain notation, no exponent). + * Negative, NaN, or non-finite numbers cannot be expressed by the contract's + * `^[0-9]+(\.[0-9]+)?$` pattern and are refused by the schema validation below. + */ +function toPlanDecimal(label: string, value: unknown): string { + if (typeof value !== "number" || !Number.isFinite(value)) { + throw new TradePlanInvalid(`${label} is not a finite number`); + } + if (value < 0) throw new TradePlanInvalid(`${label} is negative`); + return toCanonicalDecimalString(value); +} + +/** + * DEM-PRODUCER-PLAN (DEM-GOV §9; determination D-4): carry the submitted trade + * plan through to the canonical signal as `uss.plan`, an afi.trade-plan.v1 + * object validated against the governed contract. Returns undefined when the + * submission carries NO plan field at all (entry / stopLoss / takeProfits) — + * the technical lane then emits no plan facts (declared absence). A partial + * or malformed plan (stop or targets without an entry, a negative price, …) + * is REFUSED here: the levels are never silently dropped again. + */ +export function buildTradePlanFromCpj(cpj: CpjV01Payload, signalId: string): TradePlanV1 | undefined { + const x = cpj.extracted; + const hasEntry = x.entry !== undefined && x.entry !== null; + const hasStop = x.stopLoss !== undefined && x.stopLoss !== null; + const hasTargets = Array.isArray(x.takeProfits) && x.takeProfits.length > 0; + if (!hasEntry && !hasStop && !hasTargets) return undefined; + if (!hasEntry) { + throw new TradePlanInvalid("a stop or target was submitted without an entry (afi.trade-plan.v1 requires levels.entry)"); + } + + const entry = + typeof x.entry === "number" + ? toPlanDecimal("entry", x.entry) + : { min: toPlanDecimal("entry.min", (x.entry as { min: number }).min), max: toPlanDecimal("entry.max", (x.entry as { max: number }).max) }; + const levels: TradePlanV1["levels"] = { entry }; + if (hasStop) levels.stopLoss = toPlanDecimal("stopLoss", x.stopLoss); + if (hasTargets) { + levels.takeProfits = (x.takeProfits as Array<{ price: number; percentage?: number }>).map((tp, i) => { + const out: { price: string; sizePct?: string } = { price: toPlanDecimal(`takeProfits[${i}].price`, tp.price) }; + if (tp.percentage !== undefined && tp.percentage !== null) { + out.sizePct = toPlanDecimal(`takeProfits[${i}].percentage`, tp.percentage); + } + return out; + }); + } + const plan: TradePlanV1 = { schema: "afi.trade-plan.v1", signalId, levels }; + if (x.leverageHint !== undefined && x.leverageHint !== null) plan.leverageHint = toPlanDecimal("leverageHint", x.leverageHint); + if (typeof x.venueHint === "string" && x.venueHint.length > 0) plan.venueHint = x.venueHint; + if (x.marketTypeHint === "spot" || x.marketTypeHint === "perp" || x.marketTypeHint === "futures") { + plan.marketTypeHint = x.marketTypeHint; + } + + const validation = validateTradePlanV1(plan); + if (!validation.ok) { + throw new TradePlanInvalid( + "submitted levels do not conform to afi.trade-plan.v1", + validation.errors.map((e) => `${e.field || "/"} ${e.message}`) + ); + } + return plan; } /** @@ -305,6 +391,21 @@ export function mapCpjToUssV11( // Map CPJ providerType to USS-compatible providerType const ussProviderType = mapProviderType(cpj.provenance.providerType); + // DEM-PRODUCER-PLAN: the submitted trade plan survives the mapping as + // uss.plan (afi.trade-plan.v1) or is refused — never dropped. + let plan: TradePlanV1 | undefined; + try { + plan = buildTradePlanFromCpj(cpj, signalId); + } catch (error) { + if (error instanceof TradePlanInvalid) { + return { + success: false, + error: { type: "trade_plan_invalid", reason: error.message, details: error.details }, + }; + } + throw error; + } + // Construct canonical USS v1.1 const uss: UssV11Payload = { schema: "afi.usignal.v1.1", @@ -333,6 +434,10 @@ export function mapCpjToUssV11( strategy: resolvedStrategy.strategyId, direction, }, + // DEM-PRODUCER-PLAN: root-level governed block (USS v1.1's root is open; + // `facts` stays closed and untouched). Present only when a plan was + // submitted. Enters inputHash as decimal strings (afi.hash.v1). + ...(plan !== undefined ? { plan } : {}), }; return { success: true, uss }; diff --git a/src/uss/ussValidator.ts b/src/uss/ussValidator.ts index 40718e46..3e146615 100644 --- a/src/uss/ussValidator.ts +++ b/src/uss/ussValidator.ts @@ -11,6 +11,7 @@ import { Ajv } from "ajv"; import * as ajvFormatsModule from "ajv-formats"; import { readFileSync } from "fs"; +import type { TradePlanV1 } from "../types/TradePlan.js"; import { join } from "path"; import type { ValidateFunction } from "ajv"; @@ -57,6 +58,13 @@ export interface UssV11Payload { }; core?: any; lens?: string; + /** + * DEM-PRODUCER-PLAN: the submitted trade plan (afi.trade-plan.v1, decimal + * strings), carried by the CPJ→USS mapper when a plan was submitted and + * verified by the technical lane against its fetched candles (D-DEM-5(6)). + * The TradingView/MarkitTick routes carry no plan today. + */ + plan?: TradePlanV1; [key: string]: any; } diff --git a/test/guardrails/mappingPerStrategyResolution.test.ts b/test/guardrails/mappingPerStrategyResolution.test.ts index 437b3275..17578564 100644 --- a/test/guardrails/mappingPerStrategyResolution.test.ts +++ b/test/guardrails/mappingPerStrategyResolution.test.ts @@ -39,23 +39,25 @@ import { type ResolvedMappingCarrier, } from "../../src/pipeline/nodeSdk.js"; import type { FroggyEnrichedView } from "../../node_modules/afi-core/analysts/froggy.enrichment_adapter.js"; -// DEM-BIND (e2) seam-test rework (ruling R1): the adapter's expressible half -// survives as the TEST-SIDE byte-equivalence oracle — the export the FLPR -// guards and the golden harness already sanction. The live path composes -// fragment+residual only; equivalence is proven against the adapter export -// imported directly here, never via a removed legacy branch. -import { buildFroggyTrendPullbackInputFromEnriched } from "../../node_modules/afi-core/analysts/froggy.enrichment_adapter.js"; -import { scoreFroggyTrendPullback } from "../../node_modules/afi-core/analysts/froggy.trend_pullback_v1.js"; +// DEM-PRODUCER-PLAN: the retired adapter no longer yields a full scorer +// input (its rrMultiplePlanned synthesis is deleted), so the seam is proven +// against the COMPOSITION REFERENCE — the production assembly (registered +// mapping fragment + residual → composer → rubric) run out-of-band — with the +// mapping resolved by the fixture registration's mappingRef, exactly as boot +// resolves it (never the afi-config example). +import { compositionReference, froggyMappingCarrier } from "../pipeline/support/froggyMapping.js"; const REPO_ROOT = process.cwd(); const FIXTURE_CONFIG_ROOT = path.resolve(REPO_ROOT, "test/pipeline/fixtures/afi-config"); const CONFIG_REL = "registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json"; const REGISTRATION_REL = "registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json"; -const CANONICAL_MAPPING = path.resolve( - REPO_ROOT, - "node_modules/afi-config/examples/enrichment-mapping/v1/enrichment-mapping.example.json" -); const FROGGY_KEY = "froggy/trend_pullback_v1@1.0.0"; +// The registered 1.0.0 mapping, from the fixture registry (byte-identical to +// the afi-config canonical example; DEM-PRODUCER-PLAN registered 1.1.0 beside it). +const REGISTERED_MAPPING_100 = path.resolve( + FIXTURE_CONFIG_ROOT, + "registries/enrichment-mappings/froggy-trend-pullback--1.0.0.json" +); /** The production plugin registry over an empty provider runtime (binding only). */ function testBuiltinRegistry() { @@ -102,7 +104,7 @@ function removeMappingRef(root: string): void { function registerFroggyMapping(root: string, mutateDoc?: (doc: any) => void): void { const dir = join(root, "registries/enrichment-mappings"); mkdirSync(dir, { recursive: true }); - const doc = JSON.parse(readFileSync(CANONICAL_MAPPING, "utf-8")); + const doc = JSON.parse(readFileSync(REGISTERED_MAPPING_100, "utf-8")); mutateDoc?.(doc); writeFileSync(join(dir, "froggy-trend-pullback--1.0.0.json"), JSON.stringify(doc, null, 2)); } @@ -140,7 +142,9 @@ describe("D-DEM-2(6): fail-closed mapping resolution at boot", () => { it("mappingRef with NO registered document refuses boot — never a silent skip", () => { bootRefusal( scratchRoot((r) => { - rmSync(join(r, "registries/enrichment-mappings/froggy-trend-pullback--1.0.0.json")); + // Version-agnostic: remove the whole registry family, whatever version + // the fixture config names (DEM-PRODUCER-PLAN registered 1.1.0). + rmSync(join(r, "registries/enrichment-mappings"), { recursive: true, force: true }); }), /does not resolve.*fail-closed, D-DEM-2\(6\)/ ); @@ -221,15 +225,11 @@ describe("the ctx.mapping carrier is bounded: the scorer node is its sole reader }); describe("the seam: fragment+residual equals the legacy path; fired defaults are recorded", () => { - const skipIfNoMapping = existsSync(CANONICAL_MAPPING) ? describe : describe.skip; + const skipIfNoMapping = existsSync(REGISTERED_MAPPING_100) ? describe : describe.skip; skipIfNoMapping("with the canonical froggy mapping", () => { function carrier(): ResolvedMappingCarrier { - return { - mappingId: "froggy-trend-pullback", - version: "1.0.0", - doc: JSON.parse(readFileSync(CANONICAL_MAPPING, "utf-8")), - }; + return froggyMappingCarrier(FIXTURE_CONFIG_ROOT); } function ctx(withMapping: boolean): NodeRunContext { @@ -271,23 +271,40 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are return clone; } - /** The adapter export, applied as the sanctioned test-side oracle (R1). */ - function adapterOracleAnalysis(enriched: FroggyEnrichedView) { + /** The production assembly, run out-of-band (the composition reference). */ + function referenceAnalysis(enriched: FroggyEnrichedView) { const uwr = getUwrRuntimeConfigForProfile("uwr-weighted-lifts-v0.1"); - return scoreFroggyTrendPullback( - buildFroggyTrendPullbackInputFromEnriched(enriched), - uwr.config, - enriched - ); + return compositionReference(enriched, uwr.config, carrier()).analysis; } - it("mapping path output is byte-identical to the adapter oracle (no fired defaults)", async () => { - const withMapping = await scorerFroggyTrendPullbackNode.run(view(), ctx(true)); + /** A view carrying the technical lane's verified plan facts (DEM-PRODUCER-PLAN). */ + function viewWithPlan(): FroggyEnrichedView { + const v = view(); + return { + ...v, + technical: { + ...v.technical, + plan: { entryPrice: 100, stopPrice: 98, firstTargetPrice: 104, rrToFirstTarget: 2, targetCount: 1 }, + }, + }; + } + + it("mapping path output equals the composition reference (no fired defaults when the plan fact is present)", async () => { + const withMapping = await scorerFroggyTrendPullbackNode.run(viewWithPlan(), ctx(true)); const a = withMapping.output as { analysis: unknown }; - expect(normalized(a.analysis)).toEqual(normalized(adapterOracleAnalysis(view()))); + expect(normalized(a.analysis)).toEqual(normalized(referenceAnalysis(viewWithPlan()))); expect(withMapping.degradations).toEqual([]); }); + it("a plan-less view fires ONLY the rrMultiplePlanned floor default — recorded, never silent (D-DEM-5(3); §9 determination D-5)", async () => { + const result = await scorerFroggyTrendPullbackNode.run(view(), ctx(true)); + expect(result.degradations).toEqual([{ class: "declared-default-fired", detail: "rrMultiplePlanned" }]); + const a = result.output as { analysis: { analystScore: { uwrAxes: { risk: number } } } }; + // The rubric floor (rr 1 → 0.2) — a hash-committed, recorded degradation. + expect(a.analysis.analystScore.uwrAxes.risk).toBe(0.2); + expect(normalized(a.analysis)).toEqual(normalized(referenceAnalysis(view()))); + }); + it("a fired grandfathered default is a RECORDED degradation (D-DEM-5(3)), never silent", async () => { const bare = { ...view(), @@ -301,19 +318,22 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are "declared-default-fired", "declared-default-fired", "declared-default-fired", + "declared-default-fired", ]); const targets = (result.degradations ?? []).map((d) => d.detail).sort(); expect(targets).toEqual([ "atrRegime", "distanceFromDailyEmaPct", "pulledBackIntoSweetSpot", + "rrMultiplePlanned", "triggerPatternQuality", ]); - // The adapter oracle scores the same INPUT VALUES (the grandfather - // exists so DEM-BIND reproduces today's scored values exactly, - // D-DEM-5(4)). + // The composition reference scores the same INPUT VALUES (the + // grandfather exists so DEM-BIND reproduces today's scored values + // exactly, D-DEM-5(4); the plan floor is the PLAN slot's declared + // default). const a = result.output as { analysis: unknown }; - expect(normalized(a.analysis)).toEqual(normalized(adapterOracleAnalysis(bare))); + expect(normalized(a.analysis)).toEqual(normalized(referenceAnalysis(bare))); }); it("an absent resolved mapping yields NO determination (D-DEM-2(6), D-DEM-5(7))", async () => { diff --git a/test/guardrails/uwrRuntimeProfile.test.ts b/test/guardrails/uwrRuntimeProfile.test.ts index fbaa93ef..194b308b 100644 --- a/test/guardrails/uwrRuntimeProfile.test.ts +++ b/test/guardrails/uwrRuntimeProfile.test.ts @@ -46,7 +46,7 @@ import { // reason the analysts moduleNameMapper exists for runtime); the relative // path lets TypeScript check the file:-linked source directly while jest's // relative mapper resolves the identical module at runtime. -import { scoreFroggyTrendPullbackFromEnriched } from "../../node_modules/afi-core/analysts/froggy.trend_pullback_v1.js"; +import { compositionReference, froggyMappingCarrier } from "../pipeline/support/froggyMapping.js"; import type { FroggyEnrichedView } from "../../node_modules/afi-core/analysts/froggy.enrichment_adapter.js"; import { scorerFroggyTrendPullbackNode } from "../../src/pipeline/nodes/scorerFroggyTrendPullback.js"; import { @@ -59,21 +59,9 @@ import { // equivalence probes hand down the canonical registered froggy mapping // (D-DEM-2(6); its interpreter fragment is byte-equivalent to the adapter // over the full non-refusal domain, so plugin-vs-kernel equality is unmoved). -function froggyMappingCarrier(): ResolvedMappingCarrier { - return { - mappingId: "froggy-trend-pullback", - version: "1.0.0", - doc: JSON.parse( - readFileSync( - path.resolve( - process.cwd(), - "node_modules/afi-config/examples/enrichment-mapping/v1/enrichment-mapping.example.json" - ), - "utf-8" - ) - ) as Record, - }; -} +// DEM-PRODUCER-PLAN: the registered mapping is resolved from the fixture +// registry by the fixture registration's mappingRef (test/pipeline/support/ +// froggyMapping.ts) — never the afi-config example (the immutable 1.0.0 vector). /** The LIVE scoring seam (D-FCP-9: the old froggy analyst plugin is deleted; * the scorer node composes the identical afi-core kernels and emits the @@ -530,12 +518,15 @@ describe("PR-UWR-RUNTIME-READ: plugin call-site equivalence (the changed consume return clone; } - it("builtin (explicit): plugin run() output is identical to scoreFroggyTrendPullbackFromEnriched", async () => { + it("builtin (explicit): plugin run() output is identical to the composition reference", async () => { process.env[UWR_PROFILE_SOURCE_ENV] = "builtin"; __resetUwrRuntimeConfigForTests(); const enriched = enrichedFixture(); const viaPlugin = await runScorer(enriched); - const reference = scoreFroggyTrendPullbackFromEnriched(enrichedFixture()); + const reference = compositionReference( + enrichedFixture(), + getUwrRuntimeConfigForProfile("uwr-weighted-lifts-v0.1").config + ).analysis; expect(normalized(viaPlugin.analysis)).toEqual(normalized(reference)); // No response-contract leakage: output shape = enriched + analysis + // uwrResolvedSource only. (uwrResolvedSource was added by @@ -561,7 +552,10 @@ describe("PR-UWR-RUNTIME-READ: plugin call-site equivalence (the changed consume process.env[UWR_PROFILE_SOURCE_ENV] = "registry"; __resetUwrRuntimeConfigForTests(); const viaPlugin = await runScorer(enrichedFixture()); - const reference = scoreFroggyTrendPullbackFromEnriched(enrichedFixture()); + const reference = compositionReference( + enrichedFixture(), + getUwrRuntimeConfigForProfile("uwr-weighted-lifts-v0.1").config + ).analysis; expect(normalized(viaPlugin.analysis)).toEqual(normalized(reference)); // RC-6 propagation: a successful registry resolution — and only a // successful one; failure throws before scoring — flows to the stamp. diff --git a/test/guardrails/uwrStampSemantics.test.ts b/test/guardrails/uwrStampSemantics.test.ts index 37f6585b..5bb1fd92 100644 --- a/test/guardrails/uwrStampSemantics.test.ts +++ b/test/guardrails/uwrStampSemantics.test.ts @@ -53,26 +53,15 @@ import { type NodeRunContext, type ResolvedMappingCarrier, } from "../../src/pipeline/nodeSdk.js"; +import { froggyMappingCarrier } from "../pipeline/support/froggyMapping.js"; // DEM-BIND (e2): the scorer node refuses without a resolved mapping — these // semantics probes hand down the canonical registered froggy mapping // (D-DEM-2(6); its interpreter fragment is byte-equivalent to the adapter // over the full non-refusal domain, so every equivalence below is unmoved). -function froggyMappingCarrier(): ResolvedMappingCarrier { - return { - mappingId: "froggy-trend-pullback", - version: "1.0.0", - doc: JSON.parse( - readFileSync( - path.resolve( - process.cwd(), - "node_modules/afi-config/examples/enrichment-mapping/v1/enrichment-mapping.example.json" - ), - "utf-8" - ) - ) as Record, - }; -} +// DEM-PRODUCER-PLAN: the registered mapping is resolved from the fixture +// registry by the fixture registration's mappingRef (test/pipeline/support/ +// froggyMapping.ts) — never the afi-config example (the immutable 1.0.0 vector). // Repo idiom (see test/evidence/provenance/*.test.ts): jest runs from the repo root. const REPO_ROOT = process.cwd(); diff --git a/test/integration-mongo/reactorEvidencePersistence.mjs b/test/integration-mongo/reactorEvidencePersistence.mjs index b19476d1..de60bbfe 100644 --- a/test/integration-mongo/reactorEvidencePersistence.mjs +++ b/test/integration-mongo/reactorEvidencePersistence.mjs @@ -156,9 +156,10 @@ async function main() { extracted: { symbolRaw: "BTCUSDT", side: "long", - entry: 42500, - stopLoss: 41800, - takeProfits: [{ price: 43500 }], + // DEM-PRODUCER-PLAN: the plan must verify against the demo window (BTC ≈ 49k–51k). + entry: 50000, + stopLoss: 49300, + takeProfits: [{ price: 51000 }], timeframeHint: "4h", venueHint: "blofin", marketTypeHint: "perp", diff --git a/test/integration-mongo/reactorEvidenceUnavailable.mjs b/test/integration-mongo/reactorEvidenceUnavailable.mjs index f4d64254..eb8c28d8 100644 --- a/test/integration-mongo/reactorEvidenceUnavailable.mjs +++ b/test/integration-mongo/reactorEvidenceUnavailable.mjs @@ -72,7 +72,8 @@ async function main() { extracted: { symbolRaw: "BTCUSDT", side: "long", - entry: 42500, + // DEM-PRODUCER-PLAN: a plan must verify against the demo window (BTC ≈ 49k–51k). + entry: 50000, timeframeHint: "4h", venueHint: "blofin", marketTypeHint: "perp", diff --git a/test/oracle/INTENTIONAL_DIFFS.md b/test/oracle/INTENTIONAL_DIFFS.md index a85437f4..c4903ea0 100644 --- a/test/oracle/INTENTIONAL_DIFFS.md +++ b/test/oracle/INTENTIONAL_DIFFS.md @@ -545,3 +545,51 @@ retired seam byte-for-byte, incl. the grandfathered defaults — D-DEM-5(4)), both direction surfaces, and the full `httpResponse` family. No scored-value field moved. Anything else diffing would have been a defect to fix in code, never absorbed into a golden. + +--- + +# DEM-PRODUCER-PLAN reconciliation — the planned R:R becomes a provider fact (DEM-GOV §9, owner-authorized 2026-08-25) + +**Authority.** DEM-GOV §9 `DEM-PRODUCER-PLAN` (founder instruction of 2026-08-25 recorded verbatim in the DEM-GOV Status line + §9; afi-governance #55) with §9 determinations D-2/D-4/D-5 (afi-governance #56). D-DEM-7(2) authorizes the movement in class (the risk axis fed by `rrMultiplePlanned`, hence `uwrScore`/`conviction`); this section is the D-DEM-7(3) itemization. **A slot whose diff exceeds its own itemization must not merge.** + +**What changed on the path.** (1) `cpjMapper` carries the submitted trade plan as `uss.plan` (`afi.trade-plan.v1`, decimal strings) or refuses at ingest (422 `trade_plan_invalid`); TV/MarkitTick untouched. (2) The technical lane verifies every submitted level against the candles it fetched (band `[L − W, H + W]`, plan geometry from prices only) and emits `technical.plan` (`entryLow/entryHigh/entryPrice/stopPrice/firstTargetPrice/targetCount/rrToFirstTarget/envelopeLow/envelopeHigh/barCount`) or refuses the determination (422 `trade_plan_unverifiable`, no record). (3) `viewTechnical` projects `plan` verbatim. (4) Mapping **1.1.0** binds `rrMultiplePlanned ← technical.plan.rrToFirstTarget` (producer-declared optional, default **1** = the rubric floor; the firing is a recorded degradation). (5) afi-core deletes the `pulledBackIntoSweetSpot && !brokeEmaWithBody ? 2 : 1` synthesis; the composer partitions the ten fields exactly. (6) `implementationVersion`: technical `2.0.0 → 2.1.0`, merge `1.1.0 → 1.2.0`, scorer `1.3.0 → 1.4.0`; `pluginSetHash` `220c004e… → 36f911f4…`; `analystConfigHash` `aa8cf5cf… → 5cb9b7a4…` (mappingRef 1.0.0 → 1.1.0). (7) The two plan-bearing CPJ **fixtures were re-authored inside the demo feed's price envelope** (BTC: entry 42500/SL 41800/TP 43500 → 50000/49300/51000; ETH: entry 2280.5 → 3001.5) — the old levels lay thousands of dollars outside every window the demo feed can print and would have been refused by the law this slot lands; a test now proves the envelope contains every committed fixture plan. + +Goldens regenerated ONCE via `npm run oracle:regen`; audited with the committed differ `node test/oracle/support/goldenDiff.mjs` (all 12 changed; 0 byte-equal). + +## Per-golden scored-value movement (the class D-DEM-7(2) authorizes: the risk axis and its derivatives) + +| golden | `scorerInput.rrMultiplePlanned` | `uwrAxes.risk` | `uwrScore` = `conviction` | `structure` | plan on USS | `executionSummaryHash` | +|---|---|---|---|---|---|---| +| `cpj-blofin-perp-long.{builtin,registry}` | 2 → 1.4286 | 0.9 → 0.5 | 0.59167 → 0.49167 | 0.4 (unmoved) | yes (verified; rr 1000/700) | byte-equal (already degraded by a grandfather firing) | +| `cpj-blofin-perp-neutral.{builtin,registry}` | 2 → 1 | 0.9 → 0.2 | 0.4 → 0.225 | 0.4 | no → floor default fired | byte-equal (already degraded) | +| `cpj-coinbase-spot-sell.{builtin,registry}` | 2 → 1 | 0.9 → 0.2 | 0.59167 → 0.41667 | 0.4 | yes, entry-only → no R:R claim → floor default fired | **moved** (`executed` → `degraded`) | +| `tv-long.{builtin,registry}` | 2 → 1 | 0.9 → 0.2 | 0.59167 → 0.41667 | 0.4 | no → floor default fired | **moved** (`executed` → `degraded`) | +| `tv-neutral.{builtin,registry}` | 2 → 1 | 0.9 → 0.2 | 0.4 → 0.225 | 0.4 | no → floor default fired | byte-equal (already degraded) | +| `tv-short.{builtin,registry}` | 2 → 1 | 0.9 → 0.2 | 0.35 → 0.175 | 0.4 | no → floor default fired | byte-equal (already degraded) | + +`riskBucket` is unmoved on all 12 (it reads `atrRegime`, not the risk axis). `rationale`/`axisNotes` unmoved (the rubric emits no risk note). The risk axis is now **non-constant over the corpus** ({0.2, 0.5}) — via the provider's plan on the plan-bearing CPJ fixture and the declared floor elsewhere. + +## Per-golden identity-hash / input-surface movement + +| JSON path class | goldens | why | +|---|---|---| +| `/canonicalUss/plan`, `/httpResponse/uss/plan`, `/httpResponse/pipelineResult/rawUss/plan` (absent → object) | 4: the two plan-bearing CPJ fixtures × 2 modes | the carried `afi.trade-plan.v1` block | +| `/canonicalUss/provenance/ingestHash` (+ the two response echoes, `/httpResponse/ingestHash`) | same 4 | the CPJ fixture bytes changed (levels re-authored) — ingestHash hashes the CPJ payload | +| `/inputHash/value`, `/evidenceRecord/provenanceRecord/inputHash/value` | same 4 | the canonical USS gained `plan` (decimal strings; afi.hash.v1 admits them verbatim) | +| `/httpResponse/pipelineResult/lenses/0/payload/plan`, `…/_priceFeedMetadata/technicalIndicators/plan` | same 4 | the lane's verified plan facts ride the technical payload | +| `/evidenceRecord/composition/enrichmentHash/value` | same 4 | the technical lens payload gained `plan` | +| `/evidenceRecord/providerInvocations/4/{providerResultHash,categoryResultHash}/value` (technical) | same 4 | the validated technical CategoryResult gained `technical.plan` | +| `/evidenceRecord/providerInvocations/0..4/invocationInputHash/value` (all five lanes) | same 4 | every lane's proof commits to the canonical signal, which gained `plan` | +| `/evidenceRecord/composition/executionSummaryHash/value` | 4: `cpj-coinbase-spot-sell.*`, `tv-long.*` | the scorer node's status flips `executed → degraded` where the floor default is the FIRST fired default (D-DEM-5(3)); the other 8 were already `degraded` by a grandfather firing | +| `/evidenceRecord/composition/analystConfigHash/value` | all 12 | `aa8cf5cf… → 5cb9b7a4…` (mappingRef 1.1.0) | +| `/evidenceRecord/composition/pluginSetHash/value` | all 12 | `220c004e… → 36f911f4…` (three implementationVersion moves) | +| `/outputHash/value`, `/evidenceRecord/provenanceRecord/outputHash/value` | all 12 | the scored output moved (risk axis) | +| `/evidenceRecord/recordHash/value`, `/evidenceRecord/replayHash/value` | all 12 | consequences of the above | + +## Explicitly byte-EQUAL (asserted by the differ across all 12) + +`manifestHash` (the pipeline manifest is untouched), `decayParams`, `uwrProfile`, `scoredSignal.direction`/`meta.direction` (DIR-GOV D-DIR-3: the submitted side is untouched and unread by the producer), every axis other than `risk` (`structure` 0.4, `execution`, `insight` — byte-equal), `riskBucket`, `rationale`/`axisNotes`, the pattern/sentiment/news/aiMl lens payloads, the pattern/sentiment/news/aiMl `providerResultHash`/`categoryResultHash`, every `scorerInput` field other than `rrMultiplePlanned`, `canonicalUss.facts` (still exactly five keys), all six TV goldens' `canonicalUss`/`inputHash`/`ingestHash` (the TV route carries no plan), the `312da118…126e06` provenance golden and `ATLAS_MANIFEST_HASH`. No scoring-law value moved (weights, clamps, rr bands, riskBucket map untouched). + +## D-DEM-5(5) residual, recorded honestly + +On the CPJ route with a complete plan the risk axis is now a function of the provider's verified R:R and `brokeEmaWithBody` — the Evidence-§3 collinearity is **resolved** there. On every plan-less submission (all TradingView/MarkitTick traffic today, and CPJ submissions without a stop or target) the risk axis reads the declared floor and varies only through `brokeEmaWithBody`: **residual** until a plan carrier exists on that route (§9 determination D-5). The R:R is verified-plausible but submitter-chosen; a stop-placement validity rule would be a new threshold (§8) — a modelling filing. diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index 625bbcb1..dfdb6dda 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,20 +2,25 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.0.0", + "implementationVersion": "2.1.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs.", + "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs. DEM-PRODUCER-PLAN producer contract: when the canonical signal carries an afi.trade-plan.v1 plan, the lane verifies every submitted level against the fetched candle window (all levels within one observed-range width of the window; stop and targets on opposite sides of entry) and emits technical.plan; an unverifiable plan REFUSES the determination (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): technical.plan is absent when the signal carries no plan; technical.plan.rrToFirstTarget is absent when the plan carries no stop or no target.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, - "capabilities": ["provider:instance-backed"], + "capabilities": [ + "provider:instance-backed" + ], "defaultTimeoutMs": 10000, "defaultRetryPolicy": { "maxRetries": 1, "retryDelayMs": 500, "backoff": "fixed" }, - "permittedFailurePolicies": ["abort", "degrade"], + "permittedFailurePolicies": [ + "abort", + "degrade" + ], "paramsSchema": { "type": "object", "additionalProperties": false, diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index a33a534b..529dd8dd 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,14 +2,16 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.1.0", + "implementationVersion": "1.2.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", "outputSchemaRef": "afi.enrichment.enriched-view.v1", "deterministic": true, "defaultTimeoutMs": 2000, - "permittedFailurePolicies": ["abort"], + "permittedFailurePolicies": [ + "abort" + ], "paramsSchema": { "type": "object", "additionalProperties": false, diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 3d324a1c..45cf960d 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.3.0", + "implementationVersion": "1.4.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index 06b5733d..da1a4ddc 100644 --- a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,12 +22,21 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.0.0" + "version": "1.1.0" }, "decayConfig": { "ratio": { "barsPerHalfLife": 12, "unknownTimeframeMinutes": 5 } + }, + "metadata": { + "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", + "supersedes": [ + { + "analystConfigHash": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", + "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" + } + ] } } diff --git a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index 0a82d4b6..b0fabc7c 100644 --- a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json b/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json new file mode 100644 index 00000000..f1e9bca6 --- /dev/null +++ b/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json @@ -0,0 +1,99 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.1.0", + "description": "DEM-PRODUCER-PLAN: the 1.0.0 bindings plus rrMultiplePlanned bound to technical.plan.rrToFirstTarget, the technical lane's VERIFIED trade-plan fact (the submitted afi.trade-plan.v1 levels checked against the fetched candles, D-DEM-5(6)). Optional under D-DEM-5(4)(b): the producer emits no R:R for a submission carrying no complete plan; the declared default is the rubric floor (1), hash-committed when fired (DEM-GOV §9 D-5).", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + } + } +} diff --git a/test/oracle/fixtures/cpj/cpj-blofin-perp-long.json b/test/oracle/fixtures/cpj/cpj-blofin-perp-long.json index 443e431f..41ea5d1f 100644 --- a/test/oracle/fixtures/cpj/cpj-blofin-perp-long.json +++ b/test/oracle/fixtures/cpj/cpj-blofin-perp-long.json @@ -5,15 +5,19 @@ "providerId": "oracle-telegram-channel-1", "messageId": "oracle-cpj-msg-0001", "postedAt": "2026-01-15T10:00:00Z", - "rawText": "BTC LONG entry 42500 SL 41800 TP 43500 (oracle fixture)", + "rawText": "BTC LONG entry 50000 SL 49300 TP 51000 (oracle fixture)", "channelName": "Oracle Fixture Channel" }, "extracted": { "symbolRaw": "BTCUSDT", "side": "long", - "entry": 42500, - "stopLoss": 41800, - "takeProfits": [{ "price": 43500 }], + "entry": 50000, + "stopLoss": 49300, + "takeProfits": [ + { + "price": 51000 + } + ], "timeframeHint": "4h", "venueHint": "blofin", "marketTypeHint": "perp" diff --git a/test/oracle/fixtures/cpj/cpj-coinbase-spot-sell.json b/test/oracle/fixtures/cpj/cpj-coinbase-spot-sell.json index cc7f47e8..44d447c2 100644 --- a/test/oracle/fixtures/cpj/cpj-coinbase-spot-sell.json +++ b/test/oracle/fixtures/cpj/cpj-coinbase-spot-sell.json @@ -5,12 +5,12 @@ "providerId": "oracle-telegram-channel-2", "messageId": "oracle-cpj-msg-0002", "postedAt": "2026-01-15T11:00:00Z", - "rawText": "ETH sell 2280 (oracle fixture)" + "rawText": "ETH sell 3001.5 (oracle fixture)" }, "extracted": { "symbolRaw": "ETH-USD", "side": "sell", - "entry": 2280.5, + "entry": 3001.5, "timeframeHint": "1h", "venueHint": "coinbase", "marketTypeHint": "spot" diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json b/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json index a25bedc1..20ad47fa 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json @@ -3,7 +3,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.4916666666666667, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -22,10 +22,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.5, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.4916666666666667 }, "canonicalUss": { "facts": { @@ -35,11 +35,26 @@ "symbol": "BTC/USDT", "timeframe": "4h" }, + "plan": { + "levels": { + "entry": "50000", + "stopLoss": "49300", + "takeProfits": [ + { + "price": "51000" + } + ] + }, + "marketTypeHint": "perp", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", + "venueHint": "blofin" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0001", "cpjParseConfidence": 0.87, "cpjPostedAt": "2026-01-15T10:00:00Z", - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ingestedAt": "", "providerId": "oracle-telegram-channel-1", "providerRef": "Oracle Fixture Channel", @@ -65,13 +80,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "76c5d51aaf4fd3e88cc60cd11af4c8adc92d8180b41e8fa9ad0b09221c32fbdf" + "value": "18bb43eb7798b6bb32f1494e4e6e5aefe6b8c1a66db06f019469dbccc6e51efd" }, "executionSummaryHash": { "algorithm": "sha256", @@ -91,7 +106,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -109,13 +124,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "28c6017db962f2fc5c0b2bc60aeae40cbbda930766383be79bdc3cdfcd38dae7" + "value": "a80523bb6aac8e5073ce0247e87b2bfca55f4567fca4d42e0c15eeacc0321665" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d53f0adf4a948a2e7a30033c4c8d3d4e8e31a31b9032c51ec6d397049958019a" + "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -173,7 +188,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "8702b8a1556f2dd0159dbee32706858c8f316b8e277d9ba6180f2cbb7d831bda" + "value": "f0adfe203a96427dc6df4fdeba3a2581b9b87a494ad05ef88adf19919e9729d9" }, "provider": { "deterministic": false, @@ -228,7 +243,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "9d1d5fd4c9d8a4ec2737391a557f35ad90ec9e55a9665db929d9b31c3e8ebacc" + "value": "2514cfe04fce4ed88f32a88ed3c1390154d0cceee919475c522739a257db2bd0" }, "provider": { "deterministic": false, @@ -282,7 +297,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "c8eb5ca2494e8cab6cb6ea3d854112bec5361b1f450193298c64770de41fbe99" + "value": "326e37a3560b0990a7c41317e884054cb80a6ea4d2a6193ef8f9b2ac7fd80b31" }, "provider": { "deterministic": true, @@ -336,7 +351,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "e863ed9d44ce95ad0ade8846209e39e2d412ba2985195d672626f9e98494f32d" + "value": "302c40bf06714992fdfe3b90567d9d8fc5113e8da49300e0dba187176be60325" }, "provider": { "deterministic": false, @@ -381,7 +396,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "d700b9e2221bfc867533d62aefe1657405022153852e0662fe0ed9089ae24b10" + "value": "8e1b7f10070a10e5e594877f88251155c6d894c820008415b75767ba0db1aed6" }, "credential": { "mode": "keyless" @@ -390,7 +405,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "d89aedc4c0c2d1b09900b195690be54707af0acd6bff8dd669a86ce05e15b6ee" + "value": "d26929d750140bfee0eca63136a490c4c3be3a93fcbb7bec8d1182be0b0eed7c" }, "priceSource": "demo", "provider": { @@ -419,7 +434,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "92f5e94a04420fec5d91efd38fe07273edaa5870a37a4cde58747f5369094f8c" + "value": "c69857869e58c01bac4bc993681d1f4449de421d7c9ed8ddb029646f578f096c" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -430,19 +445,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "a8f27363728fdcc5d2079f6eccf4494279fdfeb3984159340cf41a548c3e6d52" + "value": "872c97d513bc807761512571204cd8840265d9b9d8217208735fc0c345605a75" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "5867dc5ab1bb27eb4517e73ffd0f54cb25833bcfccfb54c74310ecf7bc547311" + "value": "ec9f4c5b3b41835ffff942e10611b33640fe8a483342a8461d9a28c0005e5d05" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.5916666666666667, + "conviction": 0.4916666666666667, "direction": "long", "provenanceRecordRef": "provenance-record:cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "providerId": "oracle-telegram-channel-1", @@ -454,10 +469,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.5, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.4916666666666667 }, "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "strategyId": "trend_pullback_v1", @@ -470,7 +485,7 @@ } }, "httpResponse": { - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ok": true, "persistence": { "lifecycleState": "SCORED", @@ -513,6 +528,18 @@ "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 50000, + "entryLow": 50000, + "entryPrice": 50000, + "envelopeHigh": 50821.013653806585, + "envelopeLow": 49146.90317099503, + "firstTargetPrice": 51000, + "rrToFirstTarget": 1.4286, + "stopPrice": 49300, + "targetCount": 1 + }, "rsi14": 53.96469980585331, "trendBias": "range", "volumeRatio": 1.49681802597411 @@ -523,7 +550,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.4916666666666667, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -542,10 +569,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.5, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.4916666666666667 }, "decayParams": { "barsPerHalfLife": 12, @@ -564,6 +591,18 @@ "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 50000, + "entryLow": 50000, + "entryPrice": 50000, + "envelopeHigh": 50821.013653806585, + "envelopeLow": 49146.90317099503, + "firstTargetPrice": 51000, + "rrToFirstTarget": 1.4286, + "stopPrice": 49300, + "targetCount": 1 + }, "rsi14": 53.96469980585331, "trendBias": "range", "volumeRatio": 1.49681802597411 @@ -667,11 +706,26 @@ "symbol": "BTC/USDT", "timeframe": "4h" }, + "plan": { + "levels": { + "entry": "50000", + "stopLoss": "49300", + "takeProfits": [ + { + "price": "51000" + } + ] + }, + "marketTypeHint": "perp", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", + "venueHint": "blofin" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0001", "cpjParseConfidence": 0.87, "cpjPostedAt": "2026-01-15T10:00:00Z", - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ingestedAt": "", "providerId": "oracle-telegram-channel-1", "providerRef": "Oracle Fixture Channel", @@ -695,11 +749,26 @@ "symbol": "BTC/USDT", "timeframe": "4h" }, + "plan": { + "levels": { + "entry": "50000", + "stopLoss": "49300", + "takeProfits": [ + { + "price": "51000" + } + ] + }, + "marketTypeHint": "perp", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", + "venueHint": "blofin" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0001", "cpjParseConfidence": 0.87, "cpjPostedAt": "2026-01-15T10:00:00Z", - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ingestedAt": "", "providerId": "oracle-telegram-channel-1", "providerRef": "Oracle Fixture Channel", @@ -714,13 +783,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "28c6017db962f2fc5c0b2bc60aeae40cbbda930766383be79bdc3cdfcd38dae7" + "value": "a80523bb6aac8e5073ce0247e87b2bfca55f4567fca4d42e0c15eeacc0321665" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d53f0adf4a948a2e7a30033c4c8d3d4e8e31a31b9032c51ec6d397049958019a" + "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" }, "scorerInput": { "atrRegime": "normal", @@ -730,7 +799,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1.4286, "triggerPatternQuality": 2, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json b/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json index fefa610e..c9579269 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json @@ -3,7 +3,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.4916666666666667, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -22,10 +22,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.5, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.4916666666666667 }, "canonicalUss": { "facts": { @@ -35,11 +35,26 @@ "symbol": "BTC/USDT", "timeframe": "4h" }, + "plan": { + "levels": { + "entry": "50000", + "stopLoss": "49300", + "takeProfits": [ + { + "price": "51000" + } + ] + }, + "marketTypeHint": "perp", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", + "venueHint": "blofin" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0001", "cpjParseConfidence": 0.87, "cpjPostedAt": "2026-01-15T10:00:00Z", - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ingestedAt": "", "providerId": "oracle-telegram-channel-1", "providerRef": "Oracle Fixture Channel", @@ -65,13 +80,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "76c5d51aaf4fd3e88cc60cd11af4c8adc92d8180b41e8fa9ad0b09221c32fbdf" + "value": "18bb43eb7798b6bb32f1494e4e6e5aefe6b8c1a66db06f019469dbccc6e51efd" }, "executionSummaryHash": { "algorithm": "sha256", @@ -91,7 +106,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -109,13 +124,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "28c6017db962f2fc5c0b2bc60aeae40cbbda930766383be79bdc3cdfcd38dae7" + "value": "a80523bb6aac8e5073ce0247e87b2bfca55f4567fca4d42e0c15eeacc0321665" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d53f0adf4a948a2e7a30033c4c8d3d4e8e31a31b9032c51ec6d397049958019a" + "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -173,7 +188,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "8702b8a1556f2dd0159dbee32706858c8f316b8e277d9ba6180f2cbb7d831bda" + "value": "f0adfe203a96427dc6df4fdeba3a2581b9b87a494ad05ef88adf19919e9729d9" }, "provider": { "deterministic": false, @@ -228,7 +243,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "9d1d5fd4c9d8a4ec2737391a557f35ad90ec9e55a9665db929d9b31c3e8ebacc" + "value": "2514cfe04fce4ed88f32a88ed3c1390154d0cceee919475c522739a257db2bd0" }, "provider": { "deterministic": false, @@ -282,7 +297,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "c8eb5ca2494e8cab6cb6ea3d854112bec5361b1f450193298c64770de41fbe99" + "value": "326e37a3560b0990a7c41317e884054cb80a6ea4d2a6193ef8f9b2ac7fd80b31" }, "provider": { "deterministic": true, @@ -336,7 +351,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "e863ed9d44ce95ad0ade8846209e39e2d412ba2985195d672626f9e98494f32d" + "value": "302c40bf06714992fdfe3b90567d9d8fc5113e8da49300e0dba187176be60325" }, "provider": { "deterministic": false, @@ -381,7 +396,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "d700b9e2221bfc867533d62aefe1657405022153852e0662fe0ed9089ae24b10" + "value": "8e1b7f10070a10e5e594877f88251155c6d894c820008415b75767ba0db1aed6" }, "credential": { "mode": "keyless" @@ -390,7 +405,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "d89aedc4c0c2d1b09900b195690be54707af0acd6bff8dd669a86ce05e15b6ee" + "value": "d26929d750140bfee0eca63136a490c4c3be3a93fcbb7bec8d1182be0b0eed7c" }, "priceSource": "demo", "provider": { @@ -419,7 +434,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "92f5e94a04420fec5d91efd38fe07273edaa5870a37a4cde58747f5369094f8c" + "value": "c69857869e58c01bac4bc993681d1f4449de421d7c9ed8ddb029646f578f096c" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -430,19 +445,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "d38c486d600d0688a89065b9ab776892591cf77914ccc3bf64a171bbf0a35700" + "value": "ccb66974486ea9a30de99b307037aabf849983451cb7bd25f0f3611ad77f824e" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "77fc54638146048bce83ab20de51e25fdc280702baf927c19444ee95f609b56d" + "value": "bb0c4e6e9554207cb6d9b36190992e9ad43cfd0f74db78f0cd1f0409f6aaa0c6" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.5916666666666667, + "conviction": 0.4916666666666667, "direction": "long", "provenanceRecordRef": "provenance-record:cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "providerId": "oracle-telegram-channel-1", @@ -454,10 +469,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.5, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.4916666666666667 }, "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "strategyId": "trend_pullback_v1", @@ -470,7 +485,7 @@ } }, "httpResponse": { - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ok": true, "persistence": { "lifecycleState": "SCORED", @@ -513,6 +528,18 @@ "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 50000, + "entryLow": 50000, + "entryPrice": 50000, + "envelopeHigh": 50821.013653806585, + "envelopeLow": 49146.90317099503, + "firstTargetPrice": 51000, + "rrToFirstTarget": 1.4286, + "stopPrice": 49300, + "targetCount": 1 + }, "rsi14": 53.96469980585331, "trendBias": "range", "volumeRatio": 1.49681802597411 @@ -523,7 +550,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.4916666666666667, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -542,10 +569,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.5, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.4916666666666667 }, "decayParams": { "barsPerHalfLife": 12, @@ -564,6 +591,18 @@ "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 50000, + "entryLow": 50000, + "entryPrice": 50000, + "envelopeHigh": 50821.013653806585, + "envelopeLow": 49146.90317099503, + "firstTargetPrice": 51000, + "rrToFirstTarget": 1.4286, + "stopPrice": 49300, + "targetCount": 1 + }, "rsi14": 53.96469980585331, "trendBias": "range", "volumeRatio": 1.49681802597411 @@ -667,11 +706,26 @@ "symbol": "BTC/USDT", "timeframe": "4h" }, + "plan": { + "levels": { + "entry": "50000", + "stopLoss": "49300", + "takeProfits": [ + { + "price": "51000" + } + ] + }, + "marketTypeHint": "perp", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", + "venueHint": "blofin" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0001", "cpjParseConfidence": 0.87, "cpjPostedAt": "2026-01-15T10:00:00Z", - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ingestedAt": "", "providerId": "oracle-telegram-channel-1", "providerRef": "Oracle Fixture Channel", @@ -695,11 +749,26 @@ "symbol": "BTC/USDT", "timeframe": "4h" }, + "plan": { + "levels": { + "entry": "50000", + "stopLoss": "49300", + "takeProfits": [ + { + "price": "51000" + } + ] + }, + "marketTypeHint": "perp", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", + "venueHint": "blofin" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0001", "cpjParseConfidence": 0.87, "cpjPostedAt": "2026-01-15T10:00:00Z", - "ingestHash": "30e3b29854aca4c1e2d7883ab07ccec5ac9f914f390d18f4a4633f7e0e8cc979", + "ingestHash": "d5f4b7bb3d26facb9797a2ba2bd45b16446af4be0b29731b33aa334d3a7294c4", "ingestedAt": "", "providerId": "oracle-telegram-channel-1", "providerRef": "Oracle Fixture Channel", @@ -714,13 +783,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "28c6017db962f2fc5c0b2bc60aeae40cbbda930766383be79bdc3cdfcd38dae7" + "value": "a80523bb6aac8e5073ce0247e87b2bfca55f4567fca4d42e0c15eeacc0321665" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d53f0adf4a948a2e7a30033c4c8d3d4e8e31a31b9032c51ec6d397049958019a" + "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" }, "scorerInput": { "atrRegime": "normal", @@ -730,7 +799,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1.4286, "triggerPatternQuality": 2, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json index feaabb34..88a30bdb 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json @@ -7,7 +7,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "SOL", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -27,10 +27,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "canonicalUss": { "facts": { @@ -70,7 +70,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -96,7 +96,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -120,7 +120,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "796fa7e279b2433cf35f7f5c85dc6e20e689cbab8463e70940330ecaac26a98c" + "value": "dfe337e65e6568af9bf10c01523b8eccf5342fb6f9f3bf27b89d423f5b92c4b4" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -435,19 +435,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "bfa23ce42be174a80ced20342b0784d15139560487634dc6a08834e6f033f06c" + "value": "2d3c61dc5d15cce19420deb2174801228d38291a94cd437578c3c09c0e65fcff" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "717635a7456d22bca03fa37f1c62a7b8eab2e5dc0beaa83acc769835c7277469" + "value": "562068cd1d9b3015249bc7982c80863e05297591994d5c959ce98e924a650cdc" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "provenanceRecordRef": "provenance-record:cpj-discord-oracle-discord-guild-3-oracle-cpj-msg-0003", "providerId": "oracle-discord-guild-3", @@ -459,10 +459,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "signalId": "cpj-discord-oracle-discord-guild-3-oracle-cpj-msg-0003", "strategyId": "trend_pullback_v1", @@ -520,7 +520,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "SOL", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -540,10 +540,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "decayParams": { "barsPerHalfLife": 12, @@ -682,7 +682,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "796fa7e279b2433cf35f7f5c85dc6e20e689cbab8463e70940330ecaac26a98c" + "value": "dfe337e65e6568af9bf10c01523b8eccf5342fb6f9f3bf27b89d423f5b92c4b4" }, "scorerInput": { "atrRegime": "high", @@ -692,7 +692,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 0, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json index fb4cf944..6a27f570 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json @@ -7,7 +7,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "SOL", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -27,10 +27,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "canonicalUss": { "facts": { @@ -70,7 +70,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -96,7 +96,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -120,7 +120,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "796fa7e279b2433cf35f7f5c85dc6e20e689cbab8463e70940330ecaac26a98c" + "value": "dfe337e65e6568af9bf10c01523b8eccf5342fb6f9f3bf27b89d423f5b92c4b4" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -435,19 +435,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "841339e347fffafac98cd92185492ec493de9cd7a444449430ac1940f0714a81" + "value": "746c77f73e55fb392f42f20cacf4a31073d40fa4e18eaf410ab9b531510a4787" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "bbbbe59595894c6ff3fd3f4d51ab046079531c1dad91ac6454a462fb2c29cf53" + "value": "8ee23c624347df6f11ab3c3a56231f0579091f91edfd7e6d86688e2281a717d1" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "provenanceRecordRef": "provenance-record:cpj-discord-oracle-discord-guild-3-oracle-cpj-msg-0003", "providerId": "oracle-discord-guild-3", @@ -459,10 +459,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "signalId": "cpj-discord-oracle-discord-guild-3-oracle-cpj-msg-0003", "strategyId": "trend_pullback_v1", @@ -520,7 +520,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "SOL", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -540,10 +540,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "decayParams": { "barsPerHalfLife": 12, @@ -682,7 +682,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "796fa7e279b2433cf35f7f5c85dc6e20e689cbab8463e70940330ecaac26a98c" + "value": "dfe337e65e6568af9bf10c01523b8eccf5342fb6f9f3bf27b89d423f5b92c4b4" }, "scorerInput": { "atrRegime": "high", @@ -692,7 +692,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 0, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json index 098b942e..cd351d9d 100644 --- a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json +++ b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json @@ -3,7 +3,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "ETH", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -22,10 +22,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "canonicalUss": { "facts": { @@ -35,11 +35,20 @@ "symbol": "ETH/USD", "timeframe": "1h" }, + "plan": { + "levels": { + "entry": "3001.5" + }, + "marketTypeHint": "spot", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", + "venueHint": "coinbase" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0002", "cpjParseConfidence": 0.6180339887, "cpjPostedAt": "2026-01-15T11:00:00Z", - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ingestedAt": "", "providerId": "oracle-telegram-channel-2", "providerRef": "oracle-telegram-channel-2", @@ -65,19 +74,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "4eddd28c441aaf46637adfb3f018f4d7074931aeef2ebaaddea61831335e7dad" + "value": "0f706295d4b2180475d044b2931bf6fd51c89789a59da082619adfb1380ff4a2" }, "executionSummaryHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.execution-summary", - "value": "9b23d299c792c6f97f50afaef26d931f8feefe783d9bd375200e9d09978630b4" + "value": "1178e36dcbad0110b42c69bfdff3917592c5d82fef49401d815b09e7a20b5013" }, "manifestHash": { "algorithm": "sha256", @@ -91,7 +100,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -109,13 +118,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "fdf5fe2e03ebb5a62cf8cf71e2416e07d0208cff527826ad27b534c873f1a7d8" + "value": "7f1c18840dd4a715f256e4b91cd4e5f3adf6ad2b8436ab32f10461d2938b16d9" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "03e31a8e4374c7fc4e4a51c2a8f418902041e2ad64ed1fca62576e7373e36e9d" + "value": "0d6692715c09c0c89531d8f1e359761e7e2c8ca9610196992395bbf9501de876" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -173,7 +182,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "39c93e84deb72187cdd7cba99eed77fc03668bacf0568553ab4bddc3373c0295" + "value": "b55250716818e6f3faaaf74ccf4e0569f17268469606969afdcd0b1267eb505e" }, "provider": { "deterministic": false, @@ -228,7 +237,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "6b93561dc4c33405af16bd592f9666b4c8f9be53fc46d566e99cd69c1bb5852f" + "value": "8974684c2c52e6912f38159e3ee0e44cefa936139303320d0ca6a21792658b45" }, "provider": { "deterministic": false, @@ -282,7 +291,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "7feef5dbd1b0613a67b6751f1dfd6e3a4bae49f729f4625ff32934116a2d6a02" + "value": "dc53fecbb804800cbf19399949d5144bdfb583d9dfd3cb68a2574081ec4b4fe3" }, "provider": { "deterministic": true, @@ -336,7 +345,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "e913e6836cd39ceee8f4d54844d688c1ca3e22a1c575a90a0ebbcb5c55d438cd" + "value": "1586265552293589ecc4838cdb0bf78667cf441478c0d69eefff05bd8ff56566" }, "provider": { "deterministic": false, @@ -381,7 +390,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "3d06a2f88967ba527788f8895838f3965ebf35a0904bd8c16b0129649c9094ef" + "value": "435bb57dc7dc5f3ada1691e1e4a4f7fb2b35ea6c16e9bc9435bca67cb905c552" }, "credential": { "mode": "keyless" @@ -390,7 +399,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "f12cc96839ed2de196a14f799e346f9ad8e2f09c0ac11dacb3aabaf48941ec73" + "value": "9677821cd94f391031f2dfca16b78467912918d8da2308f90c84f0db2566a8fb" }, "priceSource": "demo", "provider": { @@ -419,7 +428,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "a5fbb1c2bb13428bd3513edede61517fbacaa57024c49917cc48beb1bd4db1ba" + "value": "1323407907c4a9eefc8eaa4650b386e49a8f582c21299017d17d43d09ea9d403" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -430,19 +439,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "59eb2c9faebcf82c4abaf9d663f123c9619fb48df464d1839068c3bfa1908e14" + "value": "33a4e9286ed946bc360d2efb59dbd40e3a26a548f5a111e279f3a13eec4f179c" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "1f2e31f69f481d8cb97ff82739733f46f46181aba9b6540844fad86bf079aca4" + "value": "ca32f15d3377c4c685da0f7727521366bf6bd07188386a92347a06220f78b430" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "short", "provenanceRecordRef": "provenance-record:cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", "providerId": "oracle-telegram-channel-2", @@ -454,10 +463,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", "strategyId": "trend_pullback_v1", @@ -470,7 +479,7 @@ } }, "httpResponse": { - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ok": true, "persistence": { "lifecycleState": "SCORED", @@ -513,6 +522,15 @@ "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 3001.5, + "entryLow": 3001.5, + "entryPrice": 3001.5, + "envelopeHigh": 3045.258508126984, + "envelopeLow": 2950.71729627225, + "targetCount": 0 + }, "rsi14": 45.747266481159215, "trendBias": "range", "volumeRatio": 0.7842600319383496 @@ -523,7 +541,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "ETH", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -542,10 +560,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "decayParams": { "barsPerHalfLife": 12, @@ -564,6 +582,15 @@ "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 3001.5, + "entryLow": 3001.5, + "entryPrice": 3001.5, + "envelopeHigh": 3045.258508126984, + "envelopeLow": 2950.71729627225, + "targetCount": 0 + }, "rsi14": 45.747266481159215, "trendBias": "range", "volumeRatio": 0.7842600319383496 @@ -667,11 +694,20 @@ "symbol": "ETH/USD", "timeframe": "1h" }, + "plan": { + "levels": { + "entry": "3001.5" + }, + "marketTypeHint": "spot", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", + "venueHint": "coinbase" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0002", "cpjParseConfidence": 0.6180339887, "cpjPostedAt": "2026-01-15T11:00:00Z", - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ingestedAt": "", "providerId": "oracle-telegram-channel-2", "providerRef": "oracle-telegram-channel-2", @@ -695,11 +731,20 @@ "symbol": "ETH/USD", "timeframe": "1h" }, + "plan": { + "levels": { + "entry": "3001.5" + }, + "marketTypeHint": "spot", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", + "venueHint": "coinbase" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0002", "cpjParseConfidence": 0.6180339887, "cpjPostedAt": "2026-01-15T11:00:00Z", - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ingestedAt": "", "providerId": "oracle-telegram-channel-2", "providerRef": "oracle-telegram-channel-2", @@ -714,13 +759,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "fdf5fe2e03ebb5a62cf8cf71e2416e07d0208cff527826ad27b534c873f1a7d8" + "value": "7f1c18840dd4a715f256e4b91cd4e5f3adf6ad2b8436ab32f10461d2938b16d9" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "03e31a8e4374c7fc4e4a51c2a8f418902041e2ad64ed1fca62576e7373e36e9d" + "value": "0d6692715c09c0c89531d8f1e359761e7e2c8ca9610196992395bbf9501de876" }, "scorerInput": { "atrRegime": "normal", @@ -730,7 +775,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 2, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json index 83bdf4ac..f023b492 100644 --- a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json +++ b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json @@ -3,7 +3,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "ETH", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -22,10 +22,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "canonicalUss": { "facts": { @@ -35,11 +35,20 @@ "symbol": "ETH/USD", "timeframe": "1h" }, + "plan": { + "levels": { + "entry": "3001.5" + }, + "marketTypeHint": "spot", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", + "venueHint": "coinbase" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0002", "cpjParseConfidence": 0.6180339887, "cpjPostedAt": "2026-01-15T11:00:00Z", - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ingestedAt": "", "providerId": "oracle-telegram-channel-2", "providerRef": "oracle-telegram-channel-2", @@ -65,19 +74,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "4eddd28c441aaf46637adfb3f018f4d7074931aeef2ebaaddea61831335e7dad" + "value": "0f706295d4b2180475d044b2931bf6fd51c89789a59da082619adfb1380ff4a2" }, "executionSummaryHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.execution-summary", - "value": "9b23d299c792c6f97f50afaef26d931f8feefe783d9bd375200e9d09978630b4" + "value": "1178e36dcbad0110b42c69bfdff3917592c5d82fef49401d815b09e7a20b5013" }, "manifestHash": { "algorithm": "sha256", @@ -91,7 +100,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -109,13 +118,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "fdf5fe2e03ebb5a62cf8cf71e2416e07d0208cff527826ad27b534c873f1a7d8" + "value": "7f1c18840dd4a715f256e4b91cd4e5f3adf6ad2b8436ab32f10461d2938b16d9" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "03e31a8e4374c7fc4e4a51c2a8f418902041e2ad64ed1fca62576e7373e36e9d" + "value": "0d6692715c09c0c89531d8f1e359761e7e2c8ca9610196992395bbf9501de876" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -173,7 +182,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "39c93e84deb72187cdd7cba99eed77fc03668bacf0568553ab4bddc3373c0295" + "value": "b55250716818e6f3faaaf74ccf4e0569f17268469606969afdcd0b1267eb505e" }, "provider": { "deterministic": false, @@ -228,7 +237,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "6b93561dc4c33405af16bd592f9666b4c8f9be53fc46d566e99cd69c1bb5852f" + "value": "8974684c2c52e6912f38159e3ee0e44cefa936139303320d0ca6a21792658b45" }, "provider": { "deterministic": false, @@ -282,7 +291,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "7feef5dbd1b0613a67b6751f1dfd6e3a4bae49f729f4625ff32934116a2d6a02" + "value": "dc53fecbb804800cbf19399949d5144bdfb583d9dfd3cb68a2574081ec4b4fe3" }, "provider": { "deterministic": true, @@ -336,7 +345,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "e913e6836cd39ceee8f4d54844d688c1ca3e22a1c575a90a0ebbcb5c55d438cd" + "value": "1586265552293589ecc4838cdb0bf78667cf441478c0d69eefff05bd8ff56566" }, "provider": { "deterministic": false, @@ -381,7 +390,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "3d06a2f88967ba527788f8895838f3965ebf35a0904bd8c16b0129649c9094ef" + "value": "435bb57dc7dc5f3ada1691e1e4a4f7fb2b35ea6c16e9bc9435bca67cb905c552" }, "credential": { "mode": "keyless" @@ -390,7 +399,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "f12cc96839ed2de196a14f799e346f9ad8e2f09c0ac11dacb3aabaf48941ec73" + "value": "9677821cd94f391031f2dfca16b78467912918d8da2308f90c84f0db2566a8fb" }, "priceSource": "demo", "provider": { @@ -419,7 +428,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "a5fbb1c2bb13428bd3513edede61517fbacaa57024c49917cc48beb1bd4db1ba" + "value": "1323407907c4a9eefc8eaa4650b386e49a8f582c21299017d17d43d09ea9d403" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -430,19 +439,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "8499d1f7a7b6b7b91ed0bcf8eb889f3cae3312b2bfef7dfb821337c4e73b44d0" + "value": "6b9c06c256913209f0338fda4562d9730ce01062755ddf0cdba8f5c8abb11cf7" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "9f4bebc69e8f2685b80b14a7ee5b45756b20e8601a1741617d7b9a49440eeed2" + "value": "a883346b80e6f93a802f567dabc6432c4928029fd0629350d3ad2a8f71bcfd95" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "short", "provenanceRecordRef": "provenance-record:cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", "providerId": "oracle-telegram-channel-2", @@ -454,10 +463,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", "strategyId": "trend_pullback_v1", @@ -470,7 +479,7 @@ } }, "httpResponse": { - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ok": true, "persistence": { "lifecycleState": "SCORED", @@ -513,6 +522,15 @@ "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 3001.5, + "entryLow": 3001.5, + "entryPrice": 3001.5, + "envelopeHigh": 3045.258508126984, + "envelopeLow": 2950.71729627225, + "targetCount": 0 + }, "rsi14": 45.747266481159215, "trendBias": "range", "volumeRatio": 0.7842600319383496 @@ -523,7 +541,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "ETH", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -542,10 +560,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "decayParams": { "barsPerHalfLife": 12, @@ -564,6 +582,15 @@ "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, "isInValueSweetSpot": true, + "plan": { + "barCount": 100, + "entryHigh": 3001.5, + "entryLow": 3001.5, + "entryPrice": 3001.5, + "envelopeHigh": 3045.258508126984, + "envelopeLow": 2950.71729627225, + "targetCount": 0 + }, "rsi14": 45.747266481159215, "trendBias": "range", "volumeRatio": 0.7842600319383496 @@ -667,11 +694,20 @@ "symbol": "ETH/USD", "timeframe": "1h" }, + "plan": { + "levels": { + "entry": "3001.5" + }, + "marketTypeHint": "spot", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", + "venueHint": "coinbase" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0002", "cpjParseConfidence": 0.6180339887, "cpjPostedAt": "2026-01-15T11:00:00Z", - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ingestedAt": "", "providerId": "oracle-telegram-channel-2", "providerRef": "oracle-telegram-channel-2", @@ -695,11 +731,20 @@ "symbol": "ETH/USD", "timeframe": "1h" }, + "plan": { + "levels": { + "entry": "3001.5" + }, + "marketTypeHint": "spot", + "schema": "afi.trade-plan.v1", + "signalId": "cpj-telegram-oracle-telegram-channel-2-oracle-cpj-msg-0002", + "venueHint": "coinbase" + }, "provenance": { "cpjMessageId": "oracle-cpj-msg-0002", "cpjParseConfidence": 0.6180339887, "cpjPostedAt": "2026-01-15T11:00:00Z", - "ingestHash": "5c89a9e9c8ef090a4da758910ac33fed69bc887de2d7a8c4a6224473e5ca978c", + "ingestHash": "b7bfe2d26f182396659e2548f6e7021a64a02f71e7e349a147c2c6f9716614cf", "ingestedAt": "", "providerId": "oracle-telegram-channel-2", "providerRef": "oracle-telegram-channel-2", @@ -714,13 +759,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.signal-input", - "value": "fdf5fe2e03ebb5a62cf8cf71e2416e07d0208cff527826ad27b534c873f1a7d8" + "value": "7f1c18840dd4a715f256e4b91cd4e5f3adf6ad2b8436ab32f10461d2938b16d9" }, "outputHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "03e31a8e4374c7fc4e4a51c2a8f418902041e2ad64ed1fca62576e7373e36e9d" + "value": "0d6692715c09c0c89531d8f1e359761e7e2c8ca9610196992395bbf9501de876" }, "scorerInput": { "atrRegime": "normal", @@ -730,7 +775,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 2, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/tv-long.builtin.json b/test/oracle/goldens/enriched/tv-long.builtin.json index c7e76d22..49189cec 100644 --- a/test/oracle/goldens/enriched/tv-long.builtin.json +++ b/test/oracle/goldens/enriched/tv-long.builtin.json @@ -3,7 +3,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -22,10 +22,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "canonicalUss": { "facts": { @@ -62,7 +62,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -74,7 +74,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.execution-summary", - "value": "9b23d299c792c6f97f50afaef26d931f8feefe783d9bd375200e9d09978630b4" + "value": "1178e36dcbad0110b42c69bfdff3917592c5d82fef49401d815b09e7a20b5013" }, "manifestHash": { "algorithm": "sha256", @@ -88,7 +88,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -112,7 +112,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "5af3fcb6ba64bc5ec380676653de57607607d779d9906125b681f8feb0a2502c" + "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -427,19 +427,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "19178400ced77a5fe6457f22fa491ead6b8a78e53742680cdbec80a384078d8f" + "value": "099bf59336c702007135dcd802f7eb16d67b1bd884159d096cc2adad7ec1ad60" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "465fd154fcfbc04f53f6be76c682b1d465bed24cfa5c4ecdfe991f9e8e8b9bab" + "value": "98d1dfaefe53f2964648b1a5dfd41c799c26581d887b2eb93e8baa01fef2fe74" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "long", "provenanceRecordRef": "provenance-record:oracle-tv-long-0001", "providerId": "oracle-provider-tv", @@ -451,10 +451,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "signalId": "oracle-tv-long-0001", "strategyId": "trend_pullback_v1", @@ -511,7 +511,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -530,10 +530,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "decayParams": { "barsPerHalfLife": 12, @@ -686,7 +686,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "5af3fcb6ba64bc5ec380676653de57607607d779d9906125b681f8feb0a2502c" + "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" }, "scorerInput": { "atrRegime": "normal", @@ -696,7 +696,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 2, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/tv-long.registry.json b/test/oracle/goldens/enriched/tv-long.registry.json index e3334832..9e7f66e3 100644 --- a/test/oracle/goldens/enriched/tv-long.registry.json +++ b/test/oracle/goldens/enriched/tv-long.registry.json @@ -3,7 +3,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -22,10 +22,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "canonicalUss": { "facts": { @@ -62,7 +62,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -74,7 +74,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.execution-summary", - "value": "9b23d299c792c6f97f50afaef26d931f8feefe783d9bd375200e9d09978630b4" + "value": "1178e36dcbad0110b42c69bfdff3917592c5d82fef49401d815b09e7a20b5013" }, "manifestHash": { "algorithm": "sha256", @@ -88,7 +88,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -112,7 +112,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "5af3fcb6ba64bc5ec380676653de57607607d779d9906125b681f8feb0a2502c" + "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -427,19 +427,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "09cc8b17bbeadbd6e5051ead3c398c6dafcb2a387c9c728d3e508d80afd47898" + "value": "4515e1afc437909c07930adbb85c25cb4cf2de16054025e5c2b6cb15400dab3c" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "3d544926cf935c9110b09f0ba848e60fd9c102c6aa8c62c6a69dc44ffb02464c" + "value": "1d1619ea81e682bcdecf5c492c2ba1bc296feafd6af8fec279738d50860b678b" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "long", "provenanceRecordRef": "provenance-record:oracle-tv-long-0001", "providerId": "oracle-provider-tv", @@ -451,10 +451,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "signalId": "oracle-tv-long-0001", "strategyId": "trend_pullback_v1", @@ -511,7 +511,7 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.5916666666666667, + "conviction": 0.41666666666666663, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -530,10 +530,10 @@ "uwrAxes": { "execution": 0.6666666666666666, "insight": 0.4, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.5916666666666667 + "uwrScore": 0.41666666666666663 }, "decayParams": { "barsPerHalfLife": 12, @@ -686,7 +686,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "5af3fcb6ba64bc5ec380676653de57607607d779d9906125b681f8feb0a2502c" + "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" }, "scorerInput": { "atrRegime": "normal", @@ -696,7 +696,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 2, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/tv-neutral.builtin.json b/test/oracle/goldens/enriched/tv-neutral.builtin.json index daa8126f..b10e754c 100644 --- a/test/oracle/goldens/enriched/tv-neutral.builtin.json +++ b/test/oracle/goldens/enriched/tv-neutral.builtin.json @@ -7,7 +7,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -27,10 +27,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "canonicalUss": { "facts": { @@ -67,7 +67,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -117,7 +117,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "1cd9b6f4c34c4ae5a7ce4053515542d8cd1a8712fa9a179fbe278b9779bb0924" + "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -432,19 +432,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "52c64a7cb8071a6495e4fc2d984ec831a84f2212f5a4b85d13e69c2160899b3a" + "value": "62d321e11ad3dd92e2d125976f1c6971207089ba25d49fdb65fd3f4d24763448" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "bacec88b67e3b97de52da812f039bef79ef2f3670013502112f17d7d2424dfdf" + "value": "74884fb9b27731275bd99e8707e57493ab24049e85c48bd81911fc79a8378aa1" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "provenanceRecordRef": "provenance-record:oracle-tv-neutral-0001", "providerId": "oracle-provider-tv", @@ -456,10 +456,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "signalId": "oracle-tv-neutral-0001", "strategyId": "trend_pullback_v1", @@ -508,7 +508,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -528,10 +528,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "decayParams": { "barsPerHalfLife": 12, @@ -648,7 +648,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "1cd9b6f4c34c4ae5a7ce4053515542d8cd1a8712fa9a179fbe278b9779bb0924" + "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" }, "scorerInput": { "atrRegime": "high", @@ -658,7 +658,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 0, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/tv-neutral.registry.json b/test/oracle/goldens/enriched/tv-neutral.registry.json index 54c74a91..8882e4f9 100644 --- a/test/oracle/goldens/enriched/tv-neutral.registry.json +++ b/test/oracle/goldens/enriched/tv-neutral.registry.json @@ -7,7 +7,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -27,10 +27,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "canonicalUss": { "facts": { @@ -67,7 +67,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -117,7 +117,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "1cd9b6f4c34c4ae5a7ce4053515542d8cd1a8712fa9a179fbe278b9779bb0924" + "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -432,19 +432,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "82f9c2b5d3eeb89591e70e254fa2da4af2568b64344af0e398f00a94b616d516" + "value": "eda306ba90a6044983f85e587e50483315d9369b1d5510c80a77960f9de00c84" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "27947833f82966bfb1610b8cf5b4cbcf89121a48be638e7be757b5bafe336c41" + "value": "3c41aae7db36f10475805e59fd171cd7ad19769dd60b7fa8170c4b3c4c731c60" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "provenanceRecordRef": "provenance-record:oracle-tv-neutral-0001", "providerId": "oracle-provider-tv", @@ -456,10 +456,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "signalId": "oracle-tv-neutral-0001", "strategyId": "trend_pullback_v1", @@ -508,7 +508,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.4, + "conviction": 0.22500000000000003, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -528,10 +528,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.4 + "uwrScore": 0.22500000000000003 }, "decayParams": { "barsPerHalfLife": 12, @@ -648,7 +648,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "1cd9b6f4c34c4ae5a7ce4053515542d8cd1a8712fa9a179fbe278b9779bb0924" + "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" }, "scorerInput": { "atrRegime": "high", @@ -658,7 +658,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 0, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/tv-short.builtin.json b/test/oracle/goldens/enriched/tv-short.builtin.json index 185e3afb..c41e08d9 100644 --- a/test/oracle/goldens/enriched/tv-short.builtin.json +++ b/test/oracle/goldens/enriched/tv-short.builtin.json @@ -7,7 +7,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.35000000000000003, + "conviction": 0.17500000000000002, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -27,10 +27,10 @@ "uwrAxes": { "execution": 0, "insight": 0.1, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.35000000000000003 + "uwrScore": 0.17500000000000002 }, "canonicalUss": { "facts": { @@ -67,7 +67,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -117,7 +117,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "81a258ce32bd25660b808d6404254a154d8208726851ed08809464a523282e53" + "value": "53510590e062dcbc12eaefbd5f14bdf2e3aad952ca6eb08a4a335a1e2ac7e7c7" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -432,19 +432,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "e53092704c001078b95a1459991e3382ee47e5b71bfd1813b42ba751daf59e75" + "value": "9e81a4e83e1d40d065e5ab846530a4a819ce658672ea810e479b9075a576a24c" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "7f6c0c5f7ebc1861d4060a5c913f6c961f6427b70afa541589927e9207e681ad" + "value": "e3e0ce534f54d4332c0abea71beaadc0c2b3c40b185b94a63943f2d50f5110a1" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.35000000000000003, + "conviction": 0.17500000000000002, "direction": "short", "provenanceRecordRef": "provenance-record:oracle-tv-short-0001", "providerId": "oracle-provider-tv", @@ -456,10 +456,10 @@ "uwrAxes": { "execution": 0, "insight": 0.1, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.35000000000000003 + "uwrScore": 0.17500000000000002 }, "signalId": "oracle-tv-short-0001", "strategyId": "trend_pullback_v1", @@ -508,7 +508,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.35000000000000003, + "conviction": 0.17500000000000002, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -528,10 +528,10 @@ "uwrAxes": { "execution": 0, "insight": 0.1, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.35000000000000003 + "uwrScore": 0.17500000000000002 }, "decayParams": { "barsPerHalfLife": 12, @@ -672,7 +672,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "81a258ce32bd25660b808d6404254a154d8208726851ed08809464a523282e53" + "value": "53510590e062dcbc12eaefbd5f14bdf2e3aad952ca6eb08a4a335a1e2ac7e7c7" }, "scorerInput": { "atrRegime": "low", @@ -682,7 +682,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 0, "weeklyBias": "neutral" }, diff --git a/test/oracle/goldens/enriched/tv-short.registry.json b/test/oracle/goldens/enriched/tv-short.registry.json index fc13b381..492e8764 100644 --- a/test/oracle/goldens/enriched/tv-short.registry.json +++ b/test/oracle/goldens/enriched/tv-short.registry.json @@ -7,7 +7,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.35000000000000003, + "conviction": 0.17500000000000002, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -27,10 +27,10 @@ "uwrAxes": { "execution": 0, "insight": 0.1, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.35000000000000003 + "uwrScore": 0.17500000000000002 }, "canonicalUss": { "facts": { @@ -67,7 +67,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "enrichmentHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -117,7 +117,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "81a258ce32bd25660b808d6404254a154d8208726851ed08809464a523282e53" + "value": "53510590e062dcbc12eaefbd5f14bdf2e3aad952ca6eb08a4a335a1e2ac7e7c7" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -432,19 +432,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "137ceca886e422a7accc4109bee6de71766accca2fc93b591ab6ca0268924f74" + "value": "f961f8367bf5298131a3832e862d041ff0b2b027746e152b71f4778d769e9966" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "45e924410c95665d7d579b8a9357fe7f3569a6be7bdde6bbfbf22543ef7f5f3f" + "value": "c7a83402f2891c326b5bbfbabadd5acdac02a4f39f61cd5c7b44ad5ae9d7d1e9" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.35000000000000003, + "conviction": 0.17500000000000002, "direction": "short", "provenanceRecordRef": "provenance-record:oracle-tv-short-0001", "providerId": "oracle-provider-tv", @@ -456,10 +456,10 @@ "uwrAxes": { "execution": 0, "insight": 0.1, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.35000000000000003 + "uwrScore": 0.17500000000000002 }, "signalId": "oracle-tv-short-0001", "strategyId": "trend_pullback_v1", @@ -508,7 +508,7 @@ "insight": "Liquidity or volatility context is weak." }, "baseAsset": "BTC", - "conviction": 0.35000000000000003, + "conviction": 0.17500000000000002, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", @@ -528,10 +528,10 @@ "uwrAxes": { "execution": 0, "insight": 0.1, - "risk": 0.9, + "risk": 0.2, "structure": 0.4 }, - "uwrScore": 0.35000000000000003 + "uwrScore": 0.17500000000000002 }, "decayParams": { "barsPerHalfLife": 12, @@ -672,7 +672,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "81a258ce32bd25660b808d6404254a154d8208726851ed08809464a523282e53" + "value": "53510590e062dcbc12eaefbd5f14bdf2e3aad952ca6eb08a4a335a1e2ac7e7c7" }, "scorerInput": { "atrRegime": "low", @@ -682,7 +682,7 @@ "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, - "rrMultiplePlanned": 2, + "rrMultiplePlanned": 1, "triggerPatternQuality": 0, "weeklyBias": "neutral" }, diff --git a/test/oracle/oracleErrorTable.test.ts b/test/oracle/oracleErrorTable.test.ts index bd5fbe0b..9cdab767 100644 --- a/test/oracle/oracleErrorTable.test.ts +++ b/test/oracle/oracleErrorTable.test.ts @@ -170,6 +170,35 @@ describe("oracle error table — request validation (400/401/422)", () => { expect(res.body.error).toBe("symbol_normalization_failed"); expect(res.body.symbolRaw).toBe("??!!"); }); + + // DEM-PRODUCER-PLAN (DEM-GOV D-DEM-5(6)): the submitted plan is carried + // (afi.trade-plan.v1) or refused — never dropped — and verified against the + // fetched candles by the technical lane or refused. Both refusals are + // honest 422s with NO persisted record. + it("cpj trade plan not conforming to afi.trade-plan.v1 (stop without an entry) → 422 trade_plan_invalid", async () => { + const payload = loadFixture("cpj/cpj-blofin-perp-long.json"); + delete payload.extracted.entry; + const res = await request(app).post(CPJ).send(payload); + expect(res.status).toBe(422); + expect(res.body.error).toBe("trade_plan_invalid"); + expect(res.body.persisted).toBe(false); + }); + + it("cpj trade plan unverifiable against the fetched candles (off-market levels) → 422 trade_plan_unverifiable, no record", async () => { + const store = new OracleEvidenceStore(); + setEvidenceStore(store); + const payload = loadFixture("cpj/cpj-blofin-perp-long.json"); + // Thousands of dollars outside the demo window's observed band. + payload.extracted.entry = 42500; + payload.extracted.stopLoss = 41800; + payload.extracted.takeProfits = [{ price: 43500 }]; + const res = await request(app).post(CPJ).send(payload); + expect(res.status).toBe(422); + expect(res.body.error).toBe("trade_plan_unverifiable"); + expect(res.body.persisted).toBe(false); + expect(res.body.reason).toMatch(/entry 42500 lies outside/); + expect(store.submissions).toHaveLength(0); + }); }); describe("oracle error table — strategy resolution rejections (403; W3 spec section 4)", () => { diff --git a/test/oracle/oracleGoldensEnriched.test.ts b/test/oracle/oracleGoldensEnriched.test.ts index 095e3ae1..9bc59290 100644 --- a/test/oracle/oracleGoldensEnriched.test.ts +++ b/test/oracle/oracleGoldensEnriched.test.ts @@ -74,8 +74,11 @@ import { UWR_PROFILE_SOURCE_ENV, } from "../../src/config/uwrRuntimeProfile.js"; import { scorerFroggyTrendPullbackNode } from "../../src/pipeline/nodes/scorerFroggyTrendPullback.js"; +// DEM-PRODUCER-PLAN: the golden's scorerInput is the input the LIVE scorer +// node actually handed the rubric (captured at the rubric call), not an +// adapter-side recomputation — the retired adapter no longer yields one. // @ts-ignore — afi-core subpath types resolve via package exports; jest maps to source -import { buildFroggyTrendPullbackInputFromEnriched } from "afi-core/analysts/froggy.enrichment_adapter.js"; +import * as froggyRubric from "afi-core/analysts/froggy.trend_pullback_v1.js"; import { OracleEvidenceStore, expectGolden, @@ -92,6 +95,7 @@ let restoreNet: () => void; // path now scores through the registered scorer category node. The node's // input IS the (aiMl-augmented) enriched view — same capture semantics. const analystSpy = jest.spyOn(scorerFroggyTrendPullbackNode, "run"); +const rubricSpy = jest.spyOn(froggyRubric, "scoreFroggyTrendPullback"); beforeAll(() => { restoreEnv = installOracleEnv(); @@ -101,6 +105,7 @@ beforeAll(() => { afterAll(() => { analystSpy.mockRestore(); + rubricSpy.mockRestore(); resetEvidenceStore(); shutdownDedupeCache(); restoreNet(); @@ -139,6 +144,7 @@ describe.each(UWR_MODES)( const store = new OracleEvidenceStore(); setEvidenceStore(store); analystSpy.mockClear(); + rubricSpy.mockClear(); const res = await request(app).post(endpoint).send(loadFixture(file)); expect(res.status).toBe(200); @@ -161,7 +167,8 @@ describe.each(UWR_MODES)( expect(enrichedView.news).toBeDefined(); expect(enrichedView.aiMl).toBeDefined(); - const scorerInput = buildFroggyTrendPullbackInputFromEnriched(enrichedView as never); + expect(rubricSpy).toHaveBeenCalledTimes(1); + const scorerInput = rubricSpy.mock.calls[0][0]; expect(store.submissions).toHaveLength(1); const record = store.submissions[0]; expect(record.uwrProfile.source).toBe(stampSource); diff --git a/test/oracle/oracleInvariance.test.ts b/test/oracle/oracleInvariance.test.ts index 0aa6b3ba..5e5463ad 100644 --- a/test/oracle/oracleInvariance.test.ts +++ b/test/oracle/oracleInvariance.test.ts @@ -60,7 +60,9 @@ import { setEvidenceStore, resetEvidenceStore } from "../../src/evidence/index.j import { shutdownDedupeCache } from "../../src/services/ingestDedupeService.js"; import { scorerFroggyTrendPullbackNode } from "../../src/pipeline/nodes/scorerFroggyTrendPullback.js"; // @ts-ignore — afi-core subpath types resolve via package exports; jest maps to source -import { buildFroggyTrendPullbackInputFromEnriched } from "afi-core/analysts/froggy.enrichment_adapter.js"; +// DEM-PRODUCER-PLAN: the scorer input is captured at the rubric call (what +// the live node actually scored), not recomputed adapter-side. +import * as froggyRubric from "afi-core/analysts/froggy.trend_pullback_v1.js"; import { OracleEvidenceStore, installOracleEnv, @@ -76,6 +78,7 @@ let restoreNet: () => void; // path now scores through the registered scorer category node. The node's // input IS the (aiMl-augmented) enriched view — same capture semantics. const analystSpy = jest.spyOn(scorerFroggyTrendPullbackNode, "run"); +const rubricSpy = jest.spyOn(froggyRubric, "scoreFroggyTrendPullback"); beforeAll(() => { restoreEnv = installOracleEnv(); @@ -85,6 +88,7 @@ beforeAll(() => { afterAll(() => { analystSpy.mockRestore(); + rubricSpy.mockRestore(); resetEvidenceStore(); shutdownDedupeCache(); restoreNet(); @@ -113,19 +117,19 @@ describe.each([ const store = new OracleEvidenceStore(); setEvidenceStore(store); // fresh store: every run is a first write analystSpy.mockClear(); + rubricSpy.mockClear(); const res = await request(app).post(endpoint).send(payload); expect(res.status).toBe(200); expect(store.submissions).toHaveLength(1); const record = store.submissions[0]; - const enrichedView = analystSpy.mock.calls[0][0]; + expect(analystSpy).toHaveBeenCalledTimes(1); + expect(rubricSpy).toHaveBeenCalledTimes(1); captures.push({ inputHash: stableStringify(record.provenanceRecord.inputHash), outputHash: stableStringify(record.provenanceRecord.outputHash), - scorerInput: stableStringify( - buildFroggyTrendPullbackInputFromEnriched(enrichedView as never) - ), + scorerInput: stableStringify(rubricSpy.mock.calls[0][0]), record: stableStringify(normalizeVolatile(record)), // EV3-GOV D-EV3-4(7): identical canonical inputs → identical // record-level commitments run-over-run (the replay separation). diff --git a/test/oracle/support/goldenDiff.mjs b/test/oracle/support/goldenDiff.mjs new file mode 100644 index 00000000..72c2a048 --- /dev/null +++ b/test/oracle/support/goldenDiff.mjs @@ -0,0 +1,145 @@ +#!/usr/bin/env node +/** + * Golden differ — the recursive JSON-path audit every regeneration wave has + * described in test/oracle/INTENTIONAL_DIFFS.md, committed once so the + * per-golden itemization D-DEM-7(3) requires is reproducible instead of + * ad hoc. + * + * Usage: + * node test/oracle/support/goldenDiff.mjs [--ref ] [--dir test/oracle/goldens/enriched] [--json] + * + * Compares every golden file under --dir against the same path at --ref + * (default HEAD, i.e. the committed pre-regeneration bytes). Emits, per + * JSON path, the set of goldens where it changed and the old → new values + * (abbreviated), grouped into the itemization classes used by + * INTENTIONAL_DIFFS.md, plus the list of goldens that are byte-EQUAL. + * + * Pure read-only; never rewrites a golden. Test support only (never shipped). + */ +import { execFileSync } from "node:child_process"; +import { readdirSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +const args = process.argv.slice(2); +function opt(name, fallback) { + const i = args.indexOf(name); + return i >= 0 && i + 1 < args.length ? args[i + 1] : fallback; +} +const REF = opt("--ref", "HEAD"); +const DIR = opt("--dir", "test/oracle/goldens/enriched"); +const JSON_OUT = args.includes("--json"); + +function gitShow(ref, path) { + try { + return execFileSync("git", ["show", `${ref}:${path}`], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); + } catch { + return null; + } +} + +function walk(a, b, path, out) { + if (a === b) return; + const ta = a === null ? "null" : Array.isArray(a) ? "array" : typeof a; + const tb = b === null ? "null" : Array.isArray(b) ? "array" : typeof b; + if (ta === "object" && tb === "object") { + const keys = new Set([...Object.keys(a), ...Object.keys(b)]); + for (const k of [...keys].sort()) walk(a[k], b[k], `${path}/${k}`, out); + return; + } + if (ta === "array" && tb === "array" && a.length === b.length) { + for (let i = 0; i < a.length; i++) walk(a[i], b[i], `${path}/${i}`, out); + return; + } + if (JSON.stringify(a) === JSON.stringify(b)) return; + out.push({ path, before: a, after: b }); +} + +function abbreviate(v) { + const s = JSON.stringify(v); + if (s === undefined) return ""; + return s.length > 48 ? `${s.slice(0, 20)}…${s.slice(-8)}` : s; +} + +/** Itemization classes (INTENTIONAL_DIFFS.md vocabulary). Order matters: first match wins. */ +const CLASSES = [ + ["scored-value: axes/uwrScore/conviction/riskBucket", /\/(uwrAxes\/|uwrScore$|conviction$|riskBucket$)/], + ["scored-value: scorerInput", /^\/scorerInput\//], + ["scored-value: notes/rationale/axisNotes/direction", /\/(rationale|axisNotes|notes|direction)(\/|$)/], + ["lens payload (technical/other lane bytes)", /\/(lenses\/\d+\/payload|_priceFeedMetadata)\//], + ["canonical USS (input surface)", /^\/canonicalUss\/|\/rawUss\/|\/httpResponse\/uss\//], + ["inputHash", /inputHash\/value$/], + ["outputHash", /outputHash\/value$/], + ["composition: enrichmentHash", /enrichmentHash\/value$/], + ["composition: executionSummaryHash", /executionSummaryHash\/value$/], + ["composition: analystConfigHash", /analystConfigHash\/value$/], + ["composition: pluginSetHash", /pluginSetHash\/value$/], + ["composition: manifestHash", /manifestHash\/value$/], + ["providerInvocations: invocationInputHash", /invocationInputHash\/value$/], + ["providerInvocations: providerResultHash/categoryResultHash", /(providerResultHash|categoryResultHash)\/value$/], + ["recordHash", /\/recordHash\/value$/], + ["replayHash", /\/replayHash\/value$/], + ["decayParams", /decayParams\//], + ["other", /./], +]; + +function classify(path) { + for (const [name, re] of CLASSES) if (re.test(path)) return name; + return "other"; +} + +const files = readdirSync(DIR).filter((f) => f.endsWith(".json")).sort(); +const perFile = {}; +const equal = []; +const byPath = new Map(); // path -> { files: Set, samples: [] } +for (const f of files) { + const abs = join(DIR, f); + const rel = relative(process.cwd(), abs); + const now = readFileSync(abs, "utf8"); + const before = gitShow(REF, rel); + if (before === null) { + perFile[f] = { status: "new-file" }; + continue; + } + if (before === now) { + equal.push(f); + perFile[f] = { status: "byte-equal" }; + continue; + } + const diffs = []; + walk(JSON.parse(before), JSON.parse(now), "", diffs); + perFile[f] = { status: "changed", count: diffs.length, diffs }; + for (const d of diffs) { + if (!byPath.has(d.path)) byPath.set(d.path, { files: new Set(), samples: [] }); + const e = byPath.get(d.path); + e.files.add(f); + if (e.samples.length < 2) e.samples.push(`${f}: ${abbreviate(d.before)} → ${abbreviate(d.after)}`); + } +} + +const byClass = new Map(); +for (const [path, e] of byPath) { + const c = classify(path); + if (!byClass.has(c)) byClass.set(c, []); + byClass.get(c).push({ path, files: [...e.files].sort(), samples: e.samples }); +} + +if (JSON_OUT) { + const json = { ref: REF, dir: DIR, files: files.length, byteEqual: equal, perFile: Object.fromEntries(Object.entries(perFile).map(([k, v]) => [k, v.status === "changed" ? { status: v.status, count: v.count } : v])), classes: Object.fromEntries([...byClass].map(([c, rows]) => [c, rows])) }; + process.stdout.write(JSON.stringify(json, null, 2) + "\n"); +} else { + console.log(`# goldenDiff — ${DIR} vs ${REF} (${files.length} files)`); + console.log(`byte-EQUAL (${equal.length}): ${equal.join(", ") || "none"}`); + for (const f of files) { + const s = perFile[f]; + if (s.status === "changed") console.log(`changed: ${f} (${s.count} paths)`); + else if (s.status === "new-file") console.log(`new: ${f}`); + } + console.log(""); + for (const [c, rows] of [...byClass].sort((a, b) => a[0].localeCompare(b[0]))) { + console.log(`## ${c} — ${rows.length} path(s)`); + for (const r of rows.sort((a, b) => a.path.localeCompare(b.path))) { + console.log(` ${r.path} [${r.files.length}/${files.length}: ${r.files.map((x) => x.replace(/\.json$/, "")).join(", ")}]`); + for (const s of r.samples) console.log(` ${s}`); + } + } +} diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index 625bbcb1..dfdb6dda 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,20 +2,25 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.0.0", + "implementationVersion": "2.1.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs.", + "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs. DEM-PRODUCER-PLAN producer contract: when the canonical signal carries an afi.trade-plan.v1 plan, the lane verifies every submitted level against the fetched candle window (all levels within one observed-range width of the window; stop and targets on opposite sides of entry) and emits technical.plan; an unverifiable plan REFUSES the determination (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): technical.plan is absent when the signal carries no plan; technical.plan.rrToFirstTarget is absent when the plan carries no stop or no target.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, - "capabilities": ["provider:instance-backed"], + "capabilities": [ + "provider:instance-backed" + ], "defaultTimeoutMs": 10000, "defaultRetryPolicy": { "maxRetries": 1, "retryDelayMs": 500, "backoff": "fixed" }, - "permittedFailurePolicies": ["abort", "degrade"], + "permittedFailurePolicies": [ + "abort", + "degrade" + ], "paramsSchema": { "type": "object", "additionalProperties": false, diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index a33a534b..529dd8dd 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,14 +2,16 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.1.0", + "implementationVersion": "1.2.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", "outputSchemaRef": "afi.enrichment.enriched-view.v1", "deterministic": true, "defaultTimeoutMs": 2000, - "permittedFailurePolicies": ["abort"], + "permittedFailurePolicies": [ + "abort" + ], "paramsSchema": { "type": "object", "additionalProperties": false, diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 3d324a1c..45cf960d 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.3.0", + "implementationVersion": "1.4.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index 06b5733d..da1a4ddc 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,12 +22,21 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.0.0" + "version": "1.1.0" }, "decayConfig": { "ratio": { "barsPerHalfLife": 12, "unknownTimeframeMinutes": 5 } + }, + "metadata": { + "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", + "supersedes": [ + { + "analystConfigHash": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", + "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" + } + ] } } diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index 8501afd4..d1b1351d 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json new file mode 100644 index 00000000..f1e9bca6 --- /dev/null +++ b/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json @@ -0,0 +1,99 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.1.0", + "description": "DEM-PRODUCER-PLAN: the 1.0.0 bindings plus rrMultiplePlanned bound to technical.plan.rrToFirstTarget, the technical lane's VERIFIED trade-plan fact (the submitted afi.trade-plan.v1 levels checked against the fetched candles, D-DEM-5(6)). Optional under D-DEM-5(4)(b): the producer emits no R:R for a submission carrying no complete plan; the declared default is the rubric floor (1), hash-committed when fired (DEM-GOV §9 D-5).", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + } + } +} diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index 625bbcb1..dfdb6dda 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,20 +2,25 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.0.0", + "implementationVersion": "2.1.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs.", + "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs. DEM-PRODUCER-PLAN producer contract: when the canonical signal carries an afi.trade-plan.v1 plan, the lane verifies every submitted level against the fetched candle window (all levels within one observed-range width of the window; stop and targets on opposite sides of entry) and emits technical.plan; an unverifiable plan REFUSES the determination (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): technical.plan is absent when the signal carries no plan; technical.plan.rrToFirstTarget is absent when the plan carries no stop or no target.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, - "capabilities": ["provider:instance-backed"], + "capabilities": [ + "provider:instance-backed" + ], "defaultTimeoutMs": 10000, "defaultRetryPolicy": { "maxRetries": 1, "retryDelayMs": 500, "backoff": "fixed" }, - "permittedFailurePolicies": ["abort", "degrade"], + "permittedFailurePolicies": [ + "abort", + "degrade" + ], "paramsSchema": { "type": "object", "additionalProperties": false, diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index a33a534b..529dd8dd 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,14 +2,16 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.1.0", + "implementationVersion": "1.2.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", "outputSchemaRef": "afi.enrichment.enriched-view.v1", "deterministic": true, "defaultTimeoutMs": 2000, - "permittedFailurePolicies": ["abort"], + "permittedFailurePolicies": [ + "abort" + ], "paramsSchema": { "type": "object", "additionalProperties": false, diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 3d324a1c..45cf960d 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.3.0", + "implementationVersion": "1.4.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index 06b5733d..da1a4ddc 100644 --- a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,12 +22,21 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.0.0" + "version": "1.1.0" }, "decayConfig": { "ratio": { "barsPerHalfLife": 12, "unknownTimeframeMinutes": 5 } + }, + "metadata": { + "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", + "supersedes": [ + { + "analystConfigHash": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", + "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" + } + ] } } diff --git a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index 8501afd4..d1b1351d 100644 --- a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json b/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json new file mode 100644 index 00000000..f1e9bca6 --- /dev/null +++ b/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.1.0.json @@ -0,0 +1,99 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.1.0", + "description": "DEM-PRODUCER-PLAN: the 1.0.0 bindings plus rrMultiplePlanned bound to technical.plan.rrToFirstTarget, the technical lane's VERIFIED trade-plan fact (the submitted afi.trade-plan.v1 levels checked against the fetched candles, D-DEM-5(6)). Optional under D-DEM-5(4)(b): the producer emits no R:R for a submission carrying no complete plan; the declared default is the rubric floor (1), hash-committed when fired (DEM-GOV §9 D-5).", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + } + } +} diff --git a/test/pipeline/officialArtifactPins.test.ts b/test/pipeline/officialArtifactPins.test.ts index 9fa35606..d09dd0de 100644 --- a/test/pipeline/officialArtifactPins.test.ts +++ b/test/pipeline/officialArtifactPins.test.ts @@ -31,8 +31,8 @@ import { FIXTURE_CONFIG_ROOT } from "./support/testHarness.js"; const PINS = { manifestHash: "df3372dadaca1595d0e6d2f6bad9464ccc9abb7106e9f5b7111df148a145bc4f", - analystConfigHash: "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", - pluginSetHash: "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6", + analystConfigHash: "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", + pluginSetHash: "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93", }; function readJson(path: string): unknown { diff --git a/test/pipeline/registryLoader.test.ts b/test/pipeline/registryLoader.test.ts index 4e26f79f..9d848434 100644 --- a/test/pipeline/registryLoader.test.ts +++ b/test/pipeline/registryLoader.test.ts @@ -90,10 +90,10 @@ describe("validateRuntimeConfig — positive resolution over the fixture registr "df3372dadaca1595d0e6d2f6bad9464ccc9abb7106e9f5b7111df148a145bc4f" ); expect(froggy.analystConfigHash.value).toBe( - "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479" + "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" ); expect(froggy.pluginSetHash.value).toBe( - "220c004ed615c58bd3f4187256568bfcc6ea375e49ce467ff06a2467b405c0b6" + "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" ); expect(froggy.decay).toEqual({ kind: "ratio", diff --git a/test/pipeline/support/froggyMapping.ts b/test/pipeline/support/froggyMapping.ts new file mode 100644 index 00000000..7e9f00c0 --- /dev/null +++ b/test/pipeline/support/froggyMapping.ts @@ -0,0 +1,54 @@ +/** + * Test support (DEM-PRODUCER-PLAN): the registered froggy mapping, resolved + * the way production resolves it — by the fixture analyst config's + * `mappingRef` from the fixture registry tree — instead of the afi-config + * canonical EXAMPLE (which stays the immutable 1.0.0 vector). One truth with + * the pinned analystConfigHash / registry drift guards. + * + * Also the COMPOSITION REFERENCE the seam tests compare the scorer node + * against: registered mapping fragment + residual → composer → rubric — the + * exact production assembly, run out-of-band. (The retired adapter no longer + * yields a full scorer input, so it cannot serve as the reference; the + * reference's independence now rests on the interpreter/composer KATs in + * afi-core.) + */ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import type { ResolvedMappingCarrier } from "../../../src/pipeline/nodeSdk.js"; +import type { UniversalWeightingRuleConfig } from "../../../node_modules/afi-core/validators/UniversalWeightingRule.js"; +import type { FroggyEnrichedView } from "../../../node_modules/afi-core/analysts/froggy.enrichment_adapter.js"; +import { scoreFroggyTrendPullback } from "../../../node_modules/afi-core/analysts/froggy.trend_pullback_v1.js"; +import { + buildFroggyResidualInput, + composeFroggyTrendPullbackInput, +} from "../../../node_modules/afi-core/analysts/froggy.residual_builder.js"; +import { interpretEnrichmentMapping } from "../../../node_modules/afi-core/validators/EnrichmentMappingInterpreter.js"; + +export const FIXTURE_CONFIG_ROOT = path.resolve(process.cwd(), "test/pipeline/fixtures/afi-config"); +const FROGGY_CONFIG_REL = "registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json"; + +/** The mapping the fixture froggy registration names (mappingRef), from the fixture registry. */ +export function froggyMappingCarrier(configRoot: string = FIXTURE_CONFIG_ROOT): ResolvedMappingCarrier { + const config = JSON.parse(readFileSync(path.join(configRoot, FROGGY_CONFIG_REL), "utf-8")) as { + mappingRef: { mappingId: string; version: string }; + }; + const { mappingId, version } = config.mappingRef; + const doc = JSON.parse( + readFileSync( + path.join(configRoot, "registries/enrichment-mappings", `${mappingId}--${version}.json`), + "utf-8" + ) + ) as Record; + return { mappingId, version, doc }; +} + +/** The production assembly run out-of-band (mapping fragment + residual → rubric). */ +export function compositionReference( + enriched: FroggyEnrichedView, + uwrConfig: UniversalWeightingRuleConfig, + carrier: ResolvedMappingCarrier = froggyMappingCarrier() +) { + const { fragment, firedDefaults } = interpretEnrichmentMapping(carrier.doc, enriched); + const input = composeFroggyTrendPullbackInput(fragment, buildFroggyResidualInput(enriched)); + return { input, firedDefaults, analysis: scoreFroggyTrendPullback(input, uwrConfig, enriched) }; +} diff --git a/test/pipeline/tradePlanCarrier.test.ts b/test/pipeline/tradePlanCarrier.test.ts new file mode 100644 index 00000000..4ed4c86c --- /dev/null +++ b/test/pipeline/tradePlanCarrier.test.ts @@ -0,0 +1,85 @@ +/** + * DEM-PRODUCER-PLAN — the ingest carrier: the submitted trade plan survives + * the CPJ→USS mapping as `uss.plan` (afi.trade-plan.v1, decimal strings) or is + * refused at ingest; it is never dropped. The TradingView/MarkitTick mappers + * are untouched (they carry no plan fields). + */ +import { describe, it, expect } from "@jest/globals"; +import { buildTradePlanFromCpj, mapCpjToUssV11 } from "../../src/uss/cpjMapper.js"; +import { validateUsignalV11 } from "../../src/uss/ussValidator.js"; +import { validateTradePlanV1 } from "../../src/evidence/provenance/schemaValidation.js"; +import type { CpjV01Payload } from "../../src/cpj/cpjValidator.js"; + +const STRATEGY = { strategyId: "trend_pullback_v1" }; + +function cpj(extracted: Partial): CpjV01Payload { + return { + schema: "afi.cpj.v0.1", + provenance: { + providerType: "telegram", + providerId: "carrier-test-channel", + messageId: "carrier-msg-1", + postedAt: "2026-01-15T10:00:00Z", + }, + extracted: { symbolRaw: "BTCUSDT", side: "long", timeframeHint: "4h", venueHint: "blofin", marketTypeHint: "perp", ...extracted }, + parse: { parserId: "carrier-test", parserVersion: "1.0.0", confidence: 0.9 }, + } as CpjV01Payload; +} + +describe("cpjMapper — uss.plan carrier", () => { + it("carries a full plan as afi.trade-plan.v1 with decimal-string prices, contract-valid, USS-valid", () => { + const r = mapCpjToUssV11(cpj({ entry: 50000, stopLoss: 49300, takeProfits: [{ price: 51000, percentage: 50 }, { price: 52000.5 }], leverageHint: 3 }), STRATEGY); + expect(r.success).toBe(true); + const plan = r.uss!.plan!; + expect(plan).toEqual({ + schema: "afi.trade-plan.v1", + signalId: r.uss!.provenance.signalId, + levels: { + entry: "50000", + stopLoss: "49300", + takeProfits: [{ price: "51000", sizePct: "50" }, { price: "52000.5" }], + }, + leverageHint: "3", + venueHint: "blofin", + marketTypeHint: "perp", + }); + expect(validateTradePlanV1(plan).ok).toBe(true); + expect(validateUsignalV11(r.uss).ok).toBe(true); + // facts stays exactly the closed five-key block (USS schema unchanged). + expect(Object.keys(r.uss!.facts!).sort()).toEqual(["direction", "market", "strategy", "symbol", "timeframe"]); + }); + + it("carries an entry range and a non-integer entry as plain decimal strings (never floats, never exponent notation)", () => { + const range = mapCpjToUssV11(cpj({ entry: { min: 42500, max: 42800 } }), STRATEGY).uss!.plan!; + expect(range.levels.entry).toEqual({ min: "42500", max: "42800" }); + const decimal = mapCpjToUssV11(cpj({ symbolRaw: "ETH-USD", entry: 3001.5 }), STRATEGY).uss!.plan!; + expect(decimal.levels.entry).toBe("3001.5"); + const tiny = buildTradePlanFromCpj(cpj({ entry: 0.0000015 }), "sig")!; + expect(tiny.levels.entry).toBe("0.0000015"); + }); + + it("emits NO plan block when the submission carries no plan field (declared absence)", () => { + const r = mapCpjToUssV11(cpj({}), STRATEGY); + expect(r.success).toBe(true); + expect("plan" in r.uss!).toBe(false); + }); + + it("refuses at ingest: a stop or target without an entry", () => { + const r = mapCpjToUssV11(cpj({ stopLoss: 49300 }), STRATEGY); + expect(r.success).toBe(false); + expect(r.error).toMatchObject({ type: "trade_plan_invalid", reason: expect.stringMatching(/without an entry/) }); + }); + + it("refuses at ingest: a negative or non-finite price (contract pattern cannot express it)", () => { + expect(mapCpjToUssV11(cpj({ entry: -5 }), STRATEGY).error).toMatchObject({ type: "trade_plan_invalid", reason: expect.stringMatching(/negative/) }); + expect(mapCpjToUssV11(cpj({ entry: Number.NaN }), STRATEGY).error).toMatchObject({ type: "trade_plan_invalid" }); + }); + + it("the plan enters the canonical USS (and therefore inputHash) — not ingestHash, which already committed the CPJ levels", () => { + const withPlan = mapCpjToUssV11(cpj({ entry: 50000, stopLoss: 49300, takeProfits: [{ price: 51000 }] }), STRATEGY).uss!; + const withoutPlan = mapCpjToUssV11(cpj({}), STRATEGY).uss!; + expect(withPlan.plan).toBeDefined(); + expect(withoutPlan.plan).toBeUndefined(); + expect(withPlan.provenance.ingestHash).not.toBe(withoutPlan.provenance.ingestHash); + }); +}); diff --git a/test/pipeline/tradePlanProducer.test.ts b/test/pipeline/tradePlanProducer.test.ts new file mode 100644 index 00000000..954c6dbb --- /dev/null +++ b/test/pipeline/tradePlanProducer.test.ts @@ -0,0 +1,127 @@ +/** + * DEM-PRODUCER-PLAN — the technical lane as the plan producer, through the + * real adapter with injected deterministic kernels: + * - the verified plan facts ride `technical.plan` (a provider fact); + * - ROLE SPLIT (analyst/provider): the facts are a function of the submitted + * plan and the fetched candles ONLY — analyst config and mapping cannot + * move them; + * - DIR-GOV D-DIR-3: the submitted side (facts.direction) cannot move them; + * - no plan → no block (declared absence); an unverifiable plan REFUSES the + * lane with a TradePlanVerificationError (pipeline abort, no record); + * - the demo envelope actually contains every committed CPJ fixture plan + * (so the oracle harness cannot drift into refusal silently). + */ +import { describe, it, expect } from "@jest/globals"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { createTechnicalLocalAdapter } from "../../src/providers/adapters/technicalLocalAdapter.js"; +import { computeTechnicalEnrichment } from "../../src/enrichment/technicalIndicators.js"; +import { TradePlanVerificationError, verifyTradePlan } from "../../src/enrichment/tradePlanVerification.js"; +import { SILENT_NODE_LOGGER } from "../../src/pipeline/nodeSdk.js"; +import type { ProviderAdapterContext } from "../../src/providers/types.js"; +import type { PriceFeedAdapter } from "../../src/adapters/exchanges/types.js"; +import { demoPriceFeedAdapter } from "../support/deterministicPriceFeedAdapter.js"; +import { mapCpjToUssV11 } from "../../src/uss/cpjMapper.js"; +import type { CpjV01Payload } from "../../src/cpj/cpjValidator.js"; +import type { AfiCandle } from "../../src/types/AfiCandle.js"; + +function adapter() { + return createTechnicalLocalAdapter({ + resolvePriceSource: () => "demo", + getAdapter: () => demoPriceFeedAdapter as unknown as PriceFeedAdapter, + computeTechnical: computeTechnicalEnrichment, + }); +} + +function ctx(signal: Record, config: Record = { candleLimit: 100 }): ProviderAdapterContext { + return { + signal: signal as ProviderAdapterContext["signal"], + input: undefined, + config, + logger: SILENT_NODE_LOGGER, + abort: new AbortController().signal, + credential: undefined, + } as unknown as ProviderAdapterContext; +} + +async function demoWindow(symbol: string, timeframe: string, limit = 100): Promise { + return (await demoPriceFeedAdapter.getOHLCV({ symbol, timeframe, limit })).map((c) => ({ + timestamp: c.timestamp, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, + })); +} + +function signal(direction: "long" | "short" | "neutral", plan?: Record) { + return { + schema: "afi.usignal.v1.1", + provenance: { source: "test", providerId: "plan-producer-test", signalId: "sig-plan-producer" }, + facts: { symbol: "BTC/USDT", market: "perp", timeframe: "4h", strategy: "trend_pullback_v1", direction }, + ...(plan ? { plan } : {}), + }; +} + +const PLAN = { + schema: "afi.trade-plan.v1", + signalId: "sig-plan-producer", + levels: { entry: "50000", stopLoss: "49300", takeProfits: [{ price: "51000" }] }, +}; + +describe("technical lane — the plan producer", () => { + it("emits technical.plan (verified facts) when the signal carries a plan; no block when it does not", async () => { + const withPlan = (await adapter().run(ctx(signal("long", PLAN)))) as unknown as { technical: { plan?: unknown } }; + expect(withPlan.technical.plan).toEqual({ + entryLow: 50000, + entryHigh: 50000, + entryPrice: 50000, + stopPrice: 49300, + firstTargetPrice: 51000, + targetCount: 1, + rrToFirstTarget: 1.4286, + envelopeLow: expect.any(Number), + envelopeHigh: expect.any(Number), + barCount: 100, + }); + const without = (await adapter().run(ctx(signal("long")))) as unknown as { technical: { plan?: unknown } }; + expect("plan" in without.technical).toBe(false); + }); + + it("DIR-GOV D-DIR-3: the submitted side cannot move the plan facts (long/short/neutral → byte-identical technical payload)", async () => { + const outputs = await Promise.all( + (["long", "short", "neutral"] as const).map(async (d) => JSON.stringify((await adapter().run(ctx(signal(d, PLAN)))) as object)) + ); + expect(outputs[1]).toBe(outputs[0]); + expect(outputs[2]).toBe(outputs[0]); + }); + + it("ROLE SPLIT: analyst-side inputs (node config beyond the fetch, mapping content) cannot move the provider fact", async () => { + const a = (await adapter().run(ctx(signal("long", PLAN), { candleLimit: 100 }))) as unknown as { technical: { plan: unknown } }; + const b = (await adapter().run(ctx(signal("long", PLAN), { candleLimit: 100, timeoutMs: 10000 }))) as unknown as { technical: { plan: unknown } }; + expect(b.technical.plan).toEqual(a.technical.plan); + // The facts equal the pure law applied to (plan, fetched candles) and nothing else. + const window = await demoWindow("BTC/USDT", "4h"); + expect(a.technical.plan).toEqual(verifyTradePlan(PLAN as never, window)); + }); + + it("an unverifiable plan REFUSES the lane (TradePlanVerificationError — no retry, no record)", async () => { + const offMarket = { ...PLAN, levels: { entry: "42500", stopLoss: "41800", takeProfits: [{ price: "43500" }] } }; + await expect(adapter().run(ctx(signal("long", offMarket)))).rejects.toBeInstanceOf(TradePlanVerificationError); + }); + + it("a plan on a window below the kernel floor refuses (no computable window to verify against)", async () => { + await expect(adapter().run(ctx(signal("long", PLAN), { candleLimit: 40 }))).rejects.toThrow(/kernel floor/); + }); +}); + +describe("oracle corpus discipline — the demo envelope contains every committed CPJ fixture plan", () => { + const FIXTURES = ["cpj-blofin-perp-long.json", "cpj-coinbase-spot-sell.json", "cpj-blofin-perp-neutral.json"]; + for (const file of FIXTURES) { + it(`${file}: the carried plan (if any) verifies against the fixture's demo window`, async () => { + const cpj = JSON.parse(readFileSync(path.resolve(process.cwd(), "test/oracle/fixtures/cpj", file), "utf-8")) as CpjV01Payload; + const mapped = mapCpjToUssV11(cpj, { strategyId: "trend_pullback_v1" }); + expect(mapped.success).toBe(true); + const uss = mapped.uss!; + if (!uss.plan) return; // no plan submitted — nothing to verify + const window = await demoWindow(uss.facts!.symbol!, uss.facts!.timeframe!); + expect(() => verifyTradePlan(uss.plan!, window)).not.toThrow(); + }); + } +}); diff --git a/test/pipeline/tradePlanVerification.test.ts b/test/pipeline/tradePlanVerification.test.ts new file mode 100644 index 00000000..e9d2894d --- /dev/null +++ b/test/pipeline/tradePlanVerification.test.ts @@ -0,0 +1,194 @@ +/** + * DEM-PRODUCER-PLAN — the trade-plan verification law (D-DEM-5(6)), pinned + * as known-answer vectors over hand-built candle windows. + * + * Every rule is exercised on both sides of its edge; every refusal is a + * TradePlanVerificationError (a NodeConfigurationError — pipeline abort, no + * retry, no record). Geometry is derived from submitted PRICES only: the + * producer never reads a submitted side (DIR-GOV D-DIR-3). + */ +import { describe, it, expect } from "@jest/globals"; +import { + TradePlanVerificationError, + roundRr, + verifyTradePlan, +} from "../../src/enrichment/tradePlanVerification.js"; +import { NodeConfigurationError } from "../../src/pipeline/nodeSdk.js"; +import type { AfiCandle } from "../../src/types/AfiCandle.js"; +import type { TradePlanV1 } from "../../src/types/TradePlan.js"; + +/** A window whose observed extremes are exactly [low, high]. */ +function window(low: number, high: number, bars = 60): AfiCandle[] { + const out: AfiCandle[] = []; + for (let i = 0; i < bars; i++) { + const mid = (low + high) / 2; + out.push({ timestamp: 1_700_000_000_000 + i * 60_000, open: mid, high: mid, low: mid, close: mid, volume: 1 }); + } + out[0] = { ...out[0], low, high: low }; + out[bars - 1] = { ...out[bars - 1], high, low: high }; + return out; +} + +function plan(levels: TradePlanV1["levels"]): TradePlanV1 { + return { schema: "afi.trade-plan.v1", signalId: "sig-plan-kat", levels }; +} + +// Window [90, 110]: W = 20, band = [70, 130]. +const W = window(90, 110); + +describe("verifyTradePlan — envelope band [L − W, H + W]", () => { + it("accepts an entry inside the observed window and stop/target inside the band", () => { + const facts = verifyTradePlan(plan({ entry: "100", stopLoss: "95", takeProfits: [{ price: "110" }] }), W); + expect(facts).toEqual({ + entryLow: 100, + entryHigh: 100, + entryPrice: 100, + stopPrice: 95, + firstTargetPrice: 110, + targetCount: 1, + rrToFirstTarget: 2, + envelopeLow: 90, + envelopeHigh: 110, + barCount: 60, + }); + }); + + it("accepts a breakout entry ABOVE the window and a limit entry BELOW it, inside the band", () => { + expect(verifyTradePlan(plan({ entry: "125", stopLoss: "115", takeProfits: [{ price: "130" }] }), W).rrToFirstTarget).toBe(0.5); + expect(verifyTradePlan(plan({ entry: "75", stopLoss: "70", takeProfits: [{ price: "90" }] }), W).rrToFirstTarget).toBe(3); + }); + + it("accepts levels exactly ON the band edges", () => { + const facts = verifyTradePlan(plan({ entry: "100", stopLoss: "70", takeProfits: [{ price: "130" }] }), W); + expect(facts.stopPrice).toBe(70); + expect(facts.firstTargetPrice).toBe(130); + expect(facts.rrToFirstTarget).toBe(1); + }); + + const BAND_REFUSALS: Array<[string, TradePlanV1["levels"], RegExp]> = [ + ["entry above the band", { entry: "130.01", stopLoss: "120", takeProfits: [{ price: "130" }] }, /entry 130.01 lies outside/], + ["entry below the band", { entry: "69.99", stopLoss: "60", takeProfits: [{ price: "90" }] }, /entry 69.99 lies outside/], + ["stop outside the band", { entry: "100", stopLoss: "69.99", takeProfits: [{ price: "110" }] }, /stopLoss 69.99 lies outside/], + ["target outside the band", { entry: "100", stopLoss: "95", takeProfits: [{ price: "130.5" }] }, /takeProfits\[0\].price 130.5 lies outside/], + ]; + it.each(BAND_REFUSALS)("refuses %s", (_label, levels, re) => { + expect(() => verifyTradePlan(plan(levels), W)).toThrow(re); + }); + + it("refuses a flat window (zero observed range) and an empty window", () => { + expect(() => verifyTradePlan(plan({ entry: "100" }), window(100, 100))).toThrow(/flat/); + expect(() => verifyTradePlan(plan({ entry: "100" }), [])).toThrow(/no fetched candles/); + }); +}); + +describe("verifyTradePlan — plan geometry (from submitted prices only)", () => { + it("long geometry: stop below entry, targets above; first target = nearest", () => { + const facts = verifyTradePlan( + plan({ entry: "100", stopLoss: "96", takeProfits: [{ price: "112" }, { price: "104" }, { price: "108" }] }), + W + ); + expect(facts.firstTargetPrice).toBe(104); + expect(facts.targetCount).toBe(3); + expect(facts.rrToFirstTarget).toBe(1); + }); + + it("short geometry: stop above entry, targets below (direction is never read)", () => { + const facts = verifyTradePlan(plan({ entry: "100", stopLoss: "104", takeProfits: [{ price: "94" }, { price: "90" }] }), W); + expect(facts.firstTargetPrice).toBe(94); + expect(facts.rrToFirstTarget).toBe(1.5); + }); + + const GEOMETRY_REFUSALS: Array<[string, TradePlanV1["levels"], RegExp]> = [ + ["targets on both sides of the stop", { entry: "100", stopLoss: "100.5", takeProfits: [{ price: "95" }, { price: "110" }] }, /both sides of the stop/], + ["a target equal to the stop", { entry: "100", stopLoss: "95", takeProfits: [{ price: "95" }] }, /equals the stop/], + ["stop not below the entry (long)", { entry: "100", stopLoss: "100", takeProfits: [{ price: "110" }] }, /stop 100 is not below the entry 100/], + ["target not above the entry (long)", { entry: "100", stopLoss: "95", takeProfits: [{ price: "110" }, { price: "100" }] }, /target 100 is not above the entry 100/], + ["stop not above the entry (short)", { entry: "100", stopLoss: "99", takeProfits: [{ price: "90" }] }, /is not above the entry 100|is not below the entry 100/], + ]; + it.each(GEOMETRY_REFUSALS)("refuses %s", (_label, levels, re) => { + expect(() => verifyTradePlan(plan(levels), W)).toThrow(re); + }); + + it("entry RANGE: the conservative bound (nearest the targets) is the entry price", () => { + const long = verifyTradePlan(plan({ entry: { min: "98", max: "102" }, stopLoss: "94", takeProfits: [{ price: "110" }] }), W); + expect(long.entryLow).toBe(98); + expect(long.entryHigh).toBe(102); + expect(long.entryPrice).toBe(102); // smallest reward (8), largest risk (8) → rr 1 + expect(long.rrToFirstTarget).toBe(1); + const short = verifyTradePlan(plan({ entry: { min: "98", max: "102" }, stopLoss: "106", takeProfits: [{ price: "90" }] }), W); + expect(short.entryPrice).toBe(98); + expect(short.rrToFirstTarget).toBe(1); + }); + + it("refuses an inverted entry range", () => { + expect(() => verifyTradePlan(plan({ entry: { min: "102", max: "98" } }), W)).toThrow(/inverted/); + }); +}); + +describe("verifyTradePlan — declared producer absences (D-DEM-5(4)(b))", () => { + it("entry-only plan: the entry is verified, no R:R fact is emitted", () => { + const facts = verifyTradePlan(plan({ entry: "100" }), W); + expect(facts).toEqual({ entryLow: 100, entryHigh: 100, entryPrice: 100, targetCount: 0, envelopeLow: 90, envelopeHigh: 110, barCount: 60 }); + expect(facts.rrToFirstTarget).toBeUndefined(); + }); + + it("entry + stop without a target: both verified, no R:R fact", () => { + const facts = verifyTradePlan(plan({ entry: "100", stopLoss: "95" }), W); + expect(facts.stopPrice).toBe(95); + expect(facts.rrToFirstTarget).toBeUndefined(); + expect(facts.firstTargetPrice).toBeUndefined(); + }); + + it("entry range without geometry: bounds verified, no entryPrice, no R:R", () => { + const facts = verifyTradePlan(plan({ entry: { min: "98", max: "102" } }), W); + expect(facts.entryPrice).toBeUndefined(); + expect(facts.rrToFirstTarget).toBeUndefined(); + }); + + it("an entry-only plan outside the band is still refused (an emitted price is always a verified price)", () => { + expect(() => verifyTradePlan(plan({ entry: "50" }), W)).toThrow(TradePlanVerificationError); + }); +}); + +describe("verifyTradePlan — contract discipline and error class", () => { + it("refuses non-decimal-string and non-positive prices", () => { + expect(() => verifyTradePlan(plan({ entry: 100 as unknown as string }), W)).toThrow(/neither a price nor a range/); + expect(() => verifyTradePlan(plan({ entry: "100", stopLoss: 95 as unknown as string }), W)).toThrow(/stopLoss is not a decimal-string/); + expect(() => verifyTradePlan(plan({ entry: "-5" }), W)).toThrow(/not a decimal-string/); + expect(() => verifyTradePlan(plan({ entry: "0" }), W)).toThrow(/not a positive finite/); + expect(() => verifyTradePlan(plan({ entry: "1e3" }), W)).toThrow(/not a decimal-string/); + }); + + it("refuses a non-plan object", () => { + expect(() => verifyTradePlan({ schema: "other" } as unknown as TradePlanV1, W)).toThrow(/not an afi.trade-plan.v1/); + }); + + it("every refusal is a TradePlanVerificationError, i.e. a NodeConfigurationError (no retry, pipeline abort)", () => { + try { + verifyTradePlan(plan({ entry: "50" }), W); + throw new Error("expected refusal"); + } catch (err) { + expect(err).toBeInstanceOf(TradePlanVerificationError); + expect(err).toBeInstanceOf(NodeConfigurationError); + expect((err as TradePlanVerificationError).code).toBe("trade_plan_unverifiable"); + expect((err as TradePlanVerificationError).reason).toMatch(/entry 50 lies outside/); + } + }); + + it("R:R rounds half-up to 4 decimals", () => { + expect(roundRr(1000 / 700)).toBe(1.4286); + expect(roundRr(1.23445)).toBe(1.2345); + expect(roundRr(2)).toBe(2); + const facts = verifyTradePlan(plan({ entry: "50000", stopLoss: "49300", takeProfits: [{ price: "51000" }] }), window(49000, 51000)); + expect(facts.rrToFirstTarget).toBe(1.4286); + }); + + it("is pure and side-effect free: identical inputs → identical facts; inputs untouched", () => { + const p = plan({ entry: "100", stopLoss: "95", takeProfits: [{ price: "110" }] }); + const before = JSON.stringify([p, W]); + const a = verifyTradePlan(p, W); + const b = verifyTradePlan(p, W); + expect(a).toEqual(b); + expect(JSON.stringify([p, W])).toBe(before); + }); +}); From a2602606e32e86b11e117f86342b4764657a5442 Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 25 Aug 2026 19:31:00 -0700 Subject: [PATCH 2/5] DEM-PRODUCER-PLAN (reactor): pre-open review fixes to the verification law An adversarial review of the branch (7 lenses, findings independently verified) found five real defects in the D-DEM-5(6) law. All five fixed here; no golden byte moves (every fix is refusal-side or message-side). - MAJOR: an entry RANGE was verified only at its conservative bound, so a range containing (or on the wrong side of) its own stop was ACCEPTED and an R:R emitted from one end. The stop must now clear the FAR bound as well: long -> stop < entryLow, short -> stop > entryHigh. Three refusal KATs (long straddle, short straddle, far bound EQUALS the stop) + the accept-side edge. - MINOR: the inclusive band edge was false on non-integer windows (binary float: 1.1 - 0.2 = 0.9000000000000001 refused an entry of exactly "0.9"). Relative tolerance (1e-12) at both edges + bounded-precision numbers in the refusal message; a non-integer-window KAT pins both edges. - MINOR: a non-array takeProfits (or a non-object levels) threw a bare TypeError, escaping the 422 unwrap. Both refuse properly now, with KATs. - MINOR: "half-up to 4 decimals" was not what Math.round(v*1e4)/1e4 does at binary ties. The law is documented as what it is (deterministic binary quantisation) and the unfavourable tie (0.00015 -> 0.0001) is pinned. - MINOR: missing edge KATs added: empty takeProfits array (no-target case), degenerate entry range (min === max), exotic numeric strings, an overflow-to-Infinity decimal string, malformed candles; the hedged short-geometry regex is tightened to its exact message. 756 -> 764 tests, all green; typecheck green; goldens byte-unchanged. Co-Authored-By: Claude Fable 5 --- src/enrichment/tradePlanVerification.ts | 69 ++++++++++++++++----- test/pipeline/tradePlanVerification.test.ts | 64 +++++++++++++++++-- 2 files changed, 115 insertions(+), 18 deletions(-) diff --git a/src/enrichment/tradePlanVerification.ts b/src/enrichment/tradePlanVerification.ts index dfe4b029..3b232c15 100644 --- a/src/enrichment/tradePlanVerification.ts +++ b/src/enrichment/tradePlanVerification.ts @@ -18,12 +18,12 @@ * entries below it are lawful; a level a full range-width away * from anything the market printed is not verifiable; * geometry (only when a stop AND at least one target are submitted) every - * target lies on the same side of the stop; the entry lies - * strictly between the stop and every target. For an entry - * RANGE the conservative bound is used (nearest the targets — - * smallest reward, largest risk); + * target lies on the same side of the stop, and the ENTIRE entry + * — both bounds of a range — lies strictly between the stop and + * every target. R:R is computed at the conservative bound + * (nearest the targets: smallest reward, largest risk); * R:R |firstTarget − entry| / |entry − stop| where firstTarget is the - * target nearest the entry; rounded half-up to 4 decimals. + * target nearest the entry, quantised to 4 decimals (see roundRr). * Any violation REFUSES the determination (TradePlanVerificationError — a * NodeConfigurationError: no retry, pipeline abort, no score, no record). * @@ -63,13 +63,26 @@ function parsePrice(label: string, raw: unknown): number { return value; } -/** Round half-up to 4 decimals (positive inputs only). */ +/** + * The R:R quantisation: `Math.round(value · 1e4) / 1e4` — IEEE-754 binary + * arithmetic, DETERMINISTIC for every input (what a sealed record needs), not + * exact decimal half-up: a decimal tie such as 0.00015 is not representable in + * binary and rounds by its stored value (0.00015 → 0.0001). R:R is a ratio of + * float prices, so an exact decimal tie is not reachable in practice. + */ export function roundRr(value: number): number { return Math.round(value * 1e4) / 1e4; } export function verifyTradePlan(plan: TradePlanV1, candles: readonly AfiCandle[]): TechnicalPlanFacts { - if (!plan || typeof plan !== "object" || plan.schema !== "afi.trade-plan.v1" || !plan.levels) { + if ( + !plan || + typeof plan !== "object" || + plan.schema !== "afi.trade-plan.v1" || + !plan.levels || + typeof plan.levels !== "object" || + Array.isArray(plan.levels) + ) { refuse("plan is not an afi.trade-plan.v1 object"); } if (candles.length === 0) refuse("no fetched candles to verify against"); @@ -87,11 +100,19 @@ export function verifyTradePlan(plan: TradePlanV1, candles: readonly AfiCandle[] if (!(width > 0)) refuse("fetched window is flat (zero observed range)"); const bandLow = low - width; const bandHigh = high + width; + // The band is INCLUSIVE at both edges. `low - width` is computed in binary + // floating point, so a level submitted as the exact decimal edge can land a + // few ulps outside it; compare with a relative tolerance so the inclusive + // law holds for non-integer windows too (fail-closed direction unchanged: + // this only prevents a spurious refusal at the edge itself). + const EDGE_ULPS = 1e-12; + const tolerance = (bound: number): number => Math.abs(bound) * EDGE_ULPS; + const show = (value: number): string => Number(value.toPrecision(12)).toString(); const inBand = (label: string, price: number): number => { - if (price < bandLow || price > bandHigh) { + if (price < bandLow - tolerance(bandLow) || price > bandHigh + tolerance(bandHigh)) { refuse( - `${label} ${price} lies outside the observed band [${bandLow}, ${bandHigh}] ` + - `(window low ${low}, high ${high}, ${candles.length} candles)` + `${label} ${show(price)} lies outside the observed band [${show(bandLow)}, ${show(bandHigh)}] ` + + `(window low ${show(low)}, high ${show(high)}, ${candles.length} candles)` ); } return price; @@ -116,8 +137,12 @@ export function verifyTradePlan(plan: TradePlanV1, candles: readonly AfiCandle[] plan.levels.stopLoss !== undefined ? inBand("stopLoss", parsePrice("stopLoss", plan.levels.stopLoss)) : undefined; + const rawTargets = plan.levels.takeProfits; + if (rawTargets !== undefined && !Array.isArray(rawTargets)) { + refuse(`takeProfits is not an array (${typeof rawTargets})`); + } const targets: number[] = []; - for (const [i, tp] of (plan.levels.takeProfits ?? []).entries()) { + for (const [i, tp] of (rawTargets ?? []).entries()) { targets.push(inBand(`takeProfits[${i}].price`, parsePrice(`takeProfits[${i}].price`, tp?.price))); } @@ -140,13 +165,29 @@ export function verifyTradePlan(plan: TradePlanV1, candles: readonly AfiCandle[] if (above > 0 && below > 0) refuse("targets lie on both sides of the stop"); if (above === 0 && below === 0) refuse("a target equals the stop"); const longGeometry = above > 0; - // Conservative entry bound: nearest the targets. + // Conservative entry bound: nearest the targets (smallest reward, largest + // risk). The stop is checked against the FAR bound as well, so an entry + // RANGE that contains — or sits the wrong side of — its own stop refuses + // rather than being verified at one end (review finding, 2026-08-25). const entryPrice = longGeometry ? entryHigh : entryLow; + const entryFar = longGeometry ? entryLow : entryHigh; if (longGeometry) { - if (!(stopPrice < entryPrice)) refuse(`stop ${stopPrice} is not below the entry ${entryPrice}`); + if (!(stopPrice < entryFar)) { + refuse( + entryFar === entryPrice + ? `stop ${stopPrice} is not below the entry ${entryPrice}` + : `stop ${stopPrice} is not below the whole entry range [${entryLow}, ${entryHigh}]` + ); + } for (const t of targets) if (!(t > entryPrice)) refuse(`target ${t} is not above the entry ${entryPrice}`); } else { - if (!(stopPrice > entryPrice)) refuse(`stop ${stopPrice} is not above the entry ${entryPrice}`); + if (!(stopPrice > entryFar)) { + refuse( + entryFar === entryPrice + ? `stop ${stopPrice} is not above the entry ${entryPrice}` + : `stop ${stopPrice} is not above the whole entry range [${entryLow}, ${entryHigh}]` + ); + } for (const t of targets) if (!(t < entryPrice)) refuse(`target ${t} is not below the entry ${entryPrice}`); } let firstTarget = targets[0]; diff --git a/test/pipeline/tradePlanVerification.test.ts b/test/pipeline/tradePlanVerification.test.ts index e9d2894d..1ac3b01d 100644 --- a/test/pipeline/tradePlanVerification.test.ts +++ b/test/pipeline/tradePlanVerification.test.ts @@ -79,6 +79,21 @@ describe("verifyTradePlan — envelope band [L − W, H + W]", () => { expect(() => verifyTradePlan(plan({ entry: "100" }), window(100, 100))).toThrow(/flat/); expect(() => verifyTradePlan(plan({ entry: "100" }), [])).toThrow(/no fetched candles/); }); + + it("the inclusive edge holds on a NON-integer window too (binary-float tolerance; review finding)", () => { + const w = window(1.1, 1.3); // band [0.9, 1.5] in decimal; 1.1 − 0.2 is 0.9000000000000001 in binary + expect(verifyTradePlan(plan({ entry: "0.9" }), w).entryPrice).toBe(0.9); + expect(verifyTradePlan(plan({ entry: "1.5" }), w).entryPrice).toBe(1.5); + expect(() => verifyTradePlan(plan({ entry: "0.8999" }), w)).toThrow(/lies outside the observed band/); + expect(() => verifyTradePlan(plan({ entry: "1.5001" }), w)).toThrow(/lies outside the observed band/); + }); + + it("refuses a malformed candle window (NaN, non-positive low, inverted bar)", () => { + const bad = (c: Partial): AfiCandle[] => [{ timestamp: 0, open: 100, high: 101, low: 99, close: 100, volume: 1, ...c }, ...W.slice(1)]; + expect(() => verifyTradePlan(plan({ entry: "100" }), bad({ low: Number.NaN }))).toThrow(/malformed candle/); + expect(() => verifyTradePlan(plan({ entry: "100" }), bad({ low: 0 }))).toThrow(/malformed candle/); + expect(() => verifyTradePlan(plan({ entry: "100" }), bad({ high: 98, low: 99 }))).toThrow(/malformed candle/); + }); }); describe("verifyTradePlan — plan geometry (from submitted prices only)", () => { @@ -103,7 +118,12 @@ describe("verifyTradePlan — plan geometry (from submitted prices only)", () => ["a target equal to the stop", { entry: "100", stopLoss: "95", takeProfits: [{ price: "95" }] }, /equals the stop/], ["stop not below the entry (long)", { entry: "100", stopLoss: "100", takeProfits: [{ price: "110" }] }, /stop 100 is not below the entry 100/], ["target not above the entry (long)", { entry: "100", stopLoss: "95", takeProfits: [{ price: "110" }, { price: "100" }] }, /target 100 is not above the entry 100/], - ["stop not above the entry (short)", { entry: "100", stopLoss: "99", takeProfits: [{ price: "90" }] }, /is not above the entry 100|is not below the entry 100/], + ["stop not above the entry (short)", { entry: "100", stopLoss: "99", takeProfits: [{ price: "90" }] }, /stop 99 is not above the entry 100/], + // Review finding 2026-08-25: an entry RANGE must clear the stop at BOTH + // bounds — a range containing its own stop is not a verifiable plan. + ["long entry range straddling its stop", { entry: { min: "90", max: "102" }, stopLoss: "94", takeProfits: [{ price: "110" }] }, /stop 94 is not below the whole entry range \[90, 102\]/], + ["short entry range straddling its stop", { entry: { min: "98", max: "110" }, stopLoss: "106", takeProfits: [{ price: "90" }] }, /stop 106 is not above the whole entry range \[98, 110\]/], + ["long entry range whose far bound EQUALS the stop", { entry: { min: "94", max: "102" }, stopLoss: "94", takeProfits: [{ price: "110" }] }, /stop 94 is not below the whole entry range/], ]; it.each(GEOMETRY_REFUSALS)("refuses %s", (_label, levels, re) => { expect(() => verifyTradePlan(plan(levels), W)).toThrow(re); @@ -120,8 +140,19 @@ describe("verifyTradePlan — plan geometry (from submitted prices only)", () => expect(short.rrToFirstTarget).toBe(1); }); - it("refuses an inverted entry range", () => { + it("accepts an entry range whose FAR bound clears the stop (the edge just outside)", () => { + const facts = verifyTradePlan(plan({ entry: { min: "94.0001", max: "102" }, stopLoss: "94", takeProfits: [{ price: "110" }] }), W); + expect(facts.entryPrice).toBe(102); // conservative bound + expect(facts.rrToFirstTarget).toBe(1); + }); + + it("refuses an inverted entry range; a degenerate range (min === max) is a single price", () => { expect(() => verifyTradePlan(plan({ entry: { min: "102", max: "98" } }), W)).toThrow(/inverted/); + const facts = verifyTradePlan(plan({ entry: { min: "100", max: "100" }, stopLoss: "95", takeProfits: [{ price: "110" }] }), W); + expect(facts.entryLow).toBe(100); + expect(facts.entryHigh).toBe(100); + expect(facts.entryPrice).toBe(100); + expect(facts.rrToFirstTarget).toBe(2); }); }); @@ -145,6 +176,12 @@ describe("verifyTradePlan — declared producer absences (D-DEM-5(4)(b))", () => expect(facts.rrToFirstTarget).toBeUndefined(); }); + it("an EMPTY takeProfits array is the no-target case (no R:R claim), not a refusal", () => { + const facts = verifyTradePlan(plan({ entry: "100", stopLoss: "95", takeProfits: [] }), W); + expect(facts.targetCount).toBe(0); + expect(facts.rrToFirstTarget).toBeUndefined(); + }); + it("an entry-only plan outside the band is still refused (an emitted price is always a verified price)", () => { expect(() => verifyTradePlan(plan({ entry: "50" }), W)).toThrow(TradePlanVerificationError); }); @@ -159,8 +196,22 @@ describe("verifyTradePlan — contract discipline and error class", () => { expect(() => verifyTradePlan(plan({ entry: "1e3" }), W)).toThrow(/not a decimal-string/); }); - it("refuses a non-plan object", () => { + it("refuses exotic numeric strings the contract pattern does not admit", () => { + for (const bad of ["NaN", "Infinity", ".5", "1.", " 100", "1_000", "0x64", ""]) { + expect(() => verifyTradePlan(plan({ entry: bad }), W)).toThrow(/not a decimal-string/); + } + // A syntactically valid decimal string that overflows to Infinity. + expect(() => verifyTradePlan(plan({ entry: "9".repeat(400) }), W)).toThrow(/not a positive finite/); + }); + + it("refuses a non-plan object, a non-object levels block, and a non-array takeProfits (never a TypeError)", () => { expect(() => verifyTradePlan({ schema: "other" } as unknown as TradePlanV1, W)).toThrow(/not an afi.trade-plan.v1/); + expect(() => verifyTradePlan({ schema: "afi.trade-plan.v1", levels: [] } as unknown as TradePlanV1, W)).toThrow(/not an afi.trade-plan.v1/); + for (const shape of [{}, "x", 5, true]) { + expect(() => + verifyTradePlan(plan({ entry: "100", takeProfits: shape as never }), W) + ).toThrow(TradePlanVerificationError); + } }); it("every refusal is a TradePlanVerificationError, i.e. a NodeConfigurationError (no retry, pipeline abort)", () => { @@ -175,10 +226,15 @@ describe("verifyTradePlan — contract discipline and error class", () => { } }); - it("R:R rounds half-up to 4 decimals", () => { + it("R:R is quantised to 4 decimals by Math.round(v·1e4)/1e4 — deterministic binary rounding, not exact decimal half-up", () => { expect(roundRr(1000 / 700)).toBe(1.4286); expect(roundRr(1.23445)).toBe(1.2345); expect(roundRr(2)).toBe(2); + // A decimal "tie" is not representable in binary: 0.00015 * 1e4 stores as + // 1.4999999999999998, so it rounds DOWN. Pinned so the record's law is the + // implementation's law (review finding 2026-08-25). + expect(roundRr(0.00015)).toBe(0.0001); + expect(roundRr(1.00005)).toBe(1.0001); const facts = verifyTradePlan(plan({ entry: "50000", stopLoss: "49300", takeProfits: [{ price: "51000" }] }), window(49000, 51000)); expect(facts.rrToFirstTarget).toBe(1.4286); }); From 02daa1561572dd5c0dfcbd2828abaec64293f172 Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 25 Aug 2026 19:34:13 -0700 Subject: [PATCH 3/5] DEM-PRODUCER-CANDLE (reactor): the structure axis stops being a constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEM-GOV §9 DEM-PRODUCER-CANDLE (owner-authorized 2026-08-25; afi-governance #55; determinations D-2/D-3 in #56) — the act D5-GOV D-D5-1 reserved. Merge AFTER afi-core dem/candle-core and afi-config dem/candle-config, which merge after the whole PLAN wave. Source: - enrichment/candleStructure.ts (NEW): the two producers as pure functions over the window the lane already fetches. brokeEmaWithBody = the latest bar's body closed on the counter-trend side of EMA20 (bullish: close < ema20; bearish: close > ema20; range: the body crossed EMA20 this bar) — colour-free, one law per side. Heikin-Ashi flat-back = the recurrence over the whole window, seed (o+c)/2, epsilon 0 (bullish: an up HA bar with low >= haOpen; bearish: a down HA bar with high <= haOpen), confirmed iff its side agrees with the lane's own EMA trend law. Both read the lane's computed trendBias, never a submitted direction (DIR-GOV D-DIR-3). - enrichment/technicalIndicators.ts: emits the three facts with the bundle. - pipeline/nodes/laneView.ts: viewTechnical projects them; the imported stub constant and its pin are DELETED. - types/UssLenses.ts: lens payload widened. Registries/pins: three fixture trees mirror afi-config (mapping 1.2.0, config, three implementationVersion moves); the two pin tests rotated. Goldens (regenerated ONCE, itemized in INTENTIONAL_DIFFS.md): the SOL 15m seed (tv-neutral) computes brokeEmaWithBody TRUE, so structure 0.4 -> 0.15 and risk 0.2 -> 0.0 there; the other ten are unmoved in scored values. THE GATE IS MET: structure is non-constant over the fixture corpus ({0.4, 0.15}) and so is risk ({0.5, 0.2, 0.0}). haFlatBackConfirmed is false on all twelve BY CONSTRUCTION (every demo seed is a range regime) — recorded honestly; its branches are proven by KATs, not by the corpus. Tests: candleStructure.test.ts (12 KATs: every brokeEma branch incl. the green-body-below and straddle cases, both flat-back sides, the exact-equality no-wick edge, the doji, seed-independence at the 50-bar floor, both producers together); fiveLaneAdapters' stub assertions rewritten to computed-fact assertions (and that the stub export is GONE); guardrail fixture views carry the required facts; the seam test's bare-technical probe updated (the candle binds are required, so a bare namespace now refuses). 764/764; typecheck green. No scoring-law value moves. Co-Authored-By: Claude Fable 5 --- src/enrichment/candleStructure.ts | 114 ++++++++++++++++ src/enrichment/technicalIndicators.ts | 10 ++ src/pipeline/nodes/laneView.ts | 20 ++- src/types/UssLenses.ts | 10 ++ .../mappingPerStrategyResolution.test.ts | 15 ++- .../uwrPerStrategyResolution.test.ts | 2 +- test/guardrails/uwrRuntimeProfile.test.ts | 2 + test/guardrails/uwrStampSemantics.test.ts | 2 + test/oracle/INTENTIONAL_DIFFS.md | 40 ++++++ .../afi-analysis-technical--2.0.0.json | 4 +- .../afi-merge-enriched-view--1.1.0.json | 2 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 6 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.2.0.json | 123 +++++++++++++++++ .../cpj-blofin-perp-long.builtin.json | 22 +-- .../cpj-blofin-perp-long.registry.json | 22 +-- .../cpj-blofin-perp-neutral.builtin.json | 22 +-- .../cpj-blofin-perp-neutral.registry.json | 22 +-- .../cpj-coinbase-spot-sell.builtin.json | 22 +-- .../cpj-coinbase-spot-sell.registry.json | 22 +-- .../goldens/enriched/tv-long.builtin.json | 22 +-- .../goldens/enriched/tv-long.registry.json | 22 +-- .../goldens/enriched/tv-neutral.builtin.json | 62 +++++---- .../goldens/enriched/tv-neutral.registry.json | 62 +++++---- .../goldens/enriched/tv-short.builtin.json | 22 +-- .../goldens/enriched/tv-short.registry.json | 22 +-- test/pipeline/candleStructure.test.ts | 125 ++++++++++++++++++ .../afi-analysis-technical--2.0.0.json | 4 +- .../afi-merge-enriched-view--1.1.0.json | 2 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 6 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.2.0.json | 123 +++++++++++++++++ .../afi-analysis-technical--2.0.0.json | 4 +- .../afi-merge-enriched-view--1.1.0.json | 2 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 6 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.2.0.json | 123 +++++++++++++++++ test/pipeline/officialArtifactPins.test.ts | 4 +- test/pipeline/registryLoader.test.ts | 4 +- test/providers/fiveLaneAdapters.test.ts | 28 ++-- 43 files changed, 959 insertions(+), 178 deletions(-) create mode 100644 src/enrichment/candleStructure.ts create mode 100644 test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json create mode 100644 test/pipeline/candleStructure.test.ts create mode 100644 test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json create mode 100644 test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json diff --git a/src/enrichment/candleStructure.ts b/src/enrichment/candleStructure.ts new file mode 100644 index 00000000..b0c51d48 --- /dev/null +++ b/src/enrichment/candleStructure.ts @@ -0,0 +1,114 @@ +/** + * DEM-PRODUCER-CANDLE — the technical lane's candle-structure producers + * (DEM-GOV §9 DEM-PRODUCER-CANDLE, owner-authorized 2026-08-25; the act + * D5-GOV D-D5-1 reserved). Pure functions over the candle window the lane + * already fetched — no new fetch, no new provider, no params. + * + * Both facts are evaluated on the LAST fetched bar (`candles[len − 1]`), the + * convention every existing kernel uses (technicalIndicators.ts latestCandle; + * patternRecognition.ts "current candle"). On a live feed that bar may still + * be in progress; a closed-bar convention would be a separate ruling that + * moves every kernel together (recorded open item). + * + * brokeEmaWithBody — "breaking the EMA with a body is a structural negative" + * (froggy.trend_pullback_v1.ts:77). The body — not merely a wick — closed on + * the counter-trend side of EMA20 (the EMA the sweet-spot law reads): + * trend bullish → close < ema20; trend bearish → close > ema20; + * range (no side to break against) → the body CROSSED EMA20 on this bar: + * open and close on strictly opposite sides. Colour-free; one law per side. + * + * haFlatBack / haFlatBackConfirmed — Heikin-Ashi flat-back: the recurrence + * haClose = (o + h + l + c) / 4, haOpen = (prevHaOpen + prevHaClose) / 2, + * seeded with haOpen₀ = (o₀ + c₀) / 2 on the FIRST fetched bar, run over + * the WHOLE fetched window. The latest HA bar is a bullish flat-back iff it + * is an up bar (haClose > haOpen) with no lower wick — exactly `low ≥ haOpen` + * (the HA low is min(low, haOpen, haClose)); a bearish flat-back iff it is a + * down bar with no upper wick — `high ≤ haOpen`. Epsilon = 0: exact + * comparison, no tolerance. Confirmed iff the flat-back's side agrees with + * the lane's own EMA trend law (`trendBias`); range → never confirmed. + * + * The trend side used above is the lane's computed fact (EMA20 vs EMA50), never + * a submitted or declared direction (DIR-GOV D-DIR-3). + * + * MATH-GOV: profile math of the reference technical lane; promotes nothing. + */ +import type { AfiCandle } from "../types/AfiCandle.js"; + +export type TrendBias = "bullish" | "bearish" | "range"; +export type HaFlatBack = "bullish" | "bearish" | "none"; + +export interface HeikinAshiBar { + open: number; + high: number; + low: number; + close: number; +} + +/** The Heikin-Ashi recurrence over the whole window (oldest first). */ +export function heikinAshiSeries(candles: readonly AfiCandle[]): HeikinAshiBar[] { + const out: HeikinAshiBar[] = []; + let prev: HeikinAshiBar | undefined; + for (const c of candles) { + const haClose = (c.open + c.high + c.low + c.close) / 4; + const haOpen = prev ? (prev.open + prev.close) / 2 : (c.open + c.close) / 2; + const bar: HeikinAshiBar = { + open: haOpen, + close: haClose, + high: Math.max(c.high, haOpen, haClose), + low: Math.min(c.low, haOpen, haClose), + }; + out.push(bar); + prev = bar; + } + return out; +} + +/** The latest HA bar's flat-back side (epsilon 0). */ +export function latestHaFlatBack(candles: readonly AfiCandle[]): HaFlatBack { + if (candles.length === 0) return "none"; + const series = heikinAshiSeries(candles); + const ha = series[series.length - 1]; + const raw = candles[candles.length - 1]; + if (ha.close > ha.open && raw.low >= ha.open) return "bullish"; + if (ha.close < ha.open && raw.high <= ha.open) return "bearish"; + return "none"; +} + +/** Confirmed iff the flat-back agrees with the lane's trend law; range → false. */ +export function haFlatBackConfirmed(flatBack: HaFlatBack, trendBias: TrendBias): boolean { + return (flatBack === "bullish" && trendBias === "bullish") || (flatBack === "bearish" && trendBias === "bearish"); +} + +/** The latest bar's body closed on the counter-trend side of EMA20 (range: crossed it). */ +export function brokeEmaWithBody(latest: AfiCandle, ema20: number, trendBias: TrendBias): boolean { + if (!Number.isFinite(ema20)) return false; + switch (trendBias) { + case "bullish": + return latest.close < ema20; + case "bearish": + return latest.close > ema20; + default: + return (latest.open < ema20 && latest.close > ema20) || (latest.open > ema20 && latest.close < ema20); + } +} + +export interface CandleStructureFacts { + brokeEmaWithBody: boolean; + haFlatBack: HaFlatBack; + haFlatBackConfirmed: boolean; +} + +/** Both producers over one window (the technical kernel's call site). */ +export function computeCandleStructure( + candles: readonly AfiCandle[], + ema20: number, + trendBias: TrendBias +): CandleStructureFacts { + const latest = candles[candles.length - 1]; + const flatBack = latestHaFlatBack(candles); + return { + brokeEmaWithBody: brokeEmaWithBody(latest, ema20, trendBias), + haFlatBack: flatBack, + haFlatBackConfirmed: haFlatBackConfirmed(flatBack, trendBias), + }; +} diff --git a/src/enrichment/technicalIndicators.ts b/src/enrichment/technicalIndicators.ts index f2c8bc3b..cf01074b 100644 --- a/src/enrichment/technicalIndicators.ts +++ b/src/enrichment/technicalIndicators.ts @@ -15,6 +15,7 @@ import type { TechnicalLensV1 } from "../types/UssLenses.js"; import type { AfiCandle } from "../types/AfiCandle.js"; import { computeFroggyBundle } from "../indicator/froggyProfile.js"; +import { computeCandleStructure } from "./candleStructure.js"; /** * Compute technical enrichment from OHLCV candles. @@ -123,6 +124,12 @@ export function computeTechnicalEnrichment( // too thin, never a guess. const regime = computeAtrRegime(bundle.atrSeries14); + // DEM-PRODUCER-CANDLE: the candle-structure facts over the SAME window + // (no new fetch): brokeEmaWithBody (the body closed on the counter-trend + // side of EMA20) and the Heikin-Ashi flat-back confirmation, both read + // against the lane's own trend law — never a submitted direction. + const structure = computeCandleStructure(candles, ema20, trendBias); + return { ema20, ema50, @@ -135,6 +142,9 @@ export function computeTechnicalEnrichment( ...(regime !== null ? { atrRegime: regime.atrRegime, atrPercentile: regime.atrPercentile } : {}), + brokeEmaWithBody: structure.brokeEmaWithBody, + haFlatBack: structure.haFlatBack, + haFlatBackConfirmed: structure.haFlatBackConfirmed, }; } catch (error) { console.error("❌ Technical enrichment failed:", error); diff --git a/src/pipeline/nodes/laneView.ts b/src/pipeline/nodes/laneView.ts index 78ddd47b..51ed2b16 100644 --- a/src/pipeline/nodes/laneView.ts +++ b/src/pipeline/nodes/laneView.ts @@ -7,19 +7,16 @@ * * These are pure functions over validated governed results. They contain no * vendor logic, no I/O, and no scoring: the scorer-visible values they carry - * (technical.emaDistancePct / isInValueSweetSpot / brokeEmaWithBody stub, - * pattern.patternName / patternConfidence, sentiment.tags) are projected - * byte-identically to the pre-activation runtime. The view ADDITIONALLY + * (technical.emaDistancePct / isInValueSweetSpot / brokeEmaWithBody / + * haFlatBackConfirmed / plan, pattern.patternName / patternConfidence, + * sentiment.tags) are projected verbatim from the lane payloads. The view ADDITIONALLY * carries projected-but-unread lane context (technical.atr14 / trendBias, * pattern.structureBias / trendPullbackConfirmed): no adapter or scorer input * reads them, the view feeds no hash preimage and no golden bytes, so they * change view shape, never scored values — reading any of them in the adapter * is a separately governed change. */ -import { - BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB, - type FroggyEnrichedView, -} from "afi-core/analysts/froggy.enrichment_adapter.js"; +import type { FroggyEnrichedView } from "afi-core/analysts/froggy.enrichment_adapter.js"; import type { TechnicalLensV1 } from "../../types/UssLenses.js"; /** Governed sentiment axis observation (afi.enrichment.sentiment.v1 item). */ @@ -73,10 +70,11 @@ export function viewTechnical( return { emaDistancePct: payload.emaDistancePct, isInValueSweetSpot: payload.isInValueSweetSpot, - // D5 / D5-GOV zero-movement: explicit unimplemented-input stub — NOT a - // computed candle fact. See BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB in - // afi-core/analysts/froggy.enrichment_adapter.ts. - brokeEmaWithBody: BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB, + // DEM-PRODUCER-CANDLE: the lane's COMPUTED candle-structure facts (the + // D5-GOV stub is retired — nothing here is a literal). + brokeEmaWithBody: payload.brokeEmaWithBody, + haFlatBackConfirmed: payload.haFlatBackConfirmed, + haFlatBack: payload.haFlatBack, indicators: { rsi: payload.rsi14, ema_20: payload.ema20, diff --git a/src/types/UssLenses.ts b/src/types/UssLenses.ts index bcc8105b..fa90e663 100644 --- a/src/types/UssLenses.ts +++ b/src/types/UssLenses.ts @@ -68,6 +68,16 @@ export interface TechnicalLensV1 extends UssLens { * the determination instead of producing this block. */ plan?: TechnicalPlanFacts; + /** + * DEM-PRODUCER-CANDLE: the latest bar's body closed on the counter-trend + * side of EMA20 (range: the body crossed it). Always emitted with the + * bundle (the D5-GOV stub is retired). + */ + brokeEmaWithBody?: boolean; + /** Heikin-Ashi flat-back side of the latest HA bar (epsilon 0). */ + haFlatBack?: "bullish" | "bearish" | "none"; + /** The flat-back agrees with the lane's trend law; range → false. */ + haFlatBackConfirmed?: boolean; }; } diff --git a/test/guardrails/mappingPerStrategyResolution.test.ts b/test/guardrails/mappingPerStrategyResolution.test.ts index 17578564..ed3a4e22 100644 --- a/test/guardrails/mappingPerStrategyResolution.test.ts +++ b/test/guardrails/mappingPerStrategyResolution.test.ts @@ -257,7 +257,15 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are symbol: "BTCUSDT", market: "crypto", timeframe: "4h", - technical: { emaDistancePct: 1.5, isInValueSweetSpot: true, atrRegime: "low" }, + technical: { + emaDistancePct: 1.5, + isInValueSweetSpot: true, + atrRegime: "low", + // DEM-PRODUCER-CANDLE: computed lane facts, bound as REQUIRED. + brokeEmaWithBody: false, + haFlatBack: "none", + haFlatBackConfirmed: false, + }, pattern: { patternName: "bull flag", patternConfidence: 80 }, sentiment: { score: 0.4, tags: ["liquidity sweep"] }, }; @@ -306,9 +314,12 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are }); it("a fired grandfathered default is a RECORDED degradation (D-DEM-5(3)), never silent", async () => { + // DEM-PRODUCER-CANDLE: the candle facts are REQUIRED binds, so a bare + // technical namespace refuses; the grandfather/floor firings are probed + // with the lane's computed facts present and everything else absent. const bare = { ...view(), - technical: {}, + technical: { brokeEmaWithBody: false, haFlatBack: "none" as const, haFlatBackConfirmed: false }, pattern: { patternName: "bull flag" }, }; const result = await scorerFroggyTrendPullbackNode.run(bare, ctx(true)); diff --git a/test/guardrails/uwrPerStrategyResolution.test.ts b/test/guardrails/uwrPerStrategyResolution.test.ts index fd07d069..16012e54 100644 --- a/test/guardrails/uwrPerStrategyResolution.test.ts +++ b/test/guardrails/uwrPerStrategyResolution.test.ts @@ -221,7 +221,7 @@ describe("D-CFG-4(4): the scorer node refuses without a resolved config", () => symbol: "BTCUSDT", market: "crypto", timeframe: "4h", - technical: { emaDistancePct: 1.5, isInValueSweetSpot: true, brokeEmaWithBody: false }, + technical: { emaDistancePct: 1.5, isInValueSweetSpot: true, brokeEmaWithBody: false, haFlatBack: "none", haFlatBackConfirmed: false }, pattern: { patternName: "bull flag", patternConfidence: 80 }, sentiment: { score: 0.4, tags: [] }, }; diff --git a/test/guardrails/uwrRuntimeProfile.test.ts b/test/guardrails/uwrRuntimeProfile.test.ts index 194b308b..9c0d3a01 100644 --- a/test/guardrails/uwrRuntimeProfile.test.ts +++ b/test/guardrails/uwrRuntimeProfile.test.ts @@ -503,6 +503,8 @@ describe("PR-UWR-RUNTIME-READ: plugin call-site equivalence (the changed consume emaDistancePct: 1.5, isInValueSweetSpot: true, brokeEmaWithBody: false, + haFlatBack: "none", + haFlatBackConfirmed: false, }, pattern: { patternName: "bull flag", patternConfidence: 80 }, sentiment: { score: 0.4, tags: ["liquidity sweep"] }, diff --git a/test/guardrails/uwrStampSemantics.test.ts b/test/guardrails/uwrStampSemantics.test.ts index 5bb1fd92..f573284b 100644 --- a/test/guardrails/uwrStampSemantics.test.ts +++ b/test/guardrails/uwrStampSemantics.test.ts @@ -140,6 +140,8 @@ function enrichedFixture(): FroggyEnrichedView { emaDistancePct: 1.5, isInValueSweetSpot: true, brokeEmaWithBody: false, + haFlatBack: "none", + haFlatBackConfirmed: false, }, pattern: { patternName: "bull flag", patternConfidence: 80 }, sentiment: { score: 0.4, tags: ["liquidity sweep"] }, diff --git a/test/oracle/INTENTIONAL_DIFFS.md b/test/oracle/INTENTIONAL_DIFFS.md index c4903ea0..0684a02e 100644 --- a/test/oracle/INTENTIONAL_DIFFS.md +++ b/test/oracle/INTENTIONAL_DIFFS.md @@ -593,3 +593,43 @@ Goldens regenerated ONCE via `npm run oracle:regen`; audited with the committed ## D-DEM-5(5) residual, recorded honestly On the CPJ route with a complete plan the risk axis is now a function of the provider's verified R:R and `brokeEmaWithBody` — the Evidence-§3 collinearity is **resolved** there. On every plan-less submission (all TradingView/MarkitTick traffic today, and CPJ submissions without a stop or target) the risk axis reads the declared floor and varies only through `brokeEmaWithBody`: **residual** until a plan carrier exists on that route (§9 determination D-5). The R:R is verified-plausible but submitter-chosen; a stop-placement validity rule would be a new threshold (§8) — a modelling filing. + +--- + +# DEM-PRODUCER-CANDLE reconciliation — brokeEmaWithBody and haFlatBackConfirmed become computed facts (DEM-GOV §9, owner-authorized 2026-08-25) + +**Authority.** DEM-GOV §9 `DEM-PRODUCER-CANDLE` — the act D5-GOV D-D5-1 expressly reserved — with §9 determinations D-2/D-3. D-DEM-7(2) authorizes the movement in class (the structure and risk axes fed by the named inputs); this section is the D-DEM-7(3) itemization. Baseline for every row below is the **`DEM-PRODUCER-PLAN` state** (the wave that merged immediately before), not the pre-slot main. + +**What changed on the path.** The technical kernel computes, over the window it already fetches (no new fetch, no new provider, no new parameter): `brokeEmaWithBody` (the latest bar's body closed on the counter-trend side of EMA20; in a range regime, the body crossed EMA20 on that bar), `haFlatBack` (the Heikin-Ashi flat-back side of the latest HA bar — recurrence over the whole window, seed `(o+c)/2`, epsilon 0) and `haFlatBackConfirmed` (the flat-back agrees with the lane's own EMA20/EMA50 trend law; range → false). `viewTechnical` projects all three; mapping **1.2.0** binds `brokeEmaWithBody` and `haFlatBackConfirmed` as **required** binds (no optionality: a window below the 50-candle kernel floor emits no technical payload and the determination refuses, D-DEM-5(2)). afi-core deletes `BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB`, its `??` read, and the `haFlatBackConfirmed: false` literal; the residual shrinks to the HTF bias placeholders + `liquiditySwept`. `implementationVersion`: technical `2.1.0 → 2.2.0`, merge `1.2.0 → 1.3.0`, scorer `1.4.0 → 1.5.0`; `pluginSetHash` `36f911f4… → 5eef1faf…`; `analystConfigHash` `5cb9b7a4… → 300783e4…` (mappingRef 1.2.0). + +Goldens regenerated ONCE via `npm run oracle:regen`; audited with `node test/oracle/support/goldenDiff.mjs --ref dem/plan-reactor`. + +## Per-golden scored-value movement — **the structure axis stops being a constant** + +| golden | `brokeEmaWithBody` | `haFlatBackConfirmed` | `uwrAxes.structure` | `uwrAxes.risk` | `uwrScore` = `conviction` | +|---|---|---|---|---|---| +| `tv-neutral.{builtin,registry}` (SOL/USDT 15m) | false → **true** | false (unmoved) | **0.4 → 0.15** | 0.2 → **0.0** | 0.225 → 0.1125 | +| the other ten goldens | false → false (now computed) | false → false (now computed) | 0.4 (unmoved) | unmoved | unmoved | + +On the SOL 15m seed the latest demo bar's body crosses EMA20 in a `range` regime, so the producer returns **true**: structure loses its `+0.15` no-break credit and takes the `−0.10` penalty (0.4 → 0.15), and risk takes the rubric's `−0.2` break penalty on top of the PLAN floor (0.2 → 0.0, clamped). **Both axes are therefore non-constant over the fixture corpus** — structure ∈ (0.4, 0.15), risk ∈ (0.5, 0.2, 0.0) — which is this slot's gate. `haFlatBackConfirmed` is `false` on all twelve **by construction**, not by literal: every demo seed's `trendBias` is `range` (the i.i.d. demo feed keeps EMA20 within 0.5% of EMA50), and a range regime has no side to confirm against. The HA branch is therefore proven by the KATs in `test/pipeline/candleStructure.test.ts` (both flat-back sides, the no-wick edge at exact equality, the doji case, seed-independence at the 50-bar floor), not by the corpus. + +## Per-golden identity / payload movement + +| JSON path class | goldens | why | +|---|---|---| +| `…/lenses/0/payload/{brokeEmaWithBody,haFlatBack,haFlatBackConfirmed}` and the `_priceFeedMetadata.technicalIndicators` mirror (absent → value) | all 12 | the three computed facts join the technical payload | +| `/evidenceRecord/composition/enrichmentHash/value` | all 12 | the technical lens payload gained three fields | +| `/evidenceRecord/providerInvocations/4/{providerResultHash,categoryResultHash}/value` (technical) | all 12 | the validated technical CategoryResult gained them | +| `/evidenceRecord/composition/analystConfigHash/value` | all 12 | mappingRef 1.1.0 → 1.2.0 | +| `/evidenceRecord/composition/pluginSetHash/value` | all 12 | three implementationVersion moves | +| `/scorerInput/{brokeEmaWithBody,haFlatBackConfirmed}` | 2 (`tv-neutral.*`, brokeEmaWithBody only) | the computed value differs from the retired stub on that seed alone | +| scored-value class (`uwrAxes.structure`, `uwrAxes.risk`, `uwrScore`, `conviction`, `analystScore.rationale`, both `outputHash` copies) | 2 (`tv-neutral.*`) | the axes moved on that seed | +| `/evidenceRecord/{recordHash,replayHash}/value` | all 12 | consequences of the above | + +## Explicitly byte-EQUAL (asserted by the differ) + +`manifestHash`; `inputHash`/`ingestHash`/`canonicalUss` on **all 12** (this slot touches no ingest surface); `executionSummaryHash` on all 12 (the fired-default set is unchanged — the candle binds are required, so they never fire a default); `decayParams`; `uwrProfile`; `riskBucket`; `scoredSignal.direction`/`meta.direction`; the `execution` and `insight` axes; the pattern/sentiment/news/aiMl lens payloads and their proofs; every other `scorerInput` field; the `312da118…126e06` provenance golden; `ATLAS_MANIFEST_HASH`. No scoring-law value moved. + +## D-DEM-5(5) residual after this slot + +The structure axis is no longer capped at `0.40`, but its two live terms are the sweet-spot credit and the EMA-break term; the **HTF-alignment `+0.4` term and the HA `+0.2` term remain unreachable** — the first because `weeklyBias`/`dailyBias` are still the literal `"neutral"` (that is `DEM-PRODUCER-HTF`), the second because a `range` regime cannot confirm a flat-back and the demo corpus is entirely `range`. Recorded, not hidden. diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index dfdb6dda..81038ac0 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,9 +2,9 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.1.0", + "implementationVersion": "2.2.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs. DEM-PRODUCER-PLAN producer contract: when the canonical signal carries an afi.trade-plan.v1 plan, the lane verifies every submitted level against the fetched candle window (all levels within one observed-range width of the window; stop and targets on opposite sides of entry) and emits technical.plan; an unverifiable plan REFUSES the determination (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): technical.plan is absent when the signal carries no plan; technical.plan.rrToFirstTarget is absent when the plan carries no stop or no target.", + "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef and the runtime resolves the registered technical adapter, which computes indicator features (EMA/RSI/ATR) over the candles it fetches for the validated signal. No vendor logic in the node; deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: over the same window, technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane's trend law). No absence beyond the 50-candle floor.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index 529dd8dd..e78e927c 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.2.0", + "implementationVersion": "1.3.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 45cf960d..3dbd1dcc 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.4.0", + "implementationVersion": "1.5.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index da1a4ddc..ef7bec93 100644 --- a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,7 +22,7 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.1.0" + "version": "1.2.0" }, "decayConfig": { "ratio": { @@ -33,6 +33,10 @@ "metadata": { "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", "supersedes": [ + { + "analystConfigHash": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", + "act": "DEM-GOV DEM-PRODUCER-PLAN — mappingRef froggy-trend-pullback 1.1.0 (2026-08-25)" + }, { "analystConfigHash": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" diff --git a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index b0fabc7c..22a5cd14 100644 --- a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json b/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json new file mode 100644 index 00000000..4171cb38 --- /dev/null +++ b/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json @@ -0,0 +1,123 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.2.0", + "description": "DEM-PRODUCER-CANDLE: the 1.1.0 bindings plus brokeEmaWithBody and haFlatBackConfirmed bound to the technical lane's COMPUTED candle-structure facts (the latest bar's body closed on the counter-trend side of EMA20; the Heikin-Ashi flat-back agreeing with the lane's trend law) — required binds: a window below the 50-bar kernel floor emits no technical payload and the determination refuses (D-DEM-5(2)). The D5-GOV stub and the haFlatBackConfirmed literal are retired.", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + }, + "brokeEmaWithBody": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "brokeEmaWithBody", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "haFlatBackConfirmed": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "haFlatBackConfirmed", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + } + } +} diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json b/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json index 20ad47fa..0b563ec9 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json @@ -80,13 +80,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "18bb43eb7798b6bb32f1494e4e6e5aefe6b8c1a66db06f019469dbccc6e51efd" + "value": "82ba2acc2241f75e1df20c9dc8b3561de335278f2f360ec4e934c80fe01a42c6" }, "executionSummaryHash": { "algorithm": "sha256", @@ -106,7 +106,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -188,7 +188,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "f0adfe203a96427dc6df4fdeba3a2581b9b87a494ad05ef88adf19919e9729d9" + "value": "e4786a2a9dd4d3e1901b7722f89e3736bfbb2cb26478ef494f9e4a84327757ef" }, "provider": { "deterministic": false, @@ -396,7 +396,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "8e1b7f10070a10e5e594877f88251155c6d894c820008415b75767ba0db1aed6" + "value": "f46d7dcba4876cc7014641feede2ba53b4b3670a4c9b51d4ea39def36ccf9d15" }, "credential": { "mode": "keyless" @@ -434,7 +434,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "c69857869e58c01bac4bc993681d1f4449de421d7c9ed8ddb029646f578f096c" + "value": "bcc698ee584268726060d93e5cb162a6348c115a2454a2aaf9a005b6f81e8bc9" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -445,14 +445,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "872c97d513bc807761512571204cd8840265d9b9d8217208735fc0c345605a75" + "value": "664daadb030d1ebe06d034b69ca9686867c7a23729239cb4026c031379c6351f" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "ec9f4c5b3b41835ffff942e10611b33640fe8a483342a8461d9a28c0005e5d05" + "value": "16a659a94892e34ddafae4492feb7b74176eeedbab6c5cedd3f2a5f27d250cfd" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -524,9 +524,12 @@ "atr14": 535.5148709456713, "atrPercentile": 30.5, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 50034.28297385388, "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, + "haFlatBack": "bullish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -587,9 +590,12 @@ "atr14": 535.5148709456713, "atrPercentile": 30.5, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 50034.28297385388, "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, + "haFlatBack": "bullish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json b/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json index c9579269..a8a98070 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json @@ -80,13 +80,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "18bb43eb7798b6bb32f1494e4e6e5aefe6b8c1a66db06f019469dbccc6e51efd" + "value": "82ba2acc2241f75e1df20c9dc8b3561de335278f2f360ec4e934c80fe01a42c6" }, "executionSummaryHash": { "algorithm": "sha256", @@ -106,7 +106,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -188,7 +188,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "f0adfe203a96427dc6df4fdeba3a2581b9b87a494ad05ef88adf19919e9729d9" + "value": "e4786a2a9dd4d3e1901b7722f89e3736bfbb2cb26478ef494f9e4a84327757ef" }, "provider": { "deterministic": false, @@ -396,7 +396,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "8e1b7f10070a10e5e594877f88251155c6d894c820008415b75767ba0db1aed6" + "value": "f46d7dcba4876cc7014641feede2ba53b4b3670a4c9b51d4ea39def36ccf9d15" }, "credential": { "mode": "keyless" @@ -434,7 +434,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "c69857869e58c01bac4bc993681d1f4449de421d7c9ed8ddb029646f578f096c" + "value": "bcc698ee584268726060d93e5cb162a6348c115a2454a2aaf9a005b6f81e8bc9" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -445,14 +445,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "ccb66974486ea9a30de99b307037aabf849983451cb7bd25f0f3611ad77f824e" + "value": "7a1bfbd7c17688eb19ffda93734b6538b8e156502a006e0a56a3ecc2ad6db123" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "bb0c4e6e9554207cb6d9b36190992e9ad43cfd0f74db78f0cd1f0409f6aaa0c6" + "value": "fdd98d2074a66bb52e880d82d3040c0678593639d36d60bc957215e3048259b0" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -524,9 +524,12 @@ "atr14": 535.5148709456713, "atrPercentile": 30.5, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 50034.28297385388, "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, + "haFlatBack": "bullish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -587,9 +590,12 @@ "atr14": 535.5148709456713, "atrPercentile": 30.5, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 50034.28297385388, "ema50": 50000.250065789325, "emaDistancePct": 0.6212694575958345, + "haFlatBack": "bullish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json index 88a30bdb..c6547f75 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json @@ -70,13 +70,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "9d0d44f58de79c7bf48176dbb5a5096ec4b8d06b9ea882a5ce5aea73f9a8f067" + "value": "32e190db3f5a539ef718b3dfbfad0949236ae4e3fdfdc70c6cb9dde1ab0bad13" }, "executionSummaryHash": { "algorithm": "sha256", @@ -96,7 +96,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -178,7 +178,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "37de5af583b33c5c9e21370b19f7770d3ae5ef2fffcde02804079f3f97d6351c" + "value": "27f4e9cf35f58cb89e06c2fb5b6f9d7e527748c919617b745ed28b2ef2da40a3" }, "provider": { "deterministic": false, @@ -386,7 +386,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "16a53244cb29945932a6d4cb9154ae5bea77c32f19db320eecb729bf196b04ad" + "value": "f3dbeede7f9fec7ef2e7b718dde0427a15c3e095e91fad3f58ebaeba3cea5e2e" }, "credential": { "mode": "keyless" @@ -424,7 +424,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "2b53c11e84f553e9b832c200b0a2e175117e324b3744e18831b3760243b4d26c" + "value": "d5702421d230b0143a3448026367a4cef6078abb02fc2de104c1ce8bab85b1c2" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -435,14 +435,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "2d3c61dc5d15cce19420deb2174801228d38291a94cd437578c3c09c0e65fcff" + "value": "86faf6d12c3de1340b71b50550620fefcf2b7f7257f0f96376b3292c3d610004" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "562068cd1d9b3015249bc7982c80863e05297591994d5c959ce98e924a650cdc" + "value": "a48e72b91affc90e1f16c2fe19acb280ec866db9017107aad64fd9f1fb8cbfe0" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -502,9 +502,12 @@ "atr14": 1.1685809655298571, "atrPercentile": 94.8, "atrRegime": "high", + "brokeEmaWithBody": false, "ema20": 100.01045172807879, "ema50": 100.03568834253537, "emaDistancePct": 0.2503722265458551, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", @@ -558,9 +561,12 @@ "atr14": 1.1685809655298571, "atrPercentile": 94.8, "atrRegime": "high", + "brokeEmaWithBody": false, "ema20": 100.01045172807879, "ema50": 100.03568834253537, "emaDistancePct": 0.2503722265458551, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json index 6a27f570..ae1b4ae2 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json @@ -70,13 +70,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "9d0d44f58de79c7bf48176dbb5a5096ec4b8d06b9ea882a5ce5aea73f9a8f067" + "value": "32e190db3f5a539ef718b3dfbfad0949236ae4e3fdfdc70c6cb9dde1ab0bad13" }, "executionSummaryHash": { "algorithm": "sha256", @@ -96,7 +96,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -178,7 +178,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "37de5af583b33c5c9e21370b19f7770d3ae5ef2fffcde02804079f3f97d6351c" + "value": "27f4e9cf35f58cb89e06c2fb5b6f9d7e527748c919617b745ed28b2ef2da40a3" }, "provider": { "deterministic": false, @@ -386,7 +386,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "16a53244cb29945932a6d4cb9154ae5bea77c32f19db320eecb729bf196b04ad" + "value": "f3dbeede7f9fec7ef2e7b718dde0427a15c3e095e91fad3f58ebaeba3cea5e2e" }, "credential": { "mode": "keyless" @@ -424,7 +424,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "2b53c11e84f553e9b832c200b0a2e175117e324b3744e18831b3760243b4d26c" + "value": "d5702421d230b0143a3448026367a4cef6078abb02fc2de104c1ce8bab85b1c2" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -435,14 +435,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "746c77f73e55fb392f42f20cacf4a31073d40fa4e18eaf410ab9b531510a4787" + "value": "43505099c95eb52d33b9d3c12d2f038ba3f10e01465215de9716e54c251110ef" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "8ee23c624347df6f11ab3c3a56231f0579091f91edfd7e6d86688e2281a717d1" + "value": "521e20bf5978bd3a56408e32e50d63b4398341299e5f4a2b009841500eee7ad2" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -502,9 +502,12 @@ "atr14": 1.1685809655298571, "atrPercentile": 94.8, "atrRegime": "high", + "brokeEmaWithBody": false, "ema20": 100.01045172807879, "ema50": 100.03568834253537, "emaDistancePct": 0.2503722265458551, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", @@ -558,9 +561,12 @@ "atr14": 1.1685809655298571, "atrPercentile": 94.8, "atrRegime": "high", + "brokeEmaWithBody": false, "ema20": 100.01045172807879, "ema50": 100.03568834253537, "emaDistancePct": 0.2503722265458551, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json index cd351d9d..3001fdd4 100644 --- a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json +++ b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json @@ -74,13 +74,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "0f706295d4b2180475d044b2931bf6fd51c89789a59da082619adfb1380ff4a2" + "value": "1f9c4d6d6487f75c109c60431a90c832f8f16d1872072f4a8a8d0dc158b97d19" }, "executionSummaryHash": { "algorithm": "sha256", @@ -100,7 +100,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -182,7 +182,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b55250716818e6f3faaaf74ccf4e0569f17268469606969afdcd0b1267eb505e" + "value": "ccded69c3b51ad4c8746369efb28e0aa9c5d8ea6bad4524e30a48de0d88eaa00" }, "provider": { "deterministic": false, @@ -390,7 +390,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "435bb57dc7dc5f3ada1691e1e4a4f7fb2b35ea6c16e9bc9435bca67cb905c552" + "value": "b48fbd359172adfde85b572c31cfbcef4ee647463046ca6a5201434a1a7c17b1" }, "credential": { "mode": "keyless" @@ -428,7 +428,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "1323407907c4a9eefc8eaa4650b386e49a8f582c21299017d17d43d09ea9d403" + "value": "6c274ea4f97c60082510afb84de3244427a647914159c6b21199fa884f071e09" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -439,14 +439,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "33a4e9286ed946bc360d2efb59dbd40e3a26a548f5a111e279f3a13eec4f179c" + "value": "d860612ce70427ca842ef55c836a1b712641efe684270dd785efa918c3e45744" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "ca32f15d3377c4c685da0f7727521366bf6bd07188386a92347a06220f78b430" + "value": "95ae5c24b528c6e80e9116f8362ebc6b8179c6968080ab3382127cba1783f83a" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -518,9 +518,12 @@ "atr14": 31.2485468200695, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 2998.1511085761304, "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, + "haFlatBack": "bearish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -578,9 +581,12 @@ "atr14": 31.2485468200695, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 2998.1511085761304, "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, + "haFlatBack": "bearish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, diff --git a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json index f023b492..6b713fd0 100644 --- a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json +++ b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json @@ -74,13 +74,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "0f706295d4b2180475d044b2931bf6fd51c89789a59da082619adfb1380ff4a2" + "value": "1f9c4d6d6487f75c109c60431a90c832f8f16d1872072f4a8a8d0dc158b97d19" }, "executionSummaryHash": { "algorithm": "sha256", @@ -100,7 +100,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -182,7 +182,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b55250716818e6f3faaaf74ccf4e0569f17268469606969afdcd0b1267eb505e" + "value": "ccded69c3b51ad4c8746369efb28e0aa9c5d8ea6bad4524e30a48de0d88eaa00" }, "provider": { "deterministic": false, @@ -390,7 +390,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "435bb57dc7dc5f3ada1691e1e4a4f7fb2b35ea6c16e9bc9435bca67cb905c552" + "value": "b48fbd359172adfde85b572c31cfbcef4ee647463046ca6a5201434a1a7c17b1" }, "credential": { "mode": "keyless" @@ -428,7 +428,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "1323407907c4a9eefc8eaa4650b386e49a8f582c21299017d17d43d09ea9d403" + "value": "6c274ea4f97c60082510afb84de3244427a647914159c6b21199fa884f071e09" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -439,14 +439,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "6b9c06c256913209f0338fda4562d9730ce01062755ddf0cdba8f5c8abb11cf7" + "value": "1dace50248ededef7bd87e0372c55d90eb481a00acd89d092467d0e68a4e67bd" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "a883346b80e6f93a802f567dabc6432c4928029fd0629350d3ad2a8f71bcfd95" + "value": "408b7f91e08658a13664650f8b99fff8c8fa40243e893fe9db9378e20a2075ad" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -518,9 +518,12 @@ "atr14": 31.2485468200695, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 2998.1511085761304, "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, + "haFlatBack": "bearish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -578,9 +581,12 @@ "atr14": 31.2485468200695, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 2998.1511085761304, "ema50": 2999.0429709032783, "emaDistancePct": -0.6511453704633388, + "haFlatBack": "bearish", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "plan": { "barCount": 100, diff --git a/test/oracle/goldens/enriched/tv-long.builtin.json b/test/oracle/goldens/enriched/tv-long.builtin.json index 49189cec..17734cfb 100644 --- a/test/oracle/goldens/enriched/tv-long.builtin.json +++ b/test/oracle/goldens/enriched/tv-long.builtin.json @@ -62,13 +62,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "52c47876bab954d516c3d4fd9532004e511ca4bfd82a2d7807bd69cfcef24617" + "value": "9a29ed2dfac80ba25308e967a24158500cd56206b9931e0ac9cd107776f14396" }, "executionSummaryHash": { "algorithm": "sha256", @@ -88,7 +88,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -170,7 +170,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "bbba64122687fc9c49ef9ddf4fcf1cba8f0ea0f7d63cb22137997e9cdcd17f56" + "value": "b0d82fde7aa84689f5c74f8ef92fd9d2b5cf2d0eac44da403a3ded1cae347849" }, "provider": { "deterministic": false, @@ -378,7 +378,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "99a6c9153825e84772bb2bbf9655bfab62ffd4e9c516a7621f5afbaa9b5b98b1" + "value": "c7499752e7ce6e3ad09fc0a1761ae78a23d9d5c54d4983dd75c2eead2bb59beb" }, "credential": { "mode": "keyless" @@ -416,7 +416,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "857b658b6a177c5ac27be25613460defdacb7c9a2dc01e2da2e1e82016a6a16f" + "value": "f26e948ac64636fed339e652edd25897ebd0cae1caefab3eb9ebe9bbfb54001e" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -427,14 +427,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "099bf59336c702007135dcd802f7eb16d67b1bd884159d096cc2adad7ec1ad60" + "value": "23f37872ab1f142cfea6f0e203ce54e51543d8f3683398a455daa6cb043dfbdf" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "98d1dfaefe53f2964648b1a5dfd41c799c26581d887b2eb93e8baa01fef2fe74" + "value": "26fbf76af838aa2c470314840d995d8765d148cf17f38fbb33281f838a0fe3d2" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -497,9 +497,12 @@ "atr14": 570.2640858689773, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 49996.24399889079, "ema50": 50023.438454448326, "emaDistancePct": 0.12667804556691256, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", @@ -548,9 +551,12 @@ "atr14": 570.2640858689773, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 49996.24399889079, "ema50": 50023.438454448326, "emaDistancePct": 0.12667804556691256, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/tv-long.registry.json b/test/oracle/goldens/enriched/tv-long.registry.json index 9e7f66e3..5d3b3b3b 100644 --- a/test/oracle/goldens/enriched/tv-long.registry.json +++ b/test/oracle/goldens/enriched/tv-long.registry.json @@ -62,13 +62,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "52c47876bab954d516c3d4fd9532004e511ca4bfd82a2d7807bd69cfcef24617" + "value": "9a29ed2dfac80ba25308e967a24158500cd56206b9931e0ac9cd107776f14396" }, "executionSummaryHash": { "algorithm": "sha256", @@ -88,7 +88,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -170,7 +170,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "bbba64122687fc9c49ef9ddf4fcf1cba8f0ea0f7d63cb22137997e9cdcd17f56" + "value": "b0d82fde7aa84689f5c74f8ef92fd9d2b5cf2d0eac44da403a3ded1cae347849" }, "provider": { "deterministic": false, @@ -378,7 +378,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "99a6c9153825e84772bb2bbf9655bfab62ffd4e9c516a7621f5afbaa9b5b98b1" + "value": "c7499752e7ce6e3ad09fc0a1761ae78a23d9d5c54d4983dd75c2eead2bb59beb" }, "credential": { "mode": "keyless" @@ -416,7 +416,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "857b658b6a177c5ac27be25613460defdacb7c9a2dc01e2da2e1e82016a6a16f" + "value": "f26e948ac64636fed339e652edd25897ebd0cae1caefab3eb9ebe9bbfb54001e" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -427,14 +427,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "4515e1afc437909c07930adbb85c25cb4cf2de16054025e5c2b6cb15400dab3c" + "value": "2d5c6a0197d657609d38cd88868f908b41733bb42612bfc0deecfaa2a3398679" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "1d1619ea81e682bcdecf5c492c2ba1bc296feafd6af8fec279738d50860b678b" + "value": "831353ad7671ad27c3484740f83671c19a57bdd6eb09223df08fcd2801d904d0" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -497,9 +497,12 @@ "atr14": 570.2640858689773, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 49996.24399889079, "ema50": 50023.438454448326, "emaDistancePct": 0.12667804556691256, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", @@ -548,9 +551,12 @@ "atr14": 570.2640858689773, "atrPercentile": 70.7, "atrRegime": "normal", + "brokeEmaWithBody": false, "ema20": 49996.24399889079, "ema50": 50023.438454448326, "emaDistancePct": 0.12667804556691256, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/tv-neutral.builtin.json b/test/oracle/goldens/enriched/tv-neutral.builtin.json index b10e754c..93a996ec 100644 --- a/test/oracle/goldens/enriched/tv-neutral.builtin.json +++ b/test/oracle/goldens/enriched/tv-neutral.builtin.json @@ -4,16 +4,17 @@ "assetClass": "crypto", "axisNotes": { "execution": "Trigger pattern quality is low for this setup.", - "insight": "Liquidity or volatility context is weak." + "insight": "Liquidity or volatility context is weak.", + "structure": "Weak HTF structure or EMA context." }, "baseAsset": "BTC", - "conviction": 0.22500000000000003, + "conviction": 0.11249999999999999, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", "quoteAsset": "USDT", - "rationale": "Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", + "rationale": "Weak HTF structure or EMA context. Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", "riskBucket": "high", "scoredAt": "", "signalTimeframe": "15m", @@ -27,10 +28,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.2, - "structure": 0.4 + "risk": 0, + "structure": 0.15 }, - "uwrScore": 0.22500000000000003 + "uwrScore": 0.11249999999999999 }, "canonicalUss": { "facts": { @@ -67,13 +68,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "930cacbd347c16a5b3238f6c65dc4efbfe3fc5a4d91bac7e4ef0d44be39d2919" + "value": "cfb5c348c4452c3aa8a9f8896ae6cdc06f53b1a73147a142c751300fa6aa49e4" }, "executionSummaryHash": { "algorithm": "sha256", @@ -93,7 +94,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -117,7 +118,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" + "value": "e2ed9f66d5758fbc392f408dc93e41e2af326e015bd4c02716197dce8d806f20" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -175,7 +176,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "a3e03897b25fa26305c4a5ad2b5e256f152c7ed2f8c90dd3cd94e45d65db8eef" + "value": "b79d3b1c17058895eb2bff5aad85cc8b1f4dcc35fb8e1d056944055fcc91829e" }, "provider": { "deterministic": false, @@ -383,7 +384,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "a858f4010da9cc3a87336ad2ac86ec2b63f1597cdc235fee05ff7e7afca40433" + "value": "137adc643c56ad6cc9bef935a428966de9d026469c4622a221056ad747b9c2b1" }, "credential": { "mode": "keyless" @@ -421,7 +422,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "7b6da09bad1a1f6c227077a3b4cb7f9e12ffaebe088511e732a5ca737a47cc4a" + "value": "c96a3ce4e9bcab948c56421e43a45c368f372a40911981b26ba323bf5586ec72" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -432,19 +433,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "62d321e11ad3dd92e2d125976f1c6971207089ba25d49fdb65fd3f4d24763448" + "value": "6f0e0d4c4784f3130674415b9f809cfa671a41d393891324c75f5dec9827a495" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "74884fb9b27731275bd99e8707e57493ab24049e85c48bd81911fc79a8378aa1" + "value": "327bc0a90a22aef1cc94522cca0d3b4a431f3fbc4c29d850b1a55153c677939a" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.22500000000000003, + "conviction": 0.11249999999999999, "direction": "neutral", "provenanceRecordRef": "provenance-record:oracle-tv-neutral-0001", "providerId": "oracle-provider-tv", @@ -456,10 +457,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.2, - "structure": 0.4 + "risk": 0, + "structure": 0.15 }, - "uwrScore": 0.22500000000000003 + "uwrScore": 0.11249999999999999 }, "signalId": "oracle-tv-neutral-0001", "strategyId": "trend_pullback_v1", @@ -490,9 +491,12 @@ "atr14": 1.152376597395163, "atrPercentile": 86.8, "atrRegime": "high", + "brokeEmaWithBody": true, "ema20": 99.88197044251669, "ema50": 99.9447608232299, "emaDistancePct": -0.32435456034904336, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", @@ -505,16 +509,17 @@ "assetClass": "crypto", "axisNotes": { "execution": "Trigger pattern quality is low for this setup.", - "insight": "Liquidity or volatility context is weak." + "insight": "Liquidity or volatility context is weak.", + "structure": "Weak HTF structure or EMA context." }, "baseAsset": "BTC", - "conviction": 0.22500000000000003, + "conviction": 0.11249999999999999, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", "quoteAsset": "USDT", - "rationale": "Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", + "rationale": "Weak HTF structure or EMA context. Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", "riskBucket": "high", "scoredAt": "", "signalTimeframe": "15m", @@ -528,10 +533,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.2, - "structure": 0.4 + "risk": 0, + "structure": 0.15 }, - "uwrScore": 0.22500000000000003 + "uwrScore": 0.11249999999999999 }, "decayParams": { "barsPerHalfLife": 12, @@ -546,9 +551,12 @@ "atr14": 1.152376597395163, "atrPercentile": 86.8, "atrRegime": "high", + "brokeEmaWithBody": true, "ema20": 99.88197044251669, "ema50": 99.9447608232299, "emaDistancePct": -0.32435456034904336, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", @@ -648,11 +656,11 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" + "value": "e2ed9f66d5758fbc392f408dc93e41e2af326e015bd4c02716197dce8d806f20" }, "scorerInput": { "atrRegime": "high", - "brokeEmaWithBody": false, + "brokeEmaWithBody": true, "dailyBias": "neutral", "distanceFromDailyEmaPct": -0.32435456034904336, "haFlatBackConfirmed": false, diff --git a/test/oracle/goldens/enriched/tv-neutral.registry.json b/test/oracle/goldens/enriched/tv-neutral.registry.json index 8882e4f9..d212831f 100644 --- a/test/oracle/goldens/enriched/tv-neutral.registry.json +++ b/test/oracle/goldens/enriched/tv-neutral.registry.json @@ -4,16 +4,17 @@ "assetClass": "crypto", "axisNotes": { "execution": "Trigger pattern quality is low for this setup.", - "insight": "Liquidity or volatility context is weak." + "insight": "Liquidity or volatility context is weak.", + "structure": "Weak HTF structure or EMA context." }, "baseAsset": "BTC", - "conviction": 0.22500000000000003, + "conviction": 0.11249999999999999, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", "quoteAsset": "USDT", - "rationale": "Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", + "rationale": "Weak HTF structure or EMA context. Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", "riskBucket": "high", "scoredAt": "", "signalTimeframe": "15m", @@ -27,10 +28,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.2, - "structure": 0.4 + "risk": 0, + "structure": 0.15 }, - "uwrScore": 0.22500000000000003 + "uwrScore": 0.11249999999999999 }, "canonicalUss": { "facts": { @@ -67,13 +68,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "930cacbd347c16a5b3238f6c65dc4efbfe3fc5a4d91bac7e4ef0d44be39d2919" + "value": "cfb5c348c4452c3aa8a9f8896ae6cdc06f53b1a73147a142c751300fa6aa49e4" }, "executionSummaryHash": { "algorithm": "sha256", @@ -93,7 +94,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -117,7 +118,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" + "value": "e2ed9f66d5758fbc392f408dc93e41e2af326e015bd4c02716197dce8d806f20" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -175,7 +176,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "a3e03897b25fa26305c4a5ad2b5e256f152c7ed2f8c90dd3cd94e45d65db8eef" + "value": "b79d3b1c17058895eb2bff5aad85cc8b1f4dcc35fb8e1d056944055fcc91829e" }, "provider": { "deterministic": false, @@ -383,7 +384,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "a858f4010da9cc3a87336ad2ac86ec2b63f1597cdc235fee05ff7e7afca40433" + "value": "137adc643c56ad6cc9bef935a428966de9d026469c4622a221056ad747b9c2b1" }, "credential": { "mode": "keyless" @@ -421,7 +422,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "7b6da09bad1a1f6c227077a3b4cb7f9e12ffaebe088511e732a5ca737a47cc4a" + "value": "c96a3ce4e9bcab948c56421e43a45c368f372a40911981b26ba323bf5586ec72" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -432,19 +433,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "eda306ba90a6044983f85e587e50483315d9369b1d5510c80a77960f9de00c84" + "value": "d11f22bbde3f5e653716dbdb6450e32c3de1063ab1a201507cd83e18aba0ddd4" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "3c41aae7db36f10475805e59fd171cd7ad19769dd60b7fa8170c4b3c4c731c60" + "value": "ba08fb62f47dc15d7bce1392b5b0ee1e54d0c90a4725af62fea2c6124101f595" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.22500000000000003, + "conviction": 0.11249999999999999, "direction": "neutral", "provenanceRecordRef": "provenance-record:oracle-tv-neutral-0001", "providerId": "oracle-provider-tv", @@ -456,10 +457,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.2, - "structure": 0.4 + "risk": 0, + "structure": 0.15 }, - "uwrScore": 0.22500000000000003 + "uwrScore": 0.11249999999999999 }, "signalId": "oracle-tv-neutral-0001", "strategyId": "trend_pullback_v1", @@ -490,9 +491,12 @@ "atr14": 1.152376597395163, "atrPercentile": 86.8, "atrRegime": "high", + "brokeEmaWithBody": true, "ema20": 99.88197044251669, "ema50": 99.9447608232299, "emaDistancePct": -0.32435456034904336, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", @@ -505,16 +509,17 @@ "assetClass": "crypto", "axisNotes": { "execution": "Trigger pattern quality is low for this setup.", - "insight": "Liquidity or volatility context is weak." + "insight": "Liquidity or volatility context is weak.", + "structure": "Weak HTF structure or EMA context." }, "baseAsset": "BTC", - "conviction": 0.22500000000000003, + "conviction": 0.11249999999999999, "direction": "neutral", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", "quoteAsset": "USDT", - "rationale": "Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", + "rationale": "Weak HTF structure or EMA context. Trigger pattern quality is low for this setup. Liquidity or volatility context is weak.", "riskBucket": "high", "scoredAt": "", "signalTimeframe": "15m", @@ -528,10 +533,10 @@ "uwrAxes": { "execution": 0, "insight": 0.3, - "risk": 0.2, - "structure": 0.4 + "risk": 0, + "structure": 0.15 }, - "uwrScore": 0.22500000000000003 + "uwrScore": 0.11249999999999999 }, "decayParams": { "barsPerHalfLife": 12, @@ -546,9 +551,12 @@ "atr14": 1.152376597395163, "atrPercentile": 86.8, "atrRegime": "high", + "brokeEmaWithBody": true, "ema20": 99.88197044251669, "ema50": 99.9447608232299, "emaDistancePct": -0.32435456034904336, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", @@ -648,11 +656,11 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "d042a5fa7ba5ccd0a58bfc5ac267f8c5e0cc3c33fc4770ce75e979513c18097b" + "value": "e2ed9f66d5758fbc392f408dc93e41e2af326e015bd4c02716197dce8d806f20" }, "scorerInput": { "atrRegime": "high", - "brokeEmaWithBody": false, + "brokeEmaWithBody": true, "dailyBias": "neutral", "distanceFromDailyEmaPct": -0.32435456034904336, "haFlatBackConfirmed": false, diff --git a/test/oracle/goldens/enriched/tv-short.builtin.json b/test/oracle/goldens/enriched/tv-short.builtin.json index c41e08d9..9a2c17db 100644 --- a/test/oracle/goldens/enriched/tv-short.builtin.json +++ b/test/oracle/goldens/enriched/tv-short.builtin.json @@ -67,13 +67,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "f2357ac44131f876be3052a3f8bc37c9cd3f0944d8c0f472ebc85a6408286619" + "value": "6d71a67e34c67d35b7a07ee9344dd8828cbff409cac23fc795b4abf0c6615bd1" }, "executionSummaryHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -175,7 +175,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "5f9005c500a2cc151c0c9a604bd4deb4d65c49732a12a226219019b1745668aa" + "value": "b144b16d61dae4e9403ed9ec0ab79acca23fe9837f23e03398309eea19b31015" }, "provider": { "deterministic": false, @@ -383,7 +383,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "ecbc588bcb89fd35d24a5821fb00a208e605f064ffed0d602a06016210cb4a56" + "value": "5252fad0a334a2eaf7f0472a1e58a8ff7fa376cc7be72191ca3a2a24a47fea42" }, "credential": { "mode": "keyless" @@ -421,7 +421,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "8e0249156192f08ce95945c2d9ee9428b92c403fd726aee2550570d5cc5e7e18" + "value": "3f89d32f09cbecfaf595cc879d1fe3d9dbb5fdd4f5b1491c48b8e13ceb128be1" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -432,14 +432,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "9e81a4e83e1d40d065e5ab846530a4a819ce658672ea810e479b9075a576a24c" + "value": "43649bac3580c41221fdeb392a771047a0787928f82a7951f7fd74a9ad5dbf1b" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "e3e0ce534f54d4332c0abea71beaadc0c2b3c40b185b94a63943f2d50f5110a1" + "value": "53f0a98fe4e6ee136a141bfdddf9aa851def2f00897cac2ad586e81e612a88ce" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -490,9 +490,12 @@ "atr14": 31.682075317091098, "atrPercentile": 9.8, "atrRegime": "low", + "brokeEmaWithBody": false, "ema20": 2995.7445905266545, "ema50": 2996.409633835594, "emaDistancePct": 0.2706436977661548, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", @@ -546,9 +549,12 @@ "atr14": 31.682075317091098, "atrPercentile": 9.8, "atrRegime": "low", + "brokeEmaWithBody": false, "ema20": 2995.7445905266545, "ema50": 2996.409633835594, "emaDistancePct": 0.2706436977661548, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/tv-short.registry.json b/test/oracle/goldens/enriched/tv-short.registry.json index 492e8764..2f4a27ad 100644 --- a/test/oracle/goldens/enriched/tv-short.registry.json +++ b/test/oracle/goldens/enriched/tv-short.registry.json @@ -67,13 +67,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "f2357ac44131f876be3052a3f8bc37c9cd3f0944d8c0f472ebc85a6408286619" + "value": "6d71a67e34c67d35b7a07ee9344dd8828cbff409cac23fc795b4abf0c6615bd1" }, "executionSummaryHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -175,7 +175,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "5f9005c500a2cc151c0c9a604bd4deb4d65c49732a12a226219019b1745668aa" + "value": "b144b16d61dae4e9403ed9ec0ab79acca23fe9837f23e03398309eea19b31015" }, "provider": { "deterministic": false, @@ -383,7 +383,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "ecbc588bcb89fd35d24a5821fb00a208e605f064ffed0d602a06016210cb4a56" + "value": "5252fad0a334a2eaf7f0472a1e58a8ff7fa376cc7be72191ca3a2a24a47fea42" }, "credential": { "mode": "keyless" @@ -421,7 +421,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "8e0249156192f08ce95945c2d9ee9428b92c403fd726aee2550570d5cc5e7e18" + "value": "3f89d32f09cbecfaf595cc879d1fe3d9dbb5fdd4f5b1491c48b8e13ceb128be1" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -432,14 +432,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "f961f8367bf5298131a3832e862d041ff0b2b027746e152b71f4778d769e9966" + "value": "854c7745a49520fc702c6714988b26570fb83de8f53e1b96f405ab7533c99c23" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "c7a83402f2891c326b5bbfbabadd5acdac02a4f39f61cd5c7b44ad5ae9d7d1e9" + "value": "edea624a276036e831ca3d18caba08cbf3beb1ffaf8eb1cb5f96b12abb56ffdc" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -490,9 +490,12 @@ "atr14": 31.682075317091098, "atrPercentile": 9.8, "atrRegime": "low", + "brokeEmaWithBody": false, "ema20": 2995.7445905266545, "ema50": 2996.409633835594, "emaDistancePct": 0.2706436977661548, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", @@ -546,9 +549,12 @@ "atr14": 31.682075317091098, "atrPercentile": 9.8, "atrRegime": "low", + "brokeEmaWithBody": false, "ema20": 2995.7445905266545, "ema50": 2996.409633835594, "emaDistancePct": 0.2706436977661548, + "haFlatBack": "none", + "haFlatBackConfirmed": false, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", diff --git a/test/pipeline/candleStructure.test.ts b/test/pipeline/candleStructure.test.ts new file mode 100644 index 00000000..261a9dcd --- /dev/null +++ b/test/pipeline/candleStructure.test.ts @@ -0,0 +1,125 @@ +/** + * DEM-PRODUCER-CANDLE — known-answer vectors for the candle-structure + * producers (brokeEmaWithBody, Heikin-Ashi flat-back) over hand-built + * windows, every branch on both sides of its edge. + */ +import { describe, it, expect } from "@jest/globals"; +import { + brokeEmaWithBody, + computeCandleStructure, + haFlatBackConfirmed, + heikinAshiSeries, + latestHaFlatBack, +} from "../../src/enrichment/candleStructure.js"; +import type { AfiCandle } from "../../src/types/AfiCandle.js"; + +function bar(open: number, high: number, low: number, close: number): AfiCandle { + return { timestamp: 0, open, high, low, close, volume: 1 }; +} + +describe("brokeEmaWithBody — the body closed on the counter-trend side of EMA20", () => { + it("bullish trend: true iff close < ema20, regardless of bar colour or open", () => { + expect(brokeEmaWithBody(bar(101, 102, 98, 99), 100, "bullish")).toBe(true); // red body through the EMA + expect(brokeEmaWithBody(bar(98, 99.5, 97, 99), 100, "bullish")).toBe(true); // green body entirely below (not a colour test) + expect(brokeEmaWithBody(bar(101, 102, 98, 100.5), 100, "bullish")).toBe(false); // wick through, body above + expect(brokeEmaWithBody(bar(101, 102, 99, 100), 100, "bullish")).toBe(false); // close exactly ON the EMA is not beyond it + }); + + it("bearish trend: true iff close > ema20", () => { + expect(brokeEmaWithBody(bar(99, 102, 98, 101), 100, "bearish")).toBe(true); + expect(brokeEmaWithBody(bar(102, 103, 100.5, 101), 100, "bearish")).toBe(true); + expect(brokeEmaWithBody(bar(99, 102, 98, 99.5), 100, "bearish")).toBe(false); + expect(brokeEmaWithBody(bar(99, 102, 98, 100), 100, "bearish")).toBe(false); + }); + + it("range: true iff the body CROSSED the EMA on this bar (open and close on strictly opposite sides)", () => { + expect(brokeEmaWithBody(bar(99, 102, 98, 101), 100, "range")).toBe(true); // up through + expect(brokeEmaWithBody(bar(101, 102, 98, 99), 100, "range")).toBe(true); // down through + expect(brokeEmaWithBody(bar(101, 103, 100.5, 102), 100, "range")).toBe(false); // body entirely above + expect(brokeEmaWithBody(bar(98, 99.5, 97, 99), 100, "range")).toBe(false); // body entirely below + expect(brokeEmaWithBody(bar(100, 102, 98, 101), 100, "range")).toBe(false); // open ON the EMA: not a crossing + expect(brokeEmaWithBody(bar(99, 102, 98, 100), 100, "range")).toBe(false); // close ON the EMA: not a crossing + }); + + it("a non-finite EMA never breaks (defensive; unreachable behind the kernel floor)", () => { + expect(brokeEmaWithBody(bar(99, 102, 98, 101), Number.NaN, "bullish")).toBe(false); + }); +}); + +describe("Heikin-Ashi recurrence and flat-back (seed (o+c)/2, whole window, epsilon 0)", () => { + it("first bar seeds haOpen = (open + close) / 2; subsequent haOpen = (prevHaOpen + prevHaClose) / 2", () => { + const s = heikinAshiSeries([bar(10, 12, 9, 11), bar(11, 13, 10, 12)]); + expect(s[0]).toEqual({ open: 10.5, close: 10.5, high: 12, low: 9 }); + expect(s[1].open).toBe((10.5 + 10.5) / 2); + expect(s[1].close).toBe((11 + 13 + 10 + 12) / 4); + expect(s[1].high).toBe(Math.max(13, s[1].open, s[1].close)); + expect(s[1].low).toBe(Math.min(10, s[1].open, s[1].close)); + }); + + it("bullish flat-back: an up HA bar whose raw low is ≥ haOpen (no lower wick); exact equality counts", () => { + // Prior bars establish haOpen; the last bar's low sits exactly at haOpen. + const prev = [bar(100, 101, 99, 100), bar(100, 101, 99, 100)]; + const haOpenLast = (() => { + const s = heikinAshiSeries([...prev, bar(100, 101, 99, 100)]); + return s[s.length - 1].open; + })(); + const flat = [...prev, bar(haOpenLast, haOpenLast + 3, haOpenLast, haOpenLast + 2)]; + expect(latestHaFlatBack(flat)).toBe("bullish"); + const wick = [...prev, bar(haOpenLast, haOpenLast + 3, haOpenLast - 0.0001, haOpenLast + 2)]; + expect(latestHaFlatBack(wick)).toBe("none"); + }); + + it("bearish flat-back: a down HA bar whose raw high is ≤ haOpen (no upper wick)", () => { + const prev = [bar(100, 101, 99, 100), bar(100, 101, 99, 100)]; + const s = heikinAshiSeries([...prev, bar(100, 101, 99, 100)]); + const haOpenLast = s[s.length - 1].open; + expect(latestHaFlatBack([...prev, bar(haOpenLast, haOpenLast, haOpenLast - 3, haOpenLast - 2)])).toBe("bearish"); + expect(latestHaFlatBack([...prev, bar(haOpenLast, haOpenLast + 0.0001, haOpenLast - 3, haOpenLast - 2)])).toBe("none"); + }); + + it("a doji HA bar (haClose === haOpen) is never a flat-back; an empty window is none", () => { + expect(latestHaFlatBack([bar(100, 100, 100, 100)])).toBe("none"); + expect(latestHaFlatBack([])).toBe("none"); + }); + + it("the seed's influence decays: at the 50-bar kernel floor two different seeds agree on the latest HA bar to <1e-9", () => { + const window: AfiCandle[] = []; + for (let i = 0; i < 50; i++) window.push(bar(100 + i, 101 + i, 99 + i, 100.5 + i)); + const a = heikinAshiSeries(window); + const altered = [bar(50, 60, 40, 55), ...window.slice(1)]; // a wildly different first bar + const b = heikinAshiSeries(altered); + expect(Math.abs(a[49].open - b[49].open)).toBeLessThan(1e-9); + }); + + it("confirmation requires side agreement with the lane's trend law; range never confirms", () => { + expect(haFlatBackConfirmed("bullish", "bullish")).toBe(true); + expect(haFlatBackConfirmed("bearish", "bearish")).toBe(true); + expect(haFlatBackConfirmed("bullish", "bearish")).toBe(false); + expect(haFlatBackConfirmed("bearish", "bullish")).toBe(false); + expect(haFlatBackConfirmed("bullish", "range")).toBe(false); + expect(haFlatBackConfirmed("none", "bullish")).toBe(false); + }); +}); + +describe("computeCandleStructure — both producers over one window", () => { + it("a trending window with a flat-backed continuation bar: confirmed, not broken", () => { + const window: AfiCandle[] = []; + for (let i = 0; i < 60; i++) window.push(bar(100 + i, 101.2 + i, 99.8 + i, 101 + i)); + const ha = heikinAshiSeries(window); + const haOpenNext = (ha[59].open + ha[59].close) / 2; + // Continuation bar with no lower wick relative to the next HA open. + window.push(bar(haOpenNext, haOpenNext + 2, haOpenNext, haOpenNext + 1.5)); + const ema20 = haOpenNext - 5; // the trend's EMA sits below price + const facts = computeCandleStructure(window, ema20, "bullish"); + expect(facts).toEqual({ brokeEmaWithBody: false, haFlatBack: "bullish", haFlatBackConfirmed: true }); + }); + + it("a bullish trend whose last bar closed below EMA20: broken, no confirmation", () => { + const window: AfiCandle[] = []; + for (let i = 0; i < 60; i++) window.push(bar(100 + i, 101.2 + i, 99.8 + i, 101 + i)); + window.push(bar(162, 162.5, 150, 151)); + const facts = computeCandleStructure(window, 155, "bullish"); + expect(facts.brokeEmaWithBody).toBe(true); + expect(facts.haFlatBackConfirmed).toBe(false); + }); +}); diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index dfdb6dda..81038ac0 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,9 +2,9 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.1.0", + "implementationVersion": "2.2.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs. DEM-PRODUCER-PLAN producer contract: when the canonical signal carries an afi.trade-plan.v1 plan, the lane verifies every submitted level against the fetched candle window (all levels within one observed-range width of the window; stop and targets on opposite sides of entry) and emits technical.plan; an unverifiable plan REFUSES the determination (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): technical.plan is absent when the signal carries no plan; technical.plan.rrToFirstTarget is absent when the plan carries no stop or no target.", + "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef and the runtime resolves the registered technical adapter, which computes indicator features (EMA/RSI/ATR) over the candles it fetches for the validated signal. No vendor logic in the node; deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: over the same window, technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane's trend law). No absence beyond the 50-candle floor.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index 529dd8dd..e78e927c 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.2.0", + "implementationVersion": "1.3.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 45cf960d..3dbd1dcc 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.4.0", + "implementationVersion": "1.5.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index da1a4ddc..ef7bec93 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,7 +22,7 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.1.0" + "version": "1.2.0" }, "decayConfig": { "ratio": { @@ -33,6 +33,10 @@ "metadata": { "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", "supersedes": [ + { + "analystConfigHash": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", + "act": "DEM-GOV DEM-PRODUCER-PLAN — mappingRef froggy-trend-pullback 1.1.0 (2026-08-25)" + }, { "analystConfigHash": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index d1b1351d..2a3deeb8 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json new file mode 100644 index 00000000..4171cb38 --- /dev/null +++ b/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json @@ -0,0 +1,123 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.2.0", + "description": "DEM-PRODUCER-CANDLE: the 1.1.0 bindings plus brokeEmaWithBody and haFlatBackConfirmed bound to the technical lane's COMPUTED candle-structure facts (the latest bar's body closed on the counter-trend side of EMA20; the Heikin-Ashi flat-back agreeing with the lane's trend law) — required binds: a window below the 50-bar kernel floor emits no technical payload and the determination refuses (D-DEM-5(2)). The D5-GOV stub and the haFlatBackConfirmed literal are retired.", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + }, + "brokeEmaWithBody": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "brokeEmaWithBody", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "haFlatBackConfirmed": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "haFlatBackConfirmed", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + } + } +} diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index dfdb6dda..81038ac0 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,9 +2,9 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.1.0", + "implementationVersion": "2.2.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its explicit providerInstanceRef, and the runtime provider layer resolves the instance and invokes the trusted registered technical adapter, which computes indicator features (EMA/RSI/ATR-style) over the market candles for the validated signal. The node contains no vendor logic; deterministic given the selected instance's inputs. DEM-PRODUCER-PLAN producer contract: when the canonical signal carries an afi.trade-plan.v1 plan, the lane verifies every submitted level against the fetched candle window (all levels within one observed-range width of the window; stop and targets on opposite sides of entry) and emits technical.plan; an unverifiable plan REFUSES the determination (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): technical.plan is absent when the signal carries no plan; technical.plan.rrToFirstTarget is absent when the plan carries no stop or no target.", + "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef and the runtime resolves the registered technical adapter, which computes indicator features (EMA/RSI/ATR) over the candles it fetches for the validated signal. No vendor logic in the node; deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: over the same window, technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane's trend law). No absence beyond the 50-candle floor.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index 529dd8dd..e78e927c 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.2.0", + "implementationVersion": "1.3.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 45cf960d..3dbd1dcc 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.4.0", + "implementationVersion": "1.5.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index da1a4ddc..ef7bec93 100644 --- a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,7 +22,7 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.1.0" + "version": "1.2.0" }, "decayConfig": { "ratio": { @@ -33,6 +33,10 @@ "metadata": { "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", "supersedes": [ + { + "analystConfigHash": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", + "act": "DEM-GOV DEM-PRODUCER-PLAN — mappingRef froggy-trend-pullback 1.1.0 (2026-08-25)" + }, { "analystConfigHash": "aa8cf5cfd918ae9a80532b735db3236b708ce0cb82778d845b35220627dab479", "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" diff --git a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index d1b1351d..2a3deeb8 100644 --- a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json b/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json new file mode 100644 index 00000000..4171cb38 --- /dev/null +++ b/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.2.0.json @@ -0,0 +1,123 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.2.0", + "description": "DEM-PRODUCER-CANDLE: the 1.1.0 bindings plus brokeEmaWithBody and haFlatBackConfirmed bound to the technical lane's COMPUTED candle-structure facts (the latest bar's body closed on the counter-trend side of EMA20; the Heikin-Ashi flat-back agreeing with the lane's trend law) — required binds: a window below the 50-bar kernel floor emits no technical payload and the determination refuses (D-DEM-5(2)). The D5-GOV stub and the haFlatBackConfirmed literal are retired.", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + }, + "brokeEmaWithBody": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "brokeEmaWithBody", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "haFlatBackConfirmed": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "haFlatBackConfirmed", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + } + } +} diff --git a/test/pipeline/officialArtifactPins.test.ts b/test/pipeline/officialArtifactPins.test.ts index d09dd0de..8389c42f 100644 --- a/test/pipeline/officialArtifactPins.test.ts +++ b/test/pipeline/officialArtifactPins.test.ts @@ -31,8 +31,8 @@ import { FIXTURE_CONFIG_ROOT } from "./support/testHarness.js"; const PINS = { manifestHash: "df3372dadaca1595d0e6d2f6bad9464ccc9abb7106e9f5b7111df148a145bc4f", - analystConfigHash: "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", - pluginSetHash: "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93", + analystConfigHash: "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99", + pluginSetHash: "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471", }; function readJson(path: string): unknown { diff --git a/test/pipeline/registryLoader.test.ts b/test/pipeline/registryLoader.test.ts index 9d848434..e850c189 100644 --- a/test/pipeline/registryLoader.test.ts +++ b/test/pipeline/registryLoader.test.ts @@ -90,10 +90,10 @@ describe("validateRuntimeConfig — positive resolution over the fixture registr "df3372dadaca1595d0e6d2f6bad9464ccc9abb7106e9f5b7111df148a145bc4f" ); expect(froggy.analystConfigHash.value).toBe( - "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22" + "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" ); expect(froggy.pluginSetHash.value).toBe( - "36f911f4fd37eb0d161ab4df7a9650fd1c477bf6dbd71de4a82e0f73c71c4c93" + "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" ); expect(froggy.decay).toEqual({ kind: "ratio", diff --git a/test/providers/fiveLaneAdapters.test.ts b/test/providers/fiveLaneAdapters.test.ts index c6f22aa1..9cb53a1e 100644 --- a/test/providers/fiveLaneAdapters.test.ts +++ b/test/providers/fiveLaneAdapters.test.ts @@ -831,20 +831,29 @@ describe("FLPR-GOV — laneView projections + inertness", () => { } }); - it("viewTechnical pins brokeEmaWithBody to the declared unimplemented stub, preserves scorer-visible renames, and projects unread context", () => { - const { BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB } = require("afi-core/analysts/froggy.enrichment_adapter.js") as { - BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB: false; - }; - expect(BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB).toBe(false); - const view = viewTechnical({ emaDistancePct: 1.2, isInValueSweetSpot: true, rsi14: 55, ema20: 100, ema50: 98, volumeRatio: 1.1, atr14: 42.5, trendBias: "bullish" } as never)!; + it("viewTechnical projects the lane's COMPUTED candle-structure facts (the D5-GOV stub is retired), preserves scorer-visible renames, and projects unread context", () => { + // DEM-PRODUCER-CANDLE: no literal survives on this path — the projection + // carries whatever the technical kernel computed, verbatim. + expect(() => require("afi-core/analysts/froggy.enrichment_adapter.js").BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB).not.toThrow(); + expect( + (require("afi-core/analysts/froggy.enrichment_adapter.js") as Record) + .BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB + ).toBeUndefined(); + const view = viewTechnical({ emaDistancePct: 1.2, isInValueSweetSpot: true, rsi14: 55, ema20: 100, ema50: 98, volumeRatio: 1.1, atr14: 42.5, trendBias: "bullish", brokeEmaWithBody: true, haFlatBack: "bullish", haFlatBackConfirmed: true } as never)!; expect(view).toEqual({ emaDistancePct: 1.2, isInValueSweetSpot: true, - brokeEmaWithBody: BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB, + brokeEmaWithBody: true, + haFlatBack: "bullish", + haFlatBackConfirmed: true, indicators: { rsi: 55, ema_20: 100, ema_50: 98, volume_ratio: 1.1 }, atr14: 42.5, trendBias: "bullish", }); + // The other branch of the computed fact projects just as verbatim. + const flat = viewTechnical({ emaDistancePct: 1.2, isInValueSweetSpot: true, rsi14: 55, ema20: 100, ema50: 98, trendBias: "range", brokeEmaWithBody: false, haFlatBack: "none", haFlatBackConfirmed: false } as never)!; + expect(flat.brokeEmaWithBody).toBe(false); + expect(flat.haFlatBackConfirmed).toBe(false); }); it("viewPattern carries the two scorer-visible candlestick fields plus the unread context fields when present; absent block → undefined", () => { @@ -1010,7 +1019,10 @@ describe("FLPR-GOV — five-lane graph execution", () => { expect(seenView).toBeDefined(); expect(seenView!.enrichmentMeta?.categories).toEqual(["technical", "pattern", "sentiment", "news", "aiMl"]); expect(seenView!.aiMl).toEqual({ convictionScore: 0.85, direction: "long", regime: "bull" }); - expect(seenView!.technical?.brokeEmaWithBody).toBe(false); + // DEM-PRODUCER-CANDLE: a COMPUTED fact now (the deterministic demo window + // is range-biased, so the crossing branch decides it) — never a stub. + expect(typeof seenView!.technical?.brokeEmaWithBody).toBe("boolean"); + expect(typeof seenView!.technical?.haFlatBackConfirmed).toBe("boolean"); const lenses = (seenView as unknown as { lenses: { type: string }[] }).lenses.map((l) => l.type); expect(lenses).toEqual(["technical", "pattern", "sentiment", "news", "aiMl"]); }); From 499ac5d52bcf1fcd41f49f892d3c9e2c4de40d41 Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 25 Aug 2026 19:50:11 -0700 Subject: [PATCH 4/5] DEM-PRODUCER-HTF (reactor): real higher-timeframe bias; structure passes its 0.40 cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEM-GOV §9 DEM-PRODUCER-HTF (owner-authorized 2026-08-25; afi-governance #55; determinations D-1/D-2 in #56) — the mission DIR-GOV D-DIR-3's scope-guard reserved. Merge AFTER afi-core dem/htf-core and afi-config dem/htf-config. Source: - providers/adapters/technicalLocalAdapter.ts: reads the REGISTERED composition value (nodeOverrides.technical.config.htf, boot-validated against the closed paramsSchema) and fetches the signal window + both HTF windows CONCURRENTLY (Promise.all — this lane is the entry node, so serial round-trips delay every downstream wave). Each HTF window runs the SAME EMA trend law; a window below the 50-candle floor emits no sub-block (declared absence). A malformed registered value refuses (NodeConfigurationError), never guesses. - pipeline/nodes/laneView.ts + types/UssLenses.ts: technical.htf projected. - test/support/deterministicPriceFeedAdapter.ts (TEST ONLY): a deterministic per-symbol drift for timeframes >= 1d, so the fixture corpus exercises aligned, conflicting and neutral HTF. Sub-daily timeframes byte-untouched. Goldens (regenerated ONCE, itemized in INTENTIONAL_DIFFS.md): THE GATE IS MET — the aligned-HTF +0.4 structure term fires for the first time, so structure reaches 0.8 on the four BTC goldens, BEYOND its former 0.40 cap, and spans {0.15, 0.4, 0.8} over the corpus. The ETH pair exercises a real higher-timeframe CONFLICT (weekly bearish, daily bullish): the analyst's verdict is "unknown" — the branch DIR-GOV reserved, now reachable, resolved in afi-core, and pinned by a golden. inputHash/manifestHash byte-equal everywhere. Tests: htfProducer.test.ts — the producer (registered timeframes actually drive the fetches; concurrency proven by an in-flight peak of 3; kernel-floor absence; malformed-config refusal) and THE DIR-GOV D-DIR-3 NEGATIVE TEST the gate requires: varying facts.direction over long/short/neutral leaves the entire technical payload byte-identical, the bias facts contradict the submitted side where the market does, and a comment-stripped source scan proves no producer or projection file reads the submitted direction (with the scan proven to bite, and the lawful record-stamping reader shown to be outside the scanned set). 785/785; typecheck green; all four compiled-build proofs green against a real replica set (503-unavailable, 10 persistence, 7 oracle-equivalence, SIGTERM). No scoring-law value moves. Co-Authored-By: Claude Fable 5 --- src/pipeline/nodes/laneView.ts | 4 + .../adapters/technicalLocalAdapter.ts | 78 +++++++- src/types/UssLenses.ts | 19 ++ .../mappingPerStrategyResolution.test.ts | 20 +- test/oracle/INTENTIONAL_DIFFS.md | 44 +++++ .../afi-analysis-technical--2.0.0.json | 35 +++- .../afi-merge-enriched-view--1.1.0.json | 2 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 17 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.3.0.json | 159 +++++++++++++++ .../cpj-blofin-perp-long.builtin.json | 80 +++++--- .../cpj-blofin-perp-long.registry.json | 80 +++++--- .../cpj-blofin-perp-neutral.builtin.json | 50 ++++- .../cpj-blofin-perp-neutral.registry.json | 50 ++++- .../cpj-coinbase-spot-sell.builtin.json | 58 ++++-- .../cpj-coinbase-spot-sell.registry.json | 58 ++++-- .../goldens/enriched/tv-long.builtin.json | 80 +++++--- .../goldens/enriched/tv-long.registry.json | 80 +++++--- .../goldens/enriched/tv-neutral.builtin.json | 50 ++++- .../goldens/enriched/tv-neutral.registry.json | 50 ++++- .../goldens/enriched/tv-short.builtin.json | 58 ++++-- .../goldens/enriched/tv-short.registry.json | 58 ++++-- .../afi-analysis-technical--2.0.0.json | 35 +++- .../afi-merge-enriched-view--1.1.0.json | 2 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 17 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.3.0.json | 159 +++++++++++++++ .../afi-analysis-technical--2.0.0.json | 35 +++- .../afi-merge-enriched-view--1.1.0.json | 2 +- ...i-scorer-froggy-trend-pullback--1.0.0.json | 2 +- ...oggy--trend_pullback_v1--1.0.0.config.json | 17 +- .../froggy--trend_pullback_v1--1.0.0.json | 2 +- .../froggy-trend-pullback--1.3.0.json | 159 +++++++++++++++ test/pipeline/htfProducer.test.ts | 184 ++++++++++++++++++ test/pipeline/officialArtifactPins.test.ts | 4 +- test/pipeline/registryLoader.test.ts | 4 +- test/support/deterministicPriceFeedAdapter.ts | 25 ++- 39 files changed, 1570 insertions(+), 215 deletions(-) create mode 100644 test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json create mode 100644 test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json create mode 100644 test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json create mode 100644 test/pipeline/htfProducer.test.ts diff --git a/src/pipeline/nodes/laneView.ts b/src/pipeline/nodes/laneView.ts index 51ed2b16..f11a8bf1 100644 --- a/src/pipeline/nodes/laneView.ts +++ b/src/pipeline/nodes/laneView.ts @@ -90,6 +90,10 @@ export function viewTechnical( // verbatim so the registered mapping can bind them (technical.plan.*); // omitted (not null) when the lane emitted none. ...(payload.plan !== undefined ? { plan: payload.plan } : {}), + // DEM-PRODUCER-HTF: the lane's higher-timeframe trend facts, projected + // verbatim for the registered mapping to recode into the rubric's bias + // vocabulary; omitted (not null) when the lane emitted none. + ...(payload.htf !== undefined ? { htf: payload.htf } : {}), }; } diff --git a/src/providers/adapters/technicalLocalAdapter.ts b/src/providers/adapters/technicalLocalAdapter.ts index d2941749..a33ff487 100644 --- a/src/providers/adapters/technicalLocalAdapter.ts +++ b/src/providers/adapters/technicalLocalAdapter.ts @@ -20,6 +20,58 @@ import { NodeConfigurationError } from "../../pipeline/nodeSdk.js"; import type { CategoryResult, ProviderAdapter, ProviderAdapterContext } from "../types.js"; import { TradePlanVerificationError, verifyTradePlan } from "../../enrichment/tradePlanVerification.js"; import type { TradePlanV1 } from "../../types/TradePlan.js"; +import type { TechnicalLensV1 } from "../../types/UssLenses.js"; + +/** The registered higher-timeframe selection, or undefined when unregistered. */ +interface HtfSelection { + dailyTimeframe: string; + weeklyTimeframe: string; + htfCandleLimit: number; +} + +function readHtfConfig(raw: unknown): HtfSelection | undefined { + if (raw === undefined || raw === null) return undefined; + const cfg = raw as Record; + const daily = cfg["dailyTimeframe"]; + const weekly = cfg["weeklyTimeframe"]; + if (typeof daily !== "string" || typeof weekly !== "string") { + // Boot validation against the plugin's closed paramsSchema makes this + // unreachable in a composed pipeline; fail closed rather than guess. + throw new NodeConfigurationError( + "technical adapter: params.htf must declare dailyTimeframe and weeklyTimeframe (DEM-PRODUCER-HTF)" + ); + } + const limit = cfg["htfCandleLimit"]; + return { + dailyTimeframe: daily, + weeklyTimeframe: weekly, + htfCandleLimit: typeof limit === "number" ? limit : 100, + }; +} + +/** + * The higher-timeframe bias fact: the SAME EMA20/EMA50 trend law the lane + * already applies to the signal's own window, run over a higher-timeframe + * window. Returns undefined when the window cannot be computed (below the + * 50-candle kernel floor) — a declared producer absence, never a guess. + */ +function htfBias( + computeTechnical: TechnicalLocalAdapterDeps["computeTechnical"], + raw: OHLCVCandle[] | undefined, + timeframe: string +): { timeframe: string; trendBias: "bullish" | "bearish" | "range"; ema20: number; ema50: number; barCount: number } | undefined { + if (!raw || raw.length === 0) return undefined; + const candles = toAfiCandles(raw); + const computed = computeTechnical(candles); + if (!computed) return undefined; + return { + timeframe, + trendBias: computed.trendBias, + ema20: computed.ema20, + ema50: computed.ema50, + barCount: candles.length, + }; +} export interface TechnicalLocalAdapterDeps { resolvePriceSource: typeof getDefaultPriceSource; @@ -77,11 +129,35 @@ export function createTechnicalLocalAdapter(deps?: TechnicalLocalAdapterDeps): P const limitRaw = ctx.config["candleLimit"]; const limit = typeof limitRaw === "number" ? limitRaw : 100; + // DEM-PRODUCER-HTF: the higher-timeframe selection is a REGISTERED + // COMPOSITION VALUE (the analyst config's nodeOverrides.technical.config + // .htf, validated at boot against the plugin's paramsSchema) — never a + // code constant, and never derived from a submitted field. + const htfConfig = readHtfConfig(ctx.config["htf"]); + const feed = d.getAdapter(priceSource as Parameters[0]); - const rawCandles = await feed.getOHLCV({ symbol, timeframe, limit }); + // The signal window and both higher-timeframe windows are fetched + // CONCURRENTLY: this lane is the pipeline's entry node, so serial + // round-trips here delay every downstream wave (platform-floor rule). + const [rawCandles, rawDaily, rawWeekly] = await Promise.all([ + feed.getOHLCV({ symbol, timeframe, limit }), + htfConfig ? feed.getOHLCV({ symbol, timeframe: htfConfig.dailyTimeframe, limit: htfConfig.htfCandleLimit }) : Promise.resolve(undefined), + htfConfig ? feed.getOHLCV({ symbol, timeframe: htfConfig.weeklyTimeframe, limit: htfConfig.htfCandleLimit }) : Promise.resolve(undefined), + ]); const candles = toAfiCandles(rawCandles); const technical = d.computeTechnical(candles); + if (htfConfig && technical) { + const htf: NonNullable = {}; + const daily = htfBias(d.computeTechnical, rawDaily, htfConfig.dailyTimeframe); + const weekly = htfBias(d.computeTechnical, rawWeekly, htfConfig.weeklyTimeframe); + // A window below the kernel floor emits NO sub-block — a declared + // producer absence (D-DEM-5(4)(b)), never a guessed bias. + if (daily) htf.daily = daily; + if (weekly) htf.weekly = weekly; + technical.htf = htf; + } + // DEM-PRODUCER-PLAN (D-DEM-5(6)): a submitted trade plan is verified // against THIS fetched window by THIS producer before any of its // levels becomes a fact; an unverifiable plan refuses the determination diff --git a/src/types/UssLenses.ts b/src/types/UssLenses.ts index fa90e663..067d2698 100644 --- a/src/types/UssLenses.ts +++ b/src/types/UssLenses.ts @@ -11,6 +11,15 @@ */ import type { TechnicalPlanFacts } from "./TradePlan.js"; +/** One higher-timeframe window's trend fact (DEM-PRODUCER-HTF). */ +export interface HtfBiasFact { + timeframe: string; + trendBias: "bullish" | "bearish" | "range"; + ema20: number; + ema50: number; + barCount: number; +} + /** * Base lens interface. * All lenses must have a type and version for forward compatibility. @@ -78,6 +87,16 @@ export interface TechnicalLensV1 extends UssLens { haFlatBack?: "bullish" | "bearish" | "none"; /** The flat-back agrees with the lane's trend law; range → false. */ haFlatBackConfirmed?: boolean; + /** + * DEM-PRODUCER-HTF: higher-timeframe bias facts, computed by the same EMA + * law over the separately fetched windows the registered composition + * value selects. A sub-block is absent when its window is below the + * kernel floor (declared producer absence). + */ + htf?: { + daily?: HtfBiasFact; + weekly?: HtfBiasFact; + }; }; } diff --git a/test/guardrails/mappingPerStrategyResolution.test.ts b/test/guardrails/mappingPerStrategyResolution.test.ts index ed3a4e22..02eaba21 100644 --- a/test/guardrails/mappingPerStrategyResolution.test.ts +++ b/test/guardrails/mappingPerStrategyResolution.test.ts @@ -265,6 +265,12 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are brokeEmaWithBody: false, haFlatBack: "none", haFlatBackConfirmed: false, + // DEM-PRODUCER-HTF: the lane's higher-timeframe trend facts, which + // the registered mapping recodes into the rubric's bias vocabulary. + htf: { + daily: { timeframe: "1d", trendBias: "bullish", ema20: 101, ema50: 100, barCount: 100 }, + weekly: { timeframe: "1w", trendBias: "bullish", ema20: 102, ema50: 100, barCount: 100 }, + }, }, pattern: { patternName: "bull flag", patternConfidence: 80 }, sentiment: { score: 0.4, tags: ["liquidity sweep"] }, @@ -307,6 +313,7 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are it("a plan-less view fires ONLY the rrMultiplePlanned floor default — recorded, never silent (D-DEM-5(3); §9 determination D-5)", async () => { const result = await scorerFroggyTrendPullbackNode.run(view(), ctx(true)); expect(result.degradations).toEqual([{ class: "declared-default-fired", detail: "rrMultiplePlanned" }]); + // The HTF recodes did NOT fire: this view carries the lane's bias facts. const a = result.output as { analysis: { analystScore: { uwrAxes: { risk: number } } } }; // The rubric floor (rr 1 → 0.2) — a hash-committed, recorded degradation. expect(a.analysis.analystScore.uwrAxes.risk).toBe(0.2); @@ -324,20 +331,19 @@ describe("the seam: fragment+residual equals the legacy path; fired defaults are }; const result = await scorerFroggyTrendPullbackNode.run(bare, ctx(true)); const classes = (result.degradations ?? []).map((d) => d.class); - expect(classes).toEqual([ - "declared-default-fired", - "declared-default-fired", - "declared-default-fired", - "declared-default-fired", - "declared-default-fired", - ]); + expect(classes).toEqual(Array(7).fill("declared-default-fired")); const targets = (result.degradations ?? []).map((d) => d.detail).sort(); expect(targets).toEqual([ "atrRegime", + // DEM-PRODUCER-HTF: no htf block on this bare view, so both recodes + // fire their declared `absent` member — a RECORDED absence, exactly + // like every other declared default. + "dailyBias", "distanceFromDailyEmaPct", "pulledBackIntoSweetSpot", "rrMultiplePlanned", "triggerPatternQuality", + "weeklyBias", ]); // The composition reference scores the same INPUT VALUES (the // grandfather exists so DEM-BIND reproduces today's scored values diff --git a/test/oracle/INTENTIONAL_DIFFS.md b/test/oracle/INTENTIONAL_DIFFS.md index 0684a02e..8dc74029 100644 --- a/test/oracle/INTENTIONAL_DIFFS.md +++ b/test/oracle/INTENTIONAL_DIFFS.md @@ -633,3 +633,47 @@ On the SOL 15m seed the latest demo bar's body crosses EMA20 in a `range` regime ## D-DEM-5(5) residual after this slot The structure axis is no longer capped at `0.40`, but its two live terms are the sweet-spot credit and the EMA-break term; the **HTF-alignment `+0.4` term and the HA `+0.2` term remain unreachable** — the first because `weeklyBias`/`dailyBias` are still the literal `"neutral"` (that is `DEM-PRODUCER-HTF`), the second because a `range` regime cannot confirm a flat-back and the demo corpus is entirely `range`. Recorded, not hidden. + +--- + +# DEM-PRODUCER-HTF reconciliation — real higher-timeframe bias; the structure axis passes its former 0.40 cap (DEM-GOV §9, owner-authorized 2026-08-25) + +**Authority.** DEM-GOV §9 `DEM-PRODUCER-HTF` — the mission DIR-GOV D-DIR-3's scope-guard reserved — with §9 determinations D-1 (the HTF composition value and the additive `paramsSchema.htf` within plugin identity) and D-2 (adapter identity held). D-DEM-7(2) authorizes the movement in class; this section is the D-DEM-7(3) itemization. Baseline for every row is the **`DEM-PRODUCER-CANDLE` state**. + +**What changed on the path.** The technical adapter reads the **registered composition value** (`nodeOverrides.technical.config.htf` = `{1d, 1w, 100}`, boot-validated against the plugin's closed `paramsSchema`) and fetches the signal window plus both higher-timeframe windows **concurrently** (`Promise.all` — this lane is the pipeline's entry node). Each HTF window is run through the SAME EMA20/EMA50 trend law and emitted as `technical.htf.{daily,weekly}` = `{timeframe, trendBias, ema20, ema50, barCount}`; a window below the 50-candle kernel floor emits **no sub-block** (declared producer absence). `viewTechnical` projects the block; mapping **1.3.0** recodes `weeklyBias`/`dailyBias` from the lane's trend vocabulary into the rubric's bias vocabulary (`bullish→long`, `bearish→short`, `range→neutral`; `absent→neutral`, a recorded default). afi-core deletes the two `"neutral" as const` literals and the residual shrinks to **`liquiditySwept` alone** — the D-DEM-4(2) inventory is empty. The rubric's `"unknown"`-direction branch semantics are resolved in-slot (see below). `implementationVersion`: technical `2.2.0 → 2.3.0`, merge `1.3.0 → 1.4.0`, scorer `1.5.0 → 1.6.0`; `pluginSetHash` `5eef1faf… → 59ff5437…`; `analystConfigHash` `300783e4… → 71de40d2…`. + +**Test-only fixture change.** `test/support/deterministicPriceFeedAdapter.ts` gains a deterministic per-symbol drift applied **only to timeframes ≥ 1d** (BTC up on both; ETH up daily / down weekly; everything else flat). Timeframes below 1d are byte-untouched, so every golden's own-timeframe series, indicators and pattern facts are unchanged — verified by the differ (no `lenses/0/payload/ema20` movement). + +## Per-golden scored-value movement — **the structure axis passes its former 0.40 cap** + +| golden | `weeklyBias` | `dailyBias` | `uwrAxes.structure` | `uwrScore` = `conviction` | `analystScore.direction` | +|---|---|---|---|---|---| +| `cpj-blofin-perp-long.{builtin,registry}` (BTC) | neutral → **long** | neutral → **long** | 0.4 → **0.8** | 0.49167 → 0.59167 | neutral → **long** | +| `tv-long.{builtin,registry}` (BTC) | neutral → **long** | neutral → **long** | 0.4 → **0.8** | 0.41667 → 0.51667 | neutral → **long** | +| `cpj-coinbase-spot-sell.{builtin,registry}` (ETH) | neutral → **short** | neutral → **long** | 0.4 (unmoved) | unmoved | neutral → **unknown** | +| `tv-short.{builtin,registry}` (ETH) | neutral → **short** | neutral → **long** | 0.4 (unmoved) | unmoved | neutral → **unknown** | +| `cpj-blofin-perp-neutral.{builtin,registry}` (SOL) | neutral → neutral | neutral → neutral | 0.4 (unmoved) | unmoved | neutral (unmoved) | +| `tv-neutral.{builtin,registry}` (SOL) | neutral → neutral | neutral → neutral | 0.15 (unmoved) | unmoved | neutral (unmoved) | + +**The gate is met.** The aligned-HTF `+0.4` structure term fires for the first time in the system's history: structure reaches **0.8** on the four BTC goldens — **beyond its former `0.40` cap** — and now spans `{0.15, 0.4, 0.8}` over the corpus. The ETH pair exercises a genuine higher-timeframe **conflict** (weekly bearish, daily bullish): the aligned term does not fire, and the analyst's verdict is `"unknown"` — the branch DIR-GOV's scope-guard reserved, now reachable, resolved, and pinned by a golden. `riskBucket` unmoved (it reads `atrRegime`). `rationale`/`axisNotes` move only where the structure note crosses its threshold. + +## Per-golden identity / payload movement + +| JSON path class | goldens | why | +|---|---|---| +| `…/lenses/0/payload/htf`, `…/_priceFeedMetadata/technicalIndicators/htf` (absent → object) | all 12 | the lane's HTF facts join the technical payload | +| `/scorerInput/{weeklyBias,dailyBias}` | 8 (BTC ×4, ETH ×4) | the recoded facts differ from the retired literal | +| scored-value class (`uwrAxes.structure`, `uwrScore`, `conviction`, `analystScore.direction`, `rationale`, both `outputHash` copies) | 8 | the axes/verdict moved on those seeds | +| `/evidenceRecord/composition/enrichmentHash/value` | all 12 | the technical lens payload gained `htf` | +| `/evidenceRecord/providerInvocations/4/{providerResultHash,categoryResultHash}/value` | all 12 | the validated technical CategoryResult gained `htf` | +| `/evidenceRecord/providerInvocations/4/invocationInputHash/value` | all 12 | the technical node's merged params gained the registered `htf` value | +| `/evidenceRecord/composition/{analystConfigHash,pluginSetHash}/value` | all 12 | mappingRef 1.3.0 + `nodeOverrides`; three implementationVersion moves | +| `/evidenceRecord/{recordHash,replayHash}/value` | all 12 | consequences | + +## Explicitly byte-EQUAL + +`manifestHash` (the pipeline manifest is untouched — the composition value rides `nodeOverrides`, §8 honored); `inputHash`/`ingestHash`/`canonicalUss` on all 12 (no ingest surface is touched); `executionSummaryHash` **except** where an HTF recode legitimately fires its `absent` member; the own-timeframe technical indicators (`ema20`, `ema50`, `rsi14`, `atr14`, `emaDistancePct`, `isInValueSweetSpot`, `atrRegime`, `brokeEmaWithBody`, `haFlatBack*`) on all 12 — the drift is confined to ≥ 1d timeframes; the pattern/sentiment/news/aiMl lens payloads and proofs; `execution`, `insight` and `risk` axes; `riskBucket`; `scoredSignal.direction`/`meta.direction` (the SUBMITTED side — unchanged and unread by the producer, DIR-GOV D-DIR-3); the `312da118…126e06` provenance golden; `ATLAS_MANIFEST_HASH`. **No scoring-law value moved.** + +## D-DEM-5(5) residual after the four slots + +Every scorer input is now a computed fact of a registered producer (D-DEM-5(1)); the residual is `liquiditySwept` alone, whose disposition D-DEM-3(5) expressly reserves. What remains, recorded honestly: (1) **risk on plan-less submissions** — the TradingView/MarkitTick routes carry no trade plan, so risk there is the declared floor moved only by `brokeEmaWithBody`; closing it needs a plan carrier on those routes (a separate authorization). (2) **The HA flat-back term** (+0.2) is unreachable in a `range` regime by construction of the rubric — a formula question for a modelling filing, not a placeholder. (3) **R:R is verified-plausible but submitter-chosen**: a stop-placement validity rule would be a new threshold (§8), so it belongs to a modelling filing. (4) **Symbols with fewer than 50 weekly bars** on the registered feed (listings under ~1 year) score `weeklyBias = neutral` permanently — a declared absence, not a defect, but it caps their structure at 0.6. diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index 81038ac0..35293353 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,9 +2,9 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.2.0", + "implementationVersion": "2.3.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef and the runtime resolves the registered technical adapter, which computes indicator features (EMA/RSI/ATR) over the candles it fetches for the validated signal. No vendor logic in the node; deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: over the same window, technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane's trend law). No absence beyond the 50-candle floor.", + "description": "Official technical lane manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef; the runtime resolves the registered adapter, which computes EMA/RSI/ATR features over the candles it fetches. Deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane trend law). HTF: with params.htf set, the declared daily/weekly windows are also fetched and technical.htf.{daily,weekly}.trendBias emitted by the same law; a window under the 50-candle floor emits no sub-block.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, @@ -30,6 +30,37 @@ "minimum": 1, "maximum": 1000, "description": "Number of candles fetched from the price feed for indicator computation." + }, + "htf": { + "type": "object", + "additionalProperties": false, + "required": [ + "dailyTimeframe", + "weeklyTimeframe" + ], + "description": "OPTIONAL higher-timeframe selection (DEM-GOV §9 DEM-PRODUCER-HTF, owner determination D-1: an additive optional parameter within the identity of afi-analysis-technical@2.0.0). Absent means the lane fetches only the signal's own timeframe, exactly as before — every existing valid configuration remains valid. Closed vocabularies: the runtime fails closed at boot on anything else.", + "properties": { + "dailyTimeframe": { + "type": "string", + "enum": [ + "1d" + ], + "description": "The daily-bias window's timeframe token." + }, + "weeklyTimeframe": { + "type": "string", + "enum": [ + "1w" + ], + "description": "The weekly-bias window's timeframe token." + }, + "htfCandleLimit": { + "type": "integer", + "minimum": 50, + "maximum": 1000, + "description": "Candles fetched per higher-timeframe window (at or above the 50-candle kernel floor). Default 100." + } + } } } }, diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index e78e927c..32a0450e 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.3.0", + "implementationVersion": "1.4.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", diff --git a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 3dbd1dcc..40e59fdf 100644 --- a/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.5.0", + "implementationVersion": "1.6.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index ef7bec93..111bdef5 100644 --- a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,7 +22,7 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.2.0" + "version": "1.3.0" }, "decayConfig": { "ratio": { @@ -33,6 +33,10 @@ "metadata": { "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", "supersedes": [ + { + "analystConfigHash": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99", + "act": "DEM-GOV DEM-PRODUCER-CANDLE — mappingRef froggy-trend-pullback 1.2.0 (2026-08-25)" + }, { "analystConfigHash": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", "act": "DEM-GOV DEM-PRODUCER-PLAN — mappingRef froggy-trend-pullback 1.1.0 (2026-08-25)" @@ -42,5 +46,16 @@ "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" } ] + }, + "nodeOverrides": { + "technical": { + "config": { + "htf": { + "dailyTimeframe": "1d", + "weeklyTimeframe": "1w", + "htfCandleLimit": 100 + } + } + } } } diff --git a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index 22a5cd14..f8394faf 100644 --- a/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/oracle/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json b/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json new file mode 100644 index 00000000..428e6a9d --- /dev/null +++ b/test/oracle/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json @@ -0,0 +1,159 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.3.0", + "description": "DEM-PRODUCER-HTF: the 1.2.0 bindings plus weeklyBias and dailyBias recoded from the technical lane's higher-timeframe trend facts (technical.htf.weekly.trendBias / technical.htf.daily.trendBias), computed by the same EMA law over separately fetched windows whose timeframes are a registered composition value. The vocabulary translation lives here, not in the producer (D-DEM-3). A window below the kernel floor is a declared producer absence and fires the recode's absent member.", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + }, + "brokeEmaWithBody": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "brokeEmaWithBody", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "haFlatBackConfirmed": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "haFlatBackConfirmed", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "weeklyBias": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "htf.weekly.trendBias", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "table": { + "bullish": "long", + "bearish": "short", + "range": "neutral" + }, + "fallback": "neutral", + "absent": "neutral" + }, + "dailyBias": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "htf.daily.trendBias", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "table": { + "bullish": "long", + "bearish": "short", + "range": "neutral" + }, + "fallback": "neutral", + "absent": "neutral" + } + } +} diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json b/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json index 0b563ec9..12cbc8cd 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-long.builtin.json @@ -3,8 +3,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.4916666666666667, - "direction": "neutral", + "conviction": 0.5916666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -23,9 +23,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.5, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.4916666666666667 + "uwrScore": 0.5916666666666667 }, "canonicalUss": { "facts": { @@ -80,13 +80,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "82ba2acc2241f75e1df20c9dc8b3561de335278f2f360ec4e934c80fe01a42c6" + "value": "f9a0044599f0a5768a0930b548e5161b5092a7ee140ea71edc593e17daeb84d2" }, "executionSummaryHash": { "algorithm": "sha256", @@ -106,7 +106,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -130,7 +130,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" + "value": "8d327b6e500ae48e37335a1e5aa5a6a33fe5f27be167d0db34b1988f2b4b3824" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -188,7 +188,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "e4786a2a9dd4d3e1901b7722f89e3736bfbb2cb26478ef494f9e4a84327757ef" + "value": "f3be7acb06a7ba5138e7d2d891a08b0570a7878464b24f5d6ce7165d3c2fc108" }, "provider": { "deterministic": false, @@ -396,7 +396,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "f46d7dcba4876cc7014641feede2ba53b4b3670a4c9b51d4ea39def36ccf9d15" + "value": "bd530b51080fbf7faf8e09279385745863218e15db713bab7b4b96562a796675" }, "credential": { "mode": "keyless" @@ -405,7 +405,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "d26929d750140bfee0eca63136a490c4c3be3a93fcbb7bec8d1182be0b0eed7c" + "value": "469985ca9c5226ff4feee1e7771d8dc91d7b4dbb2d56c78845fb093426d70574" }, "priceSource": "demo", "provider": { @@ -434,7 +434,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "bcc698ee584268726060d93e5cb162a6348c115a2454a2aaf9a005b6f81e8bc9" + "value": "3bcf578b51ce891302fe3a0b20e072e0a23bf60b84b1c874a86192f252176081" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -445,19 +445,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "664daadb030d1ebe06d034b69ca9686867c7a23729239cb4026c031379c6351f" + "value": "a636ebc77e30e930eeb76dc35e36e07b0d6dc7dd6d8b6173ab7f07c42001ba77" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "16a659a94892e34ddafae4492feb7b74176eeedbab6c5cedd3f2a5f27d250cfd" + "value": "52d16215c01f1f63f4affdcdd12b0624008c1aefa38ce06d263a9a87ed4f112c" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.4916666666666667, + "conviction": 0.5916666666666667, "direction": "long", "provenanceRecordRef": "provenance-record:cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "providerId": "oracle-telegram-channel-1", @@ -470,9 +470,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.5, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.4916666666666667 + "uwrScore": 0.5916666666666667 }, "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "strategyId": "trend_pullback_v1", @@ -530,6 +530,22 @@ "emaDistancePct": 0.6212694575958345, "haFlatBack": "bullish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56706.08647879475, + "ema50": 55588.2774381915, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56556.523500773656, + "ema50": 55499.21282763963, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -553,8 +569,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.4916666666666667, - "direction": "neutral", + "conviction": 0.5916666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -573,9 +589,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.5, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.4916666666666667 + "uwrScore": 0.5916666666666667 }, "decayParams": { "barsPerHalfLife": 12, @@ -596,6 +612,22 @@ "emaDistancePct": 0.6212694575958345, "haFlatBack": "bullish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56706.08647879475, + "ema50": 55588.2774381915, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56556.523500773656, + "ema50": 55499.21282763963, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -795,19 +827,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" + "value": "8d327b6e500ae48e37335a1e5aa5a6a33fe5f27be167d0db34b1988f2b4b3824" }, "scorerInput": { "atrRegime": "normal", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": 0.6212694575958345, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1.4286, "triggerPatternQuality": 2, - "weeklyBias": "neutral" + "weeklyBias": "long" }, "uwrMode": "builtin", "uwrResolvedSource": "builtin" diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json b/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json index a8a98070..ff9df610 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-long.registry.json @@ -3,8 +3,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.4916666666666667, - "direction": "neutral", + "conviction": 0.5916666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -23,9 +23,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.5, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.4916666666666667 + "uwrScore": 0.5916666666666667 }, "canonicalUss": { "facts": { @@ -80,13 +80,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "82ba2acc2241f75e1df20c9dc8b3561de335278f2f360ec4e934c80fe01a42c6" + "value": "f9a0044599f0a5768a0930b548e5161b5092a7ee140ea71edc593e17daeb84d2" }, "executionSummaryHash": { "algorithm": "sha256", @@ -106,7 +106,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -130,7 +130,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" + "value": "8d327b6e500ae48e37335a1e5aa5a6a33fe5f27be167d0db34b1988f2b4b3824" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -188,7 +188,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "e4786a2a9dd4d3e1901b7722f89e3736bfbb2cb26478ef494f9e4a84327757ef" + "value": "f3be7acb06a7ba5138e7d2d891a08b0570a7878464b24f5d6ce7165d3c2fc108" }, "provider": { "deterministic": false, @@ -396,7 +396,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "f46d7dcba4876cc7014641feede2ba53b4b3670a4c9b51d4ea39def36ccf9d15" + "value": "bd530b51080fbf7faf8e09279385745863218e15db713bab7b4b96562a796675" }, "credential": { "mode": "keyless" @@ -405,7 +405,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "d26929d750140bfee0eca63136a490c4c3be3a93fcbb7bec8d1182be0b0eed7c" + "value": "469985ca9c5226ff4feee1e7771d8dc91d7b4dbb2d56c78845fb093426d70574" }, "priceSource": "demo", "provider": { @@ -434,7 +434,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "bcc698ee584268726060d93e5cb162a6348c115a2454a2aaf9a005b6f81e8bc9" + "value": "3bcf578b51ce891302fe3a0b20e072e0a23bf60b84b1c874a86192f252176081" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -445,19 +445,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "7a1bfbd7c17688eb19ffda93734b6538b8e156502a006e0a56a3ecc2ad6db123" + "value": "0f3fb284c0f27476dbc7afbe4de8436e722b20a0c703094d002cd9b39e255267" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "fdd98d2074a66bb52e880d82d3040c0678593639d36d60bc957215e3048259b0" + "value": "18ff0cc78e62c133d25c5c582fa9a4349a2286b50df1bb922b9b8ff45bff3f6b" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.4916666666666667, + "conviction": 0.5916666666666667, "direction": "long", "provenanceRecordRef": "provenance-record:cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "providerId": "oracle-telegram-channel-1", @@ -470,9 +470,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.5, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.4916666666666667 + "uwrScore": 0.5916666666666667 }, "signalId": "cpj-telegram-oracle-telegram-channel-1-oracle-cpj-msg-0001", "strategyId": "trend_pullback_v1", @@ -530,6 +530,22 @@ "emaDistancePct": 0.6212694575958345, "haFlatBack": "bullish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56706.08647879475, + "ema50": 55588.2774381915, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56556.523500773656, + "ema50": 55499.21282763963, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -553,8 +569,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.4916666666666667, - "direction": "neutral", + "conviction": 0.5916666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -573,9 +589,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.5, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.4916666666666667 + "uwrScore": 0.5916666666666667 }, "decayParams": { "barsPerHalfLife": 12, @@ -596,6 +612,22 @@ "emaDistancePct": 0.6212694575958345, "haFlatBack": "bullish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56706.08647879475, + "ema50": 55588.2774381915, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56556.523500773656, + "ema50": 55499.21282763963, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -795,19 +827,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7d962cc8629632253a8e3f8f236ea1983a5c87d37cf10d26bcd12717ddd627da" + "value": "8d327b6e500ae48e37335a1e5aa5a6a33fe5f27be167d0db34b1988f2b4b3824" }, "scorerInput": { "atrRegime": "normal", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": 0.6212694575958345, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1.4286, "triggerPatternQuality": 2, - "weeklyBias": "neutral" + "weeklyBias": "long" }, "uwrMode": "registry", "uwrResolvedSource": "registry" diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json index c6547f75..6a0a504f 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.builtin.json @@ -70,13 +70,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "32e190db3f5a539ef718b3dfbfad0949236ae4e3fdfdc70c6cb9dde1ab0bad13" + "value": "d6b3a3449a849669599b075415af22848a62f9b0423f89b13774575f5c0ec988" }, "executionSummaryHash": { "algorithm": "sha256", @@ -96,7 +96,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -178,7 +178,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "27f4e9cf35f58cb89e06c2fb5b6f9d7e527748c919617b745ed28b2ef2da40a3" + "value": "bd6df49372cfc7329703be77bd0e0d34692ad7d1593f992c3782146038ef8a2d" }, "provider": { "deterministic": false, @@ -386,7 +386,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "f3dbeede7f9fec7ef2e7b718dde0427a15c3e095e91fad3f58ebaeba3cea5e2e" + "value": "e9b8e4acaddca12e2f97eadeca54c73f2a80a30583562267be44ff7bdd260988" }, "credential": { "mode": "keyless" @@ -395,7 +395,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "31e0eb0c56634c4ec53de88d3c7972a9b1511493362e009676e6a4f333522ff2" + "value": "544d16d4ef3849a5191036094b3c00f92d7680e026227ddfbe806e775ca14a29" }, "priceSource": "demo", "provider": { @@ -424,7 +424,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "d5702421d230b0143a3448026367a4cef6078abb02fc2de104c1ce8bab85b1c2" + "value": "b66a8e99a4e3075e6abfa052db7346c2b6bd2cbac752cf85cb2a58625679a3d1" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -435,14 +435,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "86faf6d12c3de1340b71b50550620fefcf2b7f7257f0f96376b3292c3d610004" + "value": "00abf98d3544f4bdb0d92aaed3e3419c8692749a9d67098f9ab46cd26d61cded" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "a48e72b91affc90e1f16c2fe19acb280ec866db9017107aad64fd9f1fb8cbfe0" + "value": "1165ab0fc112d74c8913a633563677099997f8388789a84da119938de32e9ab2" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -508,6 +508,22 @@ "emaDistancePct": 0.2503722265458551, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.11164929010108, + "ema50": 99.99258680396032, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.98779835286723, + "ema50": 99.96138735735582, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", @@ -567,6 +583,22 @@ "emaDistancePct": 0.2503722265458551, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.11164929010108, + "ema50": 99.99258680396032, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.98779835286723, + "ema50": 99.96138735735582, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json index ae1b4ae2..65cd102c 100644 --- a/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json +++ b/test/oracle/goldens/enriched/cpj-blofin-perp-neutral.registry.json @@ -70,13 +70,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "32e190db3f5a539ef718b3dfbfad0949236ae4e3fdfdc70c6cb9dde1ab0bad13" + "value": "d6b3a3449a849669599b075415af22848a62f9b0423f89b13774575f5c0ec988" }, "executionSummaryHash": { "algorithm": "sha256", @@ -96,7 +96,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -178,7 +178,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "27f4e9cf35f58cb89e06c2fb5b6f9d7e527748c919617b745ed28b2ef2da40a3" + "value": "bd6df49372cfc7329703be77bd0e0d34692ad7d1593f992c3782146038ef8a2d" }, "provider": { "deterministic": false, @@ -386,7 +386,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "f3dbeede7f9fec7ef2e7b718dde0427a15c3e095e91fad3f58ebaeba3cea5e2e" + "value": "e9b8e4acaddca12e2f97eadeca54c73f2a80a30583562267be44ff7bdd260988" }, "credential": { "mode": "keyless" @@ -395,7 +395,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "31e0eb0c56634c4ec53de88d3c7972a9b1511493362e009676e6a4f333522ff2" + "value": "544d16d4ef3849a5191036094b3c00f92d7680e026227ddfbe806e775ca14a29" }, "priceSource": "demo", "provider": { @@ -424,7 +424,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "d5702421d230b0143a3448026367a4cef6078abb02fc2de104c1ce8bab85b1c2" + "value": "b66a8e99a4e3075e6abfa052db7346c2b6bd2cbac752cf85cb2a58625679a3d1" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -435,14 +435,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "43505099c95eb52d33b9d3c12d2f038ba3f10e01465215de9716e54c251110ef" + "value": "95562ef43152d03ef55688dec40c479a30cb673f4ef5f1aecf8058fad70b0b04" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "521e20bf5978bd3a56408e32e50d63b4398341299e5f4a2b009841500eee7ad2" + "value": "c1705f9d7e13003a19da98a06b3df1156313b48ece7499b38c240be76e134803" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -508,6 +508,22 @@ "emaDistancePct": 0.2503722265458551, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.11164929010108, + "ema50": 99.99258680396032, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.98779835286723, + "ema50": 99.96138735735582, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", @@ -567,6 +583,22 @@ "emaDistancePct": 0.2503722265458551, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.11164929010108, + "ema50": 99.99258680396032, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.98779835286723, + "ema50": 99.96138735735582, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 51.22203250518938, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json index 3001fdd4..b90d716e 100644 --- a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json +++ b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.builtin.json @@ -4,7 +4,7 @@ "assetClass": "crypto", "baseAsset": "ETH", "conviction": 0.41666666666666663, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -74,13 +74,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "1f9c4d6d6487f75c109c60431a90c832f8f16d1872072f4a8a8d0dc158b97d19" + "value": "834fb1b3d37d4593b712a6cedab55c17fa01d90ff30f8465220fe2793588f334" }, "executionSummaryHash": { "algorithm": "sha256", @@ -100,7 +100,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -182,7 +182,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "ccded69c3b51ad4c8746369efb28e0aa9c5d8ea6bad4524e30a48de0d88eaa00" + "value": "6d415e8d58a2cdf08a043054388585b9740f9166a67e976c3160a4eade290e49" }, "provider": { "deterministic": false, @@ -390,7 +390,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "b48fbd359172adfde85b572c31cfbcef4ee647463046ca6a5201434a1a7c17b1" + "value": "110947b5f1d71735972a60e664f962be899d3dbf3d2b9ff93beb11806ba6457a" }, "credential": { "mode": "keyless" @@ -399,7 +399,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "9677821cd94f391031f2dfca16b78467912918d8da2308f90c84f0db2566a8fb" + "value": "aac53a6206cad9eb98a1db696a6bb597c5b980b531b644a28ae31c11caaa6329" }, "priceSource": "demo", "provider": { @@ -428,7 +428,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "6c274ea4f97c60082510afb84de3244427a647914159c6b21199fa884f071e09" + "value": "ac575483dae19c908d7c1103076cd055ae0c204792be1b7e0f9a0898181abb53" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -439,14 +439,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "d860612ce70427ca842ef55c836a1b712641efe684270dd785efa918c3e45744" + "value": "3d791c3b5957cd5ef6ce70727042f68ba6705d8b8647750bdc733f8d47590936" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "95ae5c24b528c6e80e9116f8362ebc6b8179c6968080ab3382127cba1783f83a" + "value": "f939ca1c70570843ecb0534845e9d95b4a43826b8fa97178678aeba94d29cc40" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -524,6 +524,22 @@ "emaDistancePct": -0.6511453704633388, "haFlatBack": "bearish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3390.958902897049, + "ema50": 3330.0763139856076, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.646988437824, + "ema50": 2664.0631803298256, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -545,7 +561,7 @@ "assetClass": "crypto", "baseAsset": "ETH", "conviction": 0.41666666666666663, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -587,6 +603,22 @@ "emaDistancePct": -0.6511453704633388, "haFlatBack": "bearish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3390.958902897049, + "ema50": 3330.0763139856076, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.646988437824, + "ema50": 2664.0631803298256, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -776,14 +808,14 @@ "scorerInput": { "atrRegime": "normal", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": -0.6511453704633388, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1, "triggerPatternQuality": 2, - "weeklyBias": "neutral" + "weeklyBias": "short" }, "uwrMode": "builtin", "uwrResolvedSource": "builtin" diff --git a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json index 6b713fd0..934da6cb 100644 --- a/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json +++ b/test/oracle/goldens/enriched/cpj-coinbase-spot-sell.registry.json @@ -4,7 +4,7 @@ "assetClass": "crypto", "baseAsset": "ETH", "conviction": 0.41666666666666663, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -74,13 +74,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "1f9c4d6d6487f75c109c60431a90c832f8f16d1872072f4a8a8d0dc158b97d19" + "value": "834fb1b3d37d4593b712a6cedab55c17fa01d90ff30f8465220fe2793588f334" }, "executionSummaryHash": { "algorithm": "sha256", @@ -100,7 +100,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -182,7 +182,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "ccded69c3b51ad4c8746369efb28e0aa9c5d8ea6bad4524e30a48de0d88eaa00" + "value": "6d415e8d58a2cdf08a043054388585b9740f9166a67e976c3160a4eade290e49" }, "provider": { "deterministic": false, @@ -390,7 +390,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "b48fbd359172adfde85b572c31cfbcef4ee647463046ca6a5201434a1a7c17b1" + "value": "110947b5f1d71735972a60e664f962be899d3dbf3d2b9ff93beb11806ba6457a" }, "credential": { "mode": "keyless" @@ -399,7 +399,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "9677821cd94f391031f2dfca16b78467912918d8da2308f90c84f0db2566a8fb" + "value": "aac53a6206cad9eb98a1db696a6bb597c5b980b531b644a28ae31c11caaa6329" }, "priceSource": "demo", "provider": { @@ -428,7 +428,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "6c274ea4f97c60082510afb84de3244427a647914159c6b21199fa884f071e09" + "value": "ac575483dae19c908d7c1103076cd055ae0c204792be1b7e0f9a0898181abb53" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -439,14 +439,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "1dace50248ededef7bd87e0372c55d90eb481a00acd89d092467d0e68a4e67bd" + "value": "ae82c74fb2810fae9fa3035da6370e588063a06fcadb62d62e08de71e3d03d74" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "408b7f91e08658a13664650f8b99fff8c8fa40243e893fe9db9378e20a2075ad" + "value": "a575e4e5810861084fdc04533a4bcbb3f02e788a9891cd0bca939f420a28ecf1" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -524,6 +524,22 @@ "emaDistancePct": -0.6511453704633388, "haFlatBack": "bearish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3390.958902897049, + "ema50": 3330.0763139856076, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.646988437824, + "ema50": 2664.0631803298256, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -545,7 +561,7 @@ "assetClass": "crypto", "baseAsset": "ETH", "conviction": 0.41666666666666663, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -587,6 +603,22 @@ "emaDistancePct": -0.6511453704633388, "haFlatBack": "bearish", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3390.958902897049, + "ema50": 3330.0763139856076, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.646988437824, + "ema50": 2664.0631803298256, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "plan": { "barCount": 100, @@ -776,14 +808,14 @@ "scorerInput": { "atrRegime": "normal", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": -0.6511453704633388, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1, "triggerPatternQuality": 2, - "weeklyBias": "neutral" + "weeklyBias": "short" }, "uwrMode": "registry", "uwrResolvedSource": "registry" diff --git a/test/oracle/goldens/enriched/tv-long.builtin.json b/test/oracle/goldens/enriched/tv-long.builtin.json index 17734cfb..c959aa12 100644 --- a/test/oracle/goldens/enriched/tv-long.builtin.json +++ b/test/oracle/goldens/enriched/tv-long.builtin.json @@ -3,8 +3,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.41666666666666663, - "direction": "neutral", + "conviction": 0.5166666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -23,9 +23,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.2, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.41666666666666663 + "uwrScore": 0.5166666666666667 }, "canonicalUss": { "facts": { @@ -62,13 +62,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "9a29ed2dfac80ba25308e967a24158500cd56206b9931e0ac9cd107776f14396" + "value": "c50179a73f3a84edf48d1899c97663f9002dbc98230233855db81b1f8c56c77c" }, "executionSummaryHash": { "algorithm": "sha256", @@ -88,7 +88,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -112,7 +112,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" + "value": "42575abc80b237e95427bbb5dfbb5ac81243a7defc705c3bcaf9b2b20ead1710" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -170,7 +170,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b0d82fde7aa84689f5c74f8ef92fd9d2b5cf2d0eac44da403a3ded1cae347849" + "value": "9ff1c88a232b34079e42e2075e47c05dbc700b0d41fcffa1b98795f4b98de9aa" }, "provider": { "deterministic": false, @@ -378,7 +378,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "c7499752e7ce6e3ad09fc0a1761ae78a23d9d5c54d4983dd75c2eead2bb59beb" + "value": "27c7e929c8dfdcd342b88999ce99af375f9053d3c6ca705eab80213ff0b85acd" }, "credential": { "mode": "keyless" @@ -387,7 +387,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "5578cf745b7e95011e3b873dda253325f6b2fd954f10560c6d24863bc9a821ba" + "value": "b755917191221074cd5fac8c2f02650901bbc3df9824d3114ea1b6ac0307d577" }, "priceSource": "demo", "provider": { @@ -416,7 +416,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "f26e948ac64636fed339e652edd25897ebd0cae1caefab3eb9ebe9bbfb54001e" + "value": "287f694f479b3f095196f398f0a9181bc2f82b5ef962b3246173599e04750884" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -427,19 +427,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "23f37872ab1f142cfea6f0e203ce54e51543d8f3683398a455daa6cb043dfbdf" + "value": "25e2bffa92b61034713d2842ee91be489820dc5f45fd8543eded3c92565bb6eb" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "26fbf76af838aa2c470314840d995d8765d148cf17f38fbb33281f838a0fe3d2" + "value": "d30ac4b5b9443244b02234b0e38d2e10ecb3cb9ea378d89c5ff909108a6a7896" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.41666666666666663, + "conviction": 0.5166666666666667, "direction": "long", "provenanceRecordRef": "provenance-record:oracle-tv-long-0001", "providerId": "oracle-provider-tv", @@ -452,9 +452,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.2, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.41666666666666663 + "uwrScore": 0.5166666666666667 }, "signalId": "oracle-tv-long-0001", "strategyId": "trend_pullback_v1", @@ -503,6 +503,22 @@ "emaDistancePct": 0.12667804556691256, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56696.7274245733, + "ema50": 55676.344218606595, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56658.227307045956, + "ema50": 55551.53781773057, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", @@ -514,8 +530,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.41666666666666663, - "direction": "neutral", + "conviction": 0.5166666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -534,9 +550,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.2, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.41666666666666663 + "uwrScore": 0.5166666666666667 }, "decayParams": { "barsPerHalfLife": 12, @@ -557,6 +573,22 @@ "emaDistancePct": 0.12667804556691256, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56696.7274245733, + "ema50": 55676.344218606595, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56658.227307045956, + "ema50": 55551.53781773057, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", @@ -692,19 +724,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" + "value": "42575abc80b237e95427bbb5dfbb5ac81243a7defc705c3bcaf9b2b20ead1710" }, "scorerInput": { "atrRegime": "normal", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": 0.12667804556691256, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1, "triggerPatternQuality": 2, - "weeklyBias": "neutral" + "weeklyBias": "long" }, "uwrMode": "builtin", "uwrResolvedSource": "builtin" diff --git a/test/oracle/goldens/enriched/tv-long.registry.json b/test/oracle/goldens/enriched/tv-long.registry.json index 5d3b3b3b..a77f8c69 100644 --- a/test/oracle/goldens/enriched/tv-long.registry.json +++ b/test/oracle/goldens/enriched/tv-long.registry.json @@ -3,8 +3,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.41666666666666663, - "direction": "neutral", + "conviction": 0.5166666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -23,9 +23,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.2, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.41666666666666663 + "uwrScore": 0.5166666666666667 }, "canonicalUss": { "facts": { @@ -62,13 +62,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "9a29ed2dfac80ba25308e967a24158500cd56206b9931e0ac9cd107776f14396" + "value": "c50179a73f3a84edf48d1899c97663f9002dbc98230233855db81b1f8c56c77c" }, "executionSummaryHash": { "algorithm": "sha256", @@ -88,7 +88,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -112,7 +112,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" + "value": "42575abc80b237e95427bbb5dfbb5ac81243a7defc705c3bcaf9b2b20ead1710" }, "schema": "afi.provenance-record.v1", "schemaVersions": { @@ -170,7 +170,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b0d82fde7aa84689f5c74f8ef92fd9d2b5cf2d0eac44da403a3ded1cae347849" + "value": "9ff1c88a232b34079e42e2075e47c05dbc700b0d41fcffa1b98795f4b98de9aa" }, "provider": { "deterministic": false, @@ -378,7 +378,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "c7499752e7ce6e3ad09fc0a1761ae78a23d9d5c54d4983dd75c2eead2bb59beb" + "value": "27c7e929c8dfdcd342b88999ce99af375f9053d3c6ca705eab80213ff0b85acd" }, "credential": { "mode": "keyless" @@ -387,7 +387,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "5578cf745b7e95011e3b873dda253325f6b2fd954f10560c6d24863bc9a821ba" + "value": "b755917191221074cd5fac8c2f02650901bbc3df9824d3114ea1b6ac0307d577" }, "priceSource": "demo", "provider": { @@ -416,7 +416,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "f26e948ac64636fed339e652edd25897ebd0cae1caefab3eb9ebe9bbfb54001e" + "value": "287f694f479b3f095196f398f0a9181bc2f82b5ef962b3246173599e04750884" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -427,19 +427,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "2d5c6a0197d657609d38cd88868f908b41733bb42612bfc0deecfaa2a3398679" + "value": "03ba5267139b8406878e8a459e47125bbb76bc22161e884efa89baa5c2a750ff" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "831353ad7671ad27c3484740f83671c19a57bdd6eb09223df08fcd2801d904d0" + "value": "ca04da108e01ad2636ed7f333f34ef49a14254135f85ffea2648f617fd13d20b" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { "analystId": "froggy", - "conviction": 0.41666666666666663, + "conviction": 0.5166666666666667, "direction": "long", "provenanceRecordRef": "provenance-record:oracle-tv-long-0001", "providerId": "oracle-provider-tv", @@ -452,9 +452,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.2, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.41666666666666663 + "uwrScore": 0.5166666666666667 }, "signalId": "oracle-tv-long-0001", "strategyId": "trend_pullback_v1", @@ -503,6 +503,22 @@ "emaDistancePct": 0.12667804556691256, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56696.7274245733, + "ema50": 55676.344218606595, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56658.227307045956, + "ema50": 55551.53781773057, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", @@ -514,8 +530,8 @@ "analystId": "froggy", "assetClass": "crypto", "baseAsset": "BTC", - "conviction": 0.41666666666666663, - "direction": "neutral", + "conviction": 0.5166666666666667, + "direction": "long", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -534,9 +550,9 @@ "execution": 0.6666666666666666, "insight": 0.4, "risk": 0.2, - "structure": 0.4 + "structure": 0.8 }, - "uwrScore": 0.41666666666666663 + "uwrScore": 0.5166666666666667 }, "decayParams": { "barsPerHalfLife": 12, @@ -557,6 +573,22 @@ "emaDistancePct": 0.12667804556691256, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 56696.7274245733, + "ema50": 55676.344218606595, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 56658.227307045956, + "ema50": 55551.53781773057, + "timeframe": "1w", + "trendBias": "bullish" + } + }, "isInValueSweetSpot": true, "rsi14": 50.587295212999244, "trendBias": "range", @@ -692,19 +724,19 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.scored-output", - "value": "7c92f64874a59b4b0d5d6111a6841388f734e0f95e766663590246306665288f" + "value": "42575abc80b237e95427bbb5dfbb5ac81243a7defc705c3bcaf9b2b20ead1710" }, "scorerInput": { "atrRegime": "normal", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": 0.12667804556691256, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1, "triggerPatternQuality": 2, - "weeklyBias": "neutral" + "weeklyBias": "long" }, "uwrMode": "registry", "uwrResolvedSource": "registry" diff --git a/test/oracle/goldens/enriched/tv-neutral.builtin.json b/test/oracle/goldens/enriched/tv-neutral.builtin.json index 93a996ec..55aef0c8 100644 --- a/test/oracle/goldens/enriched/tv-neutral.builtin.json +++ b/test/oracle/goldens/enriched/tv-neutral.builtin.json @@ -68,13 +68,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "cfb5c348c4452c3aa8a9f8896ae6cdc06f53b1a73147a142c751300fa6aa49e4" + "value": "5688fab6084f8cef96ce6516efd449f14d3a39369c35dc94818735ef427c64ff" }, "executionSummaryHash": { "algorithm": "sha256", @@ -94,7 +94,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -176,7 +176,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b79d3b1c17058895eb2bff5aad85cc8b1f4dcc35fb8e1d056944055fcc91829e" + "value": "a8b76ab842923d4de633948efa4bd6de45cd3e0b1d568571f3e0050b00e731f3" }, "provider": { "deterministic": false, @@ -384,7 +384,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "137adc643c56ad6cc9bef935a428966de9d026469c4622a221056ad747b9c2b1" + "value": "9b8fc18ee1afa42ca1c10c2130277287f3d52972fa0abf05b5810ec7aee3b42e" }, "credential": { "mode": "keyless" @@ -393,7 +393,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "27ccd79b3df23a3dec1715d2697085687a94ff78318b29b595def7954c684f5c" + "value": "c09cf9b0f67ff6854750ce1caa55cfb4a8f9300ce9bb91d0a0454ee355142d0c" }, "priceSource": "demo", "provider": { @@ -422,7 +422,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "c96a3ce4e9bcab948c56421e43a45c368f372a40911981b26ba323bf5586ec72" + "value": "1665ee320af5c831a44fed67d916794ad5b401aefe973cdf7436b93ae243a951" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -433,14 +433,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "6f0e0d4c4784f3130674415b9f809cfa671a41d393891324c75f5dec9827a495" + "value": "ef5c230a99c26e60d954e4576592af21cb04fc96e5a8d929fda9cc90a045b9be" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "327bc0a90a22aef1cc94522cca0d3b4a431f3fbc4c29d850b1a55153c677939a" + "value": "2513859686098ec6f94cbf8635f6dc3fb9bfa0660b07cfa8cab606aab1b8543b" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -497,6 +497,22 @@ "emaDistancePct": -0.32435456034904336, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.08565012114722, + "ema50": 100.04049309734384, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.90335558323204, + "ema50": 99.87299772431832, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", @@ -557,6 +573,22 @@ "emaDistancePct": -0.32435456034904336, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.08565012114722, + "ema50": 100.04049309734384, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.90335558323204, + "ema50": 99.87299772431832, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/tv-neutral.registry.json b/test/oracle/goldens/enriched/tv-neutral.registry.json index d212831f..04fb4623 100644 --- a/test/oracle/goldens/enriched/tv-neutral.registry.json +++ b/test/oracle/goldens/enriched/tv-neutral.registry.json @@ -68,13 +68,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "cfb5c348c4452c3aa8a9f8896ae6cdc06f53b1a73147a142c751300fa6aa49e4" + "value": "5688fab6084f8cef96ce6516efd449f14d3a39369c35dc94818735ef427c64ff" }, "executionSummaryHash": { "algorithm": "sha256", @@ -94,7 +94,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -176,7 +176,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b79d3b1c17058895eb2bff5aad85cc8b1f4dcc35fb8e1d056944055fcc91829e" + "value": "a8b76ab842923d4de633948efa4bd6de45cd3e0b1d568571f3e0050b00e731f3" }, "provider": { "deterministic": false, @@ -384,7 +384,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "137adc643c56ad6cc9bef935a428966de9d026469c4622a221056ad747b9c2b1" + "value": "9b8fc18ee1afa42ca1c10c2130277287f3d52972fa0abf05b5810ec7aee3b42e" }, "credential": { "mode": "keyless" @@ -393,7 +393,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "27ccd79b3df23a3dec1715d2697085687a94ff78318b29b595def7954c684f5c" + "value": "c09cf9b0f67ff6854750ce1caa55cfb4a8f9300ce9bb91d0a0454ee355142d0c" }, "priceSource": "demo", "provider": { @@ -422,7 +422,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "c96a3ce4e9bcab948c56421e43a45c368f372a40911981b26ba323bf5586ec72" + "value": "1665ee320af5c831a44fed67d916794ad5b401aefe973cdf7436b93ae243a951" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -433,14 +433,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "d11f22bbde3f5e653716dbdb6450e32c3de1063ab1a201507cd83e18aba0ddd4" + "value": "bf5188d3de09a8b610abd2c81f5bd92b52dab71775b42e47cb0f085be27d1b51" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "ba08fb62f47dc15d7bce1392b5b0ee1e54d0c90a4725af62fea2c6124101f595" + "value": "bc8d53147975b23246c62cc01041463d5e10c2a2a73177640aa9f6e9e069872d" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -497,6 +497,22 @@ "emaDistancePct": -0.32435456034904336, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.08565012114722, + "ema50": 100.04049309734384, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.90335558323204, + "ema50": 99.87299772431832, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", @@ -557,6 +573,22 @@ "emaDistancePct": -0.32435456034904336, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 100.08565012114722, + "ema50": 100.04049309734384, + "timeframe": "1d", + "trendBias": "range" + }, + "weekly": { + "barCount": 100, + "ema20": 99.90335558323204, + "ema50": 99.87299772431832, + "timeframe": "1w", + "trendBias": "range" + } + }, "isInValueSweetSpot": true, "rsi14": 47.95287622391937, "trendBias": "range", diff --git a/test/oracle/goldens/enriched/tv-short.builtin.json b/test/oracle/goldens/enriched/tv-short.builtin.json index 9a2c17db..a3529f0d 100644 --- a/test/oracle/goldens/enriched/tv-short.builtin.json +++ b/test/oracle/goldens/enriched/tv-short.builtin.json @@ -8,7 +8,7 @@ }, "baseAsset": "BTC", "conviction": 0.17500000000000002, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -67,13 +67,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "6d71a67e34c67d35b7a07ee9344dd8828cbff409cac23fc795b4abf0c6615bd1" + "value": "c155c9e45c31d64d809af77cc13253bf30c61e0a27af921e60af7704a8c0de99" }, "executionSummaryHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -175,7 +175,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b144b16d61dae4e9403ed9ec0ab79acca23fe9837f23e03398309eea19b31015" + "value": "69ea0de1d3d1fccf1b9c5f8d9f4eefba8a7ef0c856ac6f14cbfe1515121c9209" }, "provider": { "deterministic": false, @@ -383,7 +383,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "5252fad0a334a2eaf7f0472a1e58a8ff7fa376cc7be72191ca3a2a24a47fea42" + "value": "070f8f716b98d6a33d9225266a099bae0917ecd6807bb49c5e3b41e057e323ab" }, "credential": { "mode": "keyless" @@ -392,7 +392,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "3fad8e48dee464d94dd050b5cf05675c9337b5a9fe37ea9cef86ebf09e97739c" + "value": "363161ae3f0c2a479d0af3f1e92a35a6b1f653fcf9c3be6d0ca6d3df1ac5342d" }, "priceSource": "demo", "provider": { @@ -421,7 +421,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "3f89d32f09cbecfaf595cc879d1fe3d9dbb5fdd4f5b1491c48b8e13ceb128be1" + "value": "668255c429b7364ed8cac43cea40aa199812993682fb77d5e4719035422ee54f" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -432,14 +432,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "43649bac3580c41221fdeb392a771047a0787928f82a7951f7fd74a9ad5dbf1b" + "value": "153e70a9c752e16c5d68f2d2b47824b102efd0e7827bdb2ba8121ac62dbdaee2" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "53f0a98fe4e6ee136a141bfdddf9aa851def2f00897cac2ad586e81e612a88ce" + "value": "364792642ebda7a4cc93508ea20de583ac5207e5fbd330fe6e5678dac84a68c9" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -496,6 +496,22 @@ "emaDistancePct": 0.2706436977661548, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3402.5132328947357, + "ema50": 3336.7325837338044, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.260401414665, + "ema50": 2662.953004792269, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", @@ -512,7 +528,7 @@ }, "baseAsset": "BTC", "conviction": 0.17500000000000002, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -555,6 +571,22 @@ "emaDistancePct": 0.2706436977661548, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3402.5132328947357, + "ema50": 3336.7325837338044, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.260401414665, + "ema50": 2662.953004792269, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", @@ -683,14 +715,14 @@ "scorerInput": { "atrRegime": "low", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": 0.2706436977661548, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1, "triggerPatternQuality": 0, - "weeklyBias": "neutral" + "weeklyBias": "short" }, "uwrMode": "builtin", "uwrResolvedSource": "builtin" diff --git a/test/oracle/goldens/enriched/tv-short.registry.json b/test/oracle/goldens/enriched/tv-short.registry.json index 2f4a27ad..8efe2f53 100644 --- a/test/oracle/goldens/enriched/tv-short.registry.json +++ b/test/oracle/goldens/enriched/tv-short.registry.json @@ -8,7 +8,7 @@ }, "baseAsset": "BTC", "conviction": 0.17500000000000002, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -67,13 +67,13 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "enrichmentHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.enrichment-bundle", - "value": "6d71a67e34c67d35b7a07ee9344dd8828cbff409cac23fc795b4abf0c6615bd1" + "value": "c155c9e45c31d64d809af77cc13253bf30c61e0a27af921e60af7704a8c0de99" }, "executionSummaryHash": { "algorithm": "sha256", @@ -93,7 +93,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.plugin-set", - "value": "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "value": "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" }, "schema": "afi.composition-ref.v1", "scorerPluginId": "afi-scorer-froggy-trend-pullback", @@ -175,7 +175,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "b144b16d61dae4e9403ed9ec0ab79acca23fe9837f23e03398309eea19b31015" + "value": "69ea0de1d3d1fccf1b9c5f8d9f4eefba8a7ef0c856ac6f14cbfe1515121c9209" }, "provider": { "deterministic": false, @@ -383,7 +383,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.lane-output", - "value": "5252fad0a334a2eaf7f0472a1e58a8ff7fa376cc7be72191ca3a2a24a47fea42" + "value": "070f8f716b98d6a33d9225266a099bae0917ecd6807bb49c5e3b41e057e323ab" }, "credential": { "mode": "keyless" @@ -392,7 +392,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-invocation-input", - "value": "3fad8e48dee464d94dd050b5cf05675c9337b5a9fe37ea9cef86ebf09e97739c" + "value": "363161ae3f0c2a479d0af3f1e92a35a6b1f653fcf9c3be6d0ca6d3df1ac5342d" }, "priceSource": "demo", "provider": { @@ -421,7 +421,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.provider-result", - "value": "3f89d32f09cbecfaf595cc879d1fe3d9dbb5fdd4f5b1491c48b8e13ceb128be1" + "value": "668255c429b7364ed8cac43cea40aa199812993682fb77d5e4719035422ee54f" }, "resultSchema": "afi.enrichment.technical.v1", "schema": "afi.provider-invocation-proof.v1", @@ -432,14 +432,14 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-record", - "value": "854c7745a49520fc702c6714988b26570fb83de8f53e1b96f405ab7533c99c23" + "value": "b62adbc798721d01c5271fb8e919339b387df9f25080a45c370e63404c5c12e8" }, "recordVersion": 1, "replayHash": { "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.evidence-replay", - "value": "edea624a276036e831ca3d18caba08cbf3beb1ffaf8eb1cb5f96b12abb56ffdc" + "value": "778e51fdebdfe9001fdfd19d5201155f09cf66d46f6323401bd078bea31c06ec" }, "schema": "afi.scored-signal-evidence.v3", "scoredSignal": { @@ -496,6 +496,22 @@ "emaDistancePct": 0.2706436977661548, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3402.5132328947357, + "ema50": 3336.7325837338044, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.260401414665, + "ema50": 2662.953004792269, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", @@ -512,7 +528,7 @@ }, "baseAsset": "BTC", "conviction": 0.17500000000000002, - "direction": "neutral", + "direction": "unknown", "holdingHorizon": "swing", "instrumentType": "linear-perp", "marketType": "perp", @@ -555,6 +571,22 @@ "emaDistancePct": 0.2706436977661548, "haFlatBack": "none", "haFlatBackConfirmed": false, + "htf": { + "daily": { + "barCount": 100, + "ema20": 3402.5132328947357, + "ema50": 3336.7325837338044, + "timeframe": "1d", + "trendBias": "bullish" + }, + "weekly": { + "barCount": 100, + "ema20": 2598.260401414665, + "ema50": 2662.953004792269, + "timeframe": "1w", + "trendBias": "bearish" + } + }, "isInValueSweetSpot": true, "rsi14": 51.5458437981575, "trendBias": "range", @@ -683,14 +715,14 @@ "scorerInput": { "atrRegime": "low", "brokeEmaWithBody": false, - "dailyBias": "neutral", + "dailyBias": "long", "distanceFromDailyEmaPct": 0.2706436977661548, "haFlatBackConfirmed": false, "liquiditySwept": false, "pulledBackIntoSweetSpot": true, "rrMultiplePlanned": 1, "triggerPatternQuality": 0, - "weeklyBias": "neutral" + "weeklyBias": "short" }, "uwrMode": "registry", "uwrResolvedSource": "registry" diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index 81038ac0..35293353 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,9 +2,9 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.2.0", + "implementationVersion": "2.3.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef and the runtime resolves the registered technical adapter, which computes indicator features (EMA/RSI/ATR) over the candles it fetches for the validated signal. No vendor logic in the node; deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: over the same window, technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane's trend law). No absence beyond the 50-candle floor.", + "description": "Official technical lane manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef; the runtime resolves the registered adapter, which computes EMA/RSI/ATR features over the candles it fetches. Deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane trend law). HTF: with params.htf set, the declared daily/weekly windows are also fetched and technical.htf.{daily,weekly}.trendBias emitted by the same law; a window under the 50-candle floor emits no sub-block.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, @@ -30,6 +30,37 @@ "minimum": 1, "maximum": 1000, "description": "Number of candles fetched from the price feed for indicator computation." + }, + "htf": { + "type": "object", + "additionalProperties": false, + "required": [ + "dailyTimeframe", + "weeklyTimeframe" + ], + "description": "OPTIONAL higher-timeframe selection (DEM-GOV §9 DEM-PRODUCER-HTF, owner determination D-1: an additive optional parameter within the identity of afi-analysis-technical@2.0.0). Absent means the lane fetches only the signal's own timeframe, exactly as before — every existing valid configuration remains valid. Closed vocabularies: the runtime fails closed at boot on anything else.", + "properties": { + "dailyTimeframe": { + "type": "string", + "enum": [ + "1d" + ], + "description": "The daily-bias window's timeframe token." + }, + "weeklyTimeframe": { + "type": "string", + "enum": [ + "1w" + ], + "description": "The weekly-bias window's timeframe token." + }, + "htfCandleLimit": { + "type": "integer", + "minimum": 50, + "maximum": 1000, + "description": "Candles fetched per higher-timeframe window (at or above the 50-candle kernel floor). Default 100." + } + } } } }, diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index e78e927c..32a0450e 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.3.0", + "implementationVersion": "1.4.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 3dbd1dcc..40e59fdf 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.5.0", + "implementationVersion": "1.6.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index ef7bec93..111bdef5 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,7 +22,7 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.2.0" + "version": "1.3.0" }, "decayConfig": { "ratio": { @@ -33,6 +33,10 @@ "metadata": { "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", "supersedes": [ + { + "analystConfigHash": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99", + "act": "DEM-GOV DEM-PRODUCER-CANDLE — mappingRef froggy-trend-pullback 1.2.0 (2026-08-25)" + }, { "analystConfigHash": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", "act": "DEM-GOV DEM-PRODUCER-PLAN — mappingRef froggy-trend-pullback 1.1.0 (2026-08-25)" @@ -42,5 +46,16 @@ "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" } ] + }, + "nodeOverrides": { + "technical": { + "config": { + "htf": { + "dailyTimeframe": "1d", + "weeklyTimeframe": "1w", + "htfCandleLimit": 100 + } + } + } } } diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index 2a3deeb8..07ef5785 100644 --- a/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/pipeline/fixtures/afi-config-atlas/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json b/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json new file mode 100644 index 00000000..428e6a9d --- /dev/null +++ b/test/pipeline/fixtures/afi-config-atlas/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json @@ -0,0 +1,159 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.3.0", + "description": "DEM-PRODUCER-HTF: the 1.2.0 bindings plus weeklyBias and dailyBias recoded from the technical lane's higher-timeframe trend facts (technical.htf.weekly.trendBias / technical.htf.daily.trendBias), computed by the same EMA law over separately fetched windows whose timeframes are a registered composition value. The vocabulary translation lives here, not in the producer (D-DEM-3). A window below the kernel floor is a declared producer absence and fires the recode's absent member.", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + }, + "brokeEmaWithBody": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "brokeEmaWithBody", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "haFlatBackConfirmed": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "haFlatBackConfirmed", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "weeklyBias": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "htf.weekly.trendBias", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "table": { + "bullish": "long", + "bearish": "short", + "range": "neutral" + }, + "fallback": "neutral", + "absent": "neutral" + }, + "dailyBias": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "htf.daily.trendBias", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "table": { + "bullish": "long", + "bearish": "short", + "range": "neutral" + }, + "fallback": "neutral", + "absent": "neutral" + } + } +} diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json index 81038ac0..35293353 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-analysis-technical--2.0.0.json @@ -2,9 +2,9 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-analysis-technical", "pluginVersion": "2.0.0", - "implementationVersion": "2.2.0", + "implementationVersion": "2.3.0", "category": "technical", - "description": "Official technical lane plugin manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef and the runtime resolves the registered technical adapter, which computes indicator features (EMA/RSI/ATR) over the candles it fetches for the validated signal. No vendor logic in the node; deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Declared absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: over the same window, technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane's trend law). No absence beyond the 50-candle floor.", + "description": "Official technical lane manifest (vendor-neutral, provider-instance-backed): the node reads its providerInstanceRef; the runtime resolves the registered adapter, which computes EMA/RSI/ATR features over the candles it fetches. Deterministic. PRODUCER CONTRACTS (DEM-GOV §9). PLAN: a submitted afi.trade-plan.v1 is verified against the fetched window (levels within one observed-range width; stop/targets opposite sides of entry) and emitted as technical.plan; unverifiable → refusal (D-DEM-5(6)). Absences (D-DEM-5(4)(b)): no plan → no block; no stop or target → no rrToFirstTarget. CANDLE: technical.brokeEmaWithBody (latest body closed counter-trend of EMA20; range: crossed it) and haFlatBack/haFlatBackConfirmed (Heikin-Ashi flat-back, seed (o+c)/2, epsilon 0, agreeing with the lane trend law). HTF: with params.htf set, the declared daily/weekly windows are also fetched and technical.htf.{daily,weekly}.trendBias emitted by the same law; a window under the 50-candle floor emits no sub-block.", "inputSchemaRef": "afi.usignal.v1.1", "outputSchemaRef": "afi.enrichment.technical.v1", "deterministic": true, @@ -30,6 +30,37 @@ "minimum": 1, "maximum": 1000, "description": "Number of candles fetched from the price feed for indicator computation." + }, + "htf": { + "type": "object", + "additionalProperties": false, + "required": [ + "dailyTimeframe", + "weeklyTimeframe" + ], + "description": "OPTIONAL higher-timeframe selection (DEM-GOV §9 DEM-PRODUCER-HTF, owner determination D-1: an additive optional parameter within the identity of afi-analysis-technical@2.0.0). Absent means the lane fetches only the signal's own timeframe, exactly as before — every existing valid configuration remains valid. Closed vocabularies: the runtime fails closed at boot on anything else.", + "properties": { + "dailyTimeframe": { + "type": "string", + "enum": [ + "1d" + ], + "description": "The daily-bias window's timeframe token." + }, + "weeklyTimeframe": { + "type": "string", + "enum": [ + "1w" + ], + "description": "The weekly-bias window's timeframe token." + }, + "htfCandleLimit": { + "type": "integer", + "minimum": 50, + "maximum": 1000, + "description": "Candles fetched per higher-timeframe window (at or above the 50-candle kernel floor). Default 100." + } + } } } }, diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json index e78e927c..32a0450e 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-merge-enriched-view--1.1.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-merge-enriched-view", "pluginVersion": "1.1.0", - "implementationVersion": "1.3.0", + "implementationVersion": "1.4.0", "category": "merge", "description": "Official deterministic merge plugin manifest: joins the five analysis category results (technical, pattern, sentiment, news, aiMl) into one enriched view under the namespace-by-node strategy — exactly one result per category, duplicate categories rejected, degraded/skipped optional parents contribute empty namespaces. Pure structural merge — deterministic, no external I/O, no provider invocation.", "inputSchemaRef": "afi.enrichment.bundle.v1", diff --git a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json index 3dbd1dcc..40e59fdf 100644 --- a/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analysis-plugins/afi-scorer-froggy-trend-pullback--1.0.0.json @@ -2,7 +2,7 @@ "schema": "afi.analysis-plugin.v1", "pluginId": "afi-scorer-froggy-trend-pullback", "pluginVersion": "1.0.0", - "implementationVersion": "1.5.0", + "implementationVersion": "1.6.0", "category": "scorer", "description": "Official froggy trend-pullback scorer plugin manifest: the single scoring seam of the froggy-trend-pullback pipeline. Consumes the AI/ML-augmented enriched view and produces the scored output on deterministic kernels; performs the sole VALIDATED->SCORED transition in the consuming runtime (LIFE-GOV transition monopoly).", "inputSchemaRef": "afi.enrichment.aiml.v1", diff --git a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json index ef7bec93..111bdef5 100644 --- a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json +++ b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json @@ -22,7 +22,7 @@ }, "mappingRef": { "mappingId": "froggy-trend-pullback", - "version": "1.2.0" + "version": "1.3.0" }, "decayConfig": { "ratio": { @@ -33,6 +33,10 @@ "metadata": { "note": "Non-authoritative, hash-excluded lineage of this configuration version (D-CFG-5(4): the version identity IS analystConfigHash; a superseded version survives on every record sealed under it and in git history).", "supersedes": [ + { + "analystConfigHash": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99", + "act": "DEM-GOV DEM-PRODUCER-CANDLE — mappingRef froggy-trend-pullback 1.2.0 (2026-08-25)" + }, { "analystConfigHash": "5cb9b7a4652f3cfaa31d8a069c14c3282bbdf9354443badb033b05b01fd01e22", "act": "DEM-GOV DEM-PRODUCER-PLAN — mappingRef froggy-trend-pullback 1.1.0 (2026-08-25)" @@ -42,5 +46,16 @@ "act": "DEM-GOV DEM-BIND step (d) — mappingRef froggy-trend-pullback 1.0.0 (2026-08-23)" } ] + }, + "nodeOverrides": { + "technical": { + "config": { + "htf": { + "dailyTimeframe": "1d", + "weeklyTimeframe": "1w", + "htfCandleLimit": 100 + } + } + } } } diff --git a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json index 2a3deeb8..07ef5785 100644 --- a/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json +++ b/test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.json @@ -7,7 +7,7 @@ "algorithm": "sha256", "canonicalizationVersion": "afi.hash.v1", "domainTag": "afi.d2.analyst-config", - "value": "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "value": "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" }, "configRef": "afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json", "providerBindingPolicy": { diff --git a/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json b/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json new file mode 100644 index 00000000..428e6a9d --- /dev/null +++ b/test/pipeline/fixtures/afi-config/registries/enrichment-mappings/froggy-trend-pullback--1.3.0.json @@ -0,0 +1,159 @@ +{ + "schema": "afi.enrichment-mapping.v1", + "mappingId": "froggy-trend-pullback", + "version": "1.3.0", + "description": "DEM-PRODUCER-HTF: the 1.2.0 bindings plus weeklyBias and dailyBias recoded from the technical lane's higher-timeframe trend facts (technical.htf.weekly.trendBias / technical.htf.daily.trendBias), computed by the same EMA law over separately fetched windows whose timeframes are a registered composition value. The vocabulary translation lives here, not in the producer (D-DEM-3). A window below the kernel floor is a declared producer absence and fires the recode's absent member.", + "namespaceDefaults": [ + "technical", + "pattern" + ], + "bindings": { + "distanceFromDailyEmaPct": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "emaDistancePct" + }, + "type": "number", + "optionality": { + "ground": "grandfather", + "default": 0 + } + }, + "pulledBackIntoSweetSpot": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "isInValueSweetSpot" + }, + "type": "boolean", + "optionality": { + "ground": "grandfather", + "default": false + } + }, + "triggerPatternQuality": { + "operator": "band", + "source": { + "lane": "pattern", + "path": "patternConfidence" + }, + "rows": [ + { + "when": { + "gte": 75 + }, + "value": 3 + }, + { + "when": { + "gte": 65 + }, + "value": 2 + }, + { + "when": { + "gt": 0 + }, + "value": 1 + } + ], + "otherwise": 0, + "absent": 0 + }, + "atrRegime": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "atrRegime" + }, + "table": { + "low": "low", + "high": "high", + "extreme": "extreme" + }, + "fallback": "normal", + "absent": "normal" + }, + "rrMultiplePlanned": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "plan.rrToFirstTarget", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "number", + "optionality": { + "ground": "producer-declared", + "default": 1, + "producerRef": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + } + }, + "brokeEmaWithBody": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "brokeEmaWithBody", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "haFlatBackConfirmed": { + "operator": "bind", + "source": { + "lane": "technical", + "path": "haFlatBackConfirmed", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "type": "boolean" + }, + "weeklyBias": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "htf.weekly.trendBias", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "table": { + "bullish": "long", + "bearish": "short", + "range": "neutral" + }, + "fallback": "neutral", + "absent": "neutral" + }, + "dailyBias": { + "operator": "recode", + "source": { + "lane": "technical", + "path": "htf.daily.trendBias", + "producedBy": { + "pluginId": "afi-analysis-technical", + "pluginVersion": "2.0.0" + } + }, + "table": { + "bullish": "long", + "bearish": "short", + "range": "neutral" + }, + "fallback": "neutral", + "absent": "neutral" + } + } +} diff --git a/test/pipeline/htfProducer.test.ts b/test/pipeline/htfProducer.test.ts new file mode 100644 index 00000000..10cd9a32 --- /dev/null +++ b/test/pipeline/htfProducer.test.ts @@ -0,0 +1,184 @@ +/** + * DEM-PRODUCER-HTF — the technical lane's higher-timeframe bias producer, and + * the DIR-GOV D-DIR-3 negative test the accepted gate requires: + * + * "DIR-GOV D-DIR-3 honored — no submitted or declared trade direction + * reaches any bias or axis input, proven by a dedicated negative test" + * + * plus: timeframe selection comes from the REGISTERED COMPOSITION VALUE (the + * analyst config's nodeOverrides, handed down as ctx.config), never a code + * constant; the windows are fetched concurrently; a window below the kernel + * floor is a declared absence, never a guessed bias. + */ +import { describe, it, expect } from "@jest/globals"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { createTechnicalLocalAdapter } from "../../src/providers/adapters/technicalLocalAdapter.js"; +import { computeTechnicalEnrichment } from "../../src/enrichment/technicalIndicators.js"; +import { SILENT_NODE_LOGGER } from "../../src/pipeline/nodeSdk.js"; +import type { ProviderAdapterContext } from "../../src/providers/types.js"; +import type { PriceFeedAdapter } from "../../src/adapters/exchanges/types.js"; +import type { OHLCVCandle } from "../../src/adapters/exchanges/types.js"; +import { demoPriceFeedAdapter } from "../support/deterministicPriceFeedAdapter.js"; + +const HTF = { dailyTimeframe: "1d", weeklyTimeframe: "1w", htfCandleLimit: 100 }; + +/** The registered composition value, as the fixture analyst config carries it. */ +const FIXTURE_CONFIG = path.resolve( + process.cwd(), + "test/pipeline/fixtures/afi-config/registries/analyst-strategies/froggy--trend_pullback_v1--1.0.0.config.json" +); + +function adapter(feed: PriceFeedAdapter = demoPriceFeedAdapter as unknown as PriceFeedAdapter) { + return createTechnicalLocalAdapter({ + resolvePriceSource: () => "demo", + getAdapter: () => feed, + computeTechnical: computeTechnicalEnrichment, + }); +} + +function ctx(signal: Record, config: Record): ProviderAdapterContext { + return { + signal: signal as ProviderAdapterContext["signal"], + input: undefined, + config, + logger: SILENT_NODE_LOGGER, + abort: new AbortController().signal, + credential: undefined, + } as unknown as ProviderAdapterContext; +} + +function signal(symbol: string, direction: "long" | "short" | "neutral", timeframe = "4h") { + return { + schema: "afi.usignal.v1.1", + provenance: { source: "test", providerId: "htf-test", signalId: "sig-htf" }, + facts: { symbol, market: "perp", timeframe, strategy: "trend_pullback_v1", direction }, + }; +} + +type Htf = { daily?: { trendBias: string; timeframe: string; barCount: number }; weekly?: { trendBias: string; timeframe: string } }; +async function run(sym: string, dir: "long" | "short" | "neutral", config: Record = { candleLimit: 100, htf: HTF }) { + return (await adapter().run(ctx(signal(sym, dir), config))) as unknown as { + technical: { htf?: Htf; trendBias: string }; + }; +} + +describe("DEM-PRODUCER-HTF — the higher-timeframe bias producer", () => { + it("emits htf.{daily,weekly} from separately fetched windows when the composition registers them", async () => { + const out = await run("BTC/USDT", "long"); + expect(out.technical.htf).toBeDefined(); + expect(out.technical.htf!.daily).toMatchObject({ timeframe: "1d", trendBias: "bullish", barCount: 100 }); + expect(out.technical.htf!.weekly).toMatchObject({ timeframe: "1w", trendBias: "bullish" }); + }); + + it("emits NO htf block when the composition registers none (every pre-HTF configuration stays valid)", async () => { + const out = await run("BTC/USDT", "long", { candleLimit: 100 }); + expect(out.technical.htf).toBeUndefined(); + }); + + it("the two timeframes come from the REGISTERED value, not a code constant: changing them changes the windows fetched", async () => { + const seen: Array<{ timeframe: string; limit?: number }> = []; + const spy: PriceFeedAdapter = { + ...(demoPriceFeedAdapter as unknown as PriceFeedAdapter), + async getOHLCV(params: { symbol: string; timeframe: string; limit?: number }): Promise { + seen.push({ timeframe: params.timeframe, limit: params.limit }); + return demoPriceFeedAdapter.getOHLCV(params); + }, + }; + await adapter(spy).run(ctx(signal("BTC/USDT", "long"), { candleLimit: 100, htf: HTF })); + expect(seen.map((s) => s.timeframe).sort()).toEqual(["1d", "1w", "4h"]); + expect(seen.find((s) => s.timeframe === "1d")!.limit).toBe(100); + // And the registered value the FIXTURE config carries is exactly this one. + const registered = JSON.parse(readFileSync(FIXTURE_CONFIG, "utf-8")) as { + nodeOverrides?: { technical?: { config?: { htf?: unknown } } }; + }; + expect(registered.nodeOverrides!.technical!.config!.htf).toEqual(HTF); + }); + + it("fetches the three windows CONCURRENTLY (the lane is the pipeline's entry node)", async () => { + let inFlight = 0; + let peak = 0; + const slow: PriceFeedAdapter = { + ...(demoPriceFeedAdapter as unknown as PriceFeedAdapter), + async getOHLCV(params: { symbol: string; timeframe: string; limit?: number }): Promise { + inFlight += 1; + peak = Math.max(peak, inFlight); + await new Promise((r) => setTimeout(r, 5)); + inFlight -= 1; + return demoPriceFeedAdapter.getOHLCV(params); + }, + }; + await adapter(slow).run(ctx(signal("BTC/USDT", "long"), { candleLimit: 100, htf: HTF })); + expect(peak).toBe(3); + }); + + it("a window below the kernel floor emits NO sub-block (declared absence, never a guessed bias)", async () => { + const out = await run("BTC/USDT", "long", { candleLimit: 100, htf: { ...HTF, htfCandleLimit: 51 } }); + // 51 candles clears the 50-bar floor… + expect(out.technical.htf!.daily).toBeDefined(); + const thin = (await adapter().run( + ctx(signal("BTC/USDT", "long"), { candleLimit: 100, htf: { ...HTF, htfCandleLimit: 40 } }) + )) as unknown as { technical: { htf?: Htf } }; + // …40 does not: the block is emitted with no sub-blocks at all. + expect(thin.technical.htf).toEqual({}); + }); + + it("refuses a malformed registered htf value rather than guessing (fail-closed)", async () => { + await expect( + adapter().run(ctx(signal("BTC/USDT", "long"), { candleLimit: 100, htf: { dailyTimeframe: "1d" } })) + ).rejects.toThrow(/must declare dailyTimeframe and weeklyTimeframe/); + }); +}); + +describe("DIR-GOV D-DIR-3 — no submitted or declared direction reaches any bias or axis input", () => { + it("NEGATIVE TEST: varying facts.direction over long/short/neutral leaves the ENTIRE technical payload byte-identical", async () => { + const [long, short, neutral] = await Promise.all( + (["long", "short", "neutral"] as const).map(async (d) => JSON.stringify(await run("BTC/USDT", d))) + ); + expect(short).toBe(long); + expect(neutral).toBe(long); + }); + + it("NEGATIVE TEST: the bias facts are a function of the fetched windows alone — same symbol, opposite submitted sides, identical htf", async () => { + const up = await run("BTC/USDT", "short"); // submitted SHORT on a symbol whose HTF windows trend UP + expect(up.technical.htf!.daily!.trendBias).toBe("bullish"); + expect(up.technical.htf!.weekly!.trendBias).toBe("bullish"); + const down = await run("ETH/USD", "long"); // submitted LONG on a symbol whose weekly window trends DOWN + expect(down.technical.htf!.weekly!.trendBias).toBe("bearish"); + expect(down.technical.htf!.daily!.trendBias).toBe("bullish"); + }); + + it("SOURCE SCAN: no producer or projection file reads the signal's direction", () => { + const files = [ + "src/enrichment/technicalIndicators.ts", + "src/enrichment/candleStructure.ts", + "src/enrichment/tradePlanVerification.ts", + "src/providers/adapters/technicalLocalAdapter.ts", + "src/pipeline/nodes/laneView.ts", + "src/pipeline/nodes/mergeEnrichedView.ts", + "src/pipeline/nodes/scorerFroggyTrendPullback.ts", + ]; + // The submitted side lives at facts.direction on the canonical signal and + // is read ONLY by the record-stamping seam (graphScoringService), never by + // a producer, a projection, or the scorer node. + // Comments naming the rule are not reads: strip block and line comments + // before scanning, so the guard tests CODE, not prose. + const stripComments = (src: string): string => + src.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " "); + const READS_SUBMITTED_DIRECTION = + /facts\s*[?.]*\.\s*direction|facts\s*[?.]*\s*\[\s*["']direction["']\s*\]|signal\s*[?.]*\.\s*direction/; + const offenders = files.filter((rel) => + READS_SUBMITTED_DIRECTION.test(stripComments(readFileSync(path.resolve(process.cwd(), rel), "utf-8"))) + ); + expect(offenders).toEqual([]); + + // The scan is proven to BITE: the same pattern fires on a real read… + expect(READS_SUBMITTED_DIRECTION.test(stripComments("const d = ctx.signal.facts.direction;"))).toBe(true); + expect(READS_SUBMITTED_DIRECTION.test(stripComments('const d = signal.facts?.["direction"];'))).toBe(true); + // …and NOT on a comment that merely names it. + expect(READS_SUBMITTED_DIRECTION.test(stripComments("// never facts.direction (DIR-GOV D-DIR-3)"))).toBe(false); + // The lawful reader — the record-stamping seam — is outside the scanned set. + const stamp = readFileSync(path.resolve(process.cwd(), "src/services/graphScoringService.ts"), "utf-8"); + expect(READS_SUBMITTED_DIRECTION.test(stripComments(stamp))).toBe(true); + }); +}); diff --git a/test/pipeline/officialArtifactPins.test.ts b/test/pipeline/officialArtifactPins.test.ts index 8389c42f..950936bb 100644 --- a/test/pipeline/officialArtifactPins.test.ts +++ b/test/pipeline/officialArtifactPins.test.ts @@ -31,8 +31,8 @@ import { FIXTURE_CONFIG_ROOT } from "./support/testHarness.js"; const PINS = { manifestHash: "df3372dadaca1595d0e6d2f6bad9464ccc9abb7106e9f5b7111df148a145bc4f", - analystConfigHash: "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99", - pluginSetHash: "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471", + analystConfigHash: "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1", + pluginSetHash: "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419", }; function readJson(path: string): unknown { diff --git a/test/pipeline/registryLoader.test.ts b/test/pipeline/registryLoader.test.ts index e850c189..a0d9b3ff 100644 --- a/test/pipeline/registryLoader.test.ts +++ b/test/pipeline/registryLoader.test.ts @@ -90,10 +90,10 @@ describe("validateRuntimeConfig — positive resolution over the fixture registr "df3372dadaca1595d0e6d2f6bad9464ccc9abb7106e9f5b7111df148a145bc4f" ); expect(froggy.analystConfigHash.value).toBe( - "300783e4f93ae07b2d758a3780b03cfbf6e4742bb4e0e5c9e06fb54c1df1ff99" + "71de40d255a22e50d9b353f05722f560fc03911c8bd2daffb974f068060aa4b1" ); expect(froggy.pluginSetHash.value).toBe( - "5eef1faf93d0a1fd5f616fd19a98a10da514afa73141aaddd77e308290a72471" + "59ff54379d44ec250d924a5000f3f85a2e041fb1b12bc5c424d877fb2e2aa419" ); expect(froggy.decay).toEqual({ kind: "ratio", diff --git a/test/support/deterministicPriceFeedAdapter.ts b/test/support/deterministicPriceFeedAdapter.ts index 334c51cc..1f71a723 100644 --- a/test/support/deterministicPriceFeedAdapter.ts +++ b/test/support/deterministicPriceFeedAdapter.ts @@ -91,9 +91,17 @@ class DemoPriceFeedAdapter implements PriceFeedAdapter { const candles: OHLCVCandle[] = []; const timeframeMs = this.parseTimeframeToMs(timeframe); + // DEM-PRODUCER-HTF: higher-timeframe windows (>= 1d) carry a deterministic + // per-symbol DRIFT so the EMA trend law resolves to a real direction on + // them — the signal-timeframe windows (< 1d) keep their exact + // pre-existing i.i.d. shape, so every committed golden's own-timeframe + // bytes are untouched by this change. + const driftPerBar = this.driftForSymbol(symbol, timeframeMs); + for (let i = limit - 1; i >= 0; i--) { const timestamp = DEMO_TIME_ANCHOR_MS - i * timeframeMs; - const open = basePrice + (rand() - 0.5) * basePrice * 0.02; + const trend = basePrice * driftPerBar * (limit - 1 - i); + const open = basePrice + trend + (rand() - 0.5) * basePrice * 0.02; const close = open + (rand() - 0.5) * open * 0.01; const high = Math.max(open, close) + rand() * open * 0.005; const low = Math.min(open, close) - rand() * open * 0.005; @@ -146,6 +154,21 @@ class DemoPriceFeedAdapter implements PriceFeedAdapter { return 100; // Default } + /** + * Deterministic per-bar drift, applied ONLY to timeframes of one day or + * more (DEM-PRODUCER-HTF): BTC trends up, ETH trends down, everything else + * stays flat, so the fixture corpus exercises aligned, conflicting and + * neutral higher-timeframe bias without any wall-clock or randomness. + */ + private driftForSymbol(symbol: string, timeframeMs: number): number { + if (timeframeMs < 24 * 60 * 60 * 1000) return 0; + const s = symbol.toUpperCase(); + const weekly = timeframeMs >= 7 * 24 * 60 * 60 * 1000; + if (s.includes("BTC")) return 0.0015; + if (s.includes("ETH")) return weekly ? -0.0015 : 0.0015; // weekly down, daily up: a CONFLICT + return 0; + } + /** * Parse timeframe string to milliseconds */ From fb2dc34fc900a50ded317d0fe1e389c33b1ab53e Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Tue, 25 Aug 2026 19:57:13 -0700 Subject: [PATCH 5/5] DEM-PLACEHOLDER-GUARD (reactor): the guard over src/pipeline/nodes/** MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEM-GOV §9 DEM-PLACEHOLDER-GUARD (owner-authorized 2026-08-25; afi-governance #55), enforcing D-DEM-4(5). Merge LAST, after afi-core dem/guard-core. test/pipeline/placeholderLiteralGuard.test.ts (NEW, 9 tests) runs the afi-core guard over this repo's bound, src/pipeline/nodes/**, plus the residue checks the gate's last clause requires: - the bound is CLEAN — the D-DEM-4(2) inventory is empty; - NEGATIVE TEST on the exact retired pin: a faithful reconstruction of laneView.ts:79 as it stood before DEM-PRODUCER-CANDLE — the stub imported ACROSS the package boundary — is caught, and the finding's reason names the literal type, proving it was the type checker (not syntax) that saw it; - three more negative tests for the retired haFlatBackConfirmed literal, the neutral bias literal and the R:R synthesis; - proof it does NOT fire on a projected lane fact, on liquiditySwept, or on a value equal to a registered mapping default; - an inventory sweep over the whole bound (comment-stripped, so documentation naming a retired constant is not mistaken for code) and a D-FLPR-6 residue sweep: no shim, alias, dual-run mode, fallback flag or commented-out copy; - a check that the file sits under a glob jest actually runs — the gate is only real if CI runs it, and no testMatch amendment is needed (DEM-GOV §8). Zero scored-value movement by this slot: no golden byte moves, no registry byte moves, no source file outside test/ is touched. 794/794 green. Co-Authored-By: Claude Fable 5 --- test/pipeline/placeholderLiteralGuard.test.ts | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 test/pipeline/placeholderLiteralGuard.test.ts diff --git a/test/pipeline/placeholderLiteralGuard.test.ts b/test/pipeline/placeholderLiteralGuard.test.ts new file mode 100644 index 00000000..5890a36c --- /dev/null +++ b/test/pipeline/placeholderLiteralGuard.test.ts @@ -0,0 +1,153 @@ +/** + * DEM-PLACEHOLDER-GUARD — the terminal CI guard over + * afi-reactor/src/pipeline/nodes/** (D-DEM-4(5); DEM-GOV §9, owner-authorized + * 2026-08-25). The afi-core half of the same guard covers + * afi-core/analysts/** and lives at + * afi-core/test/guardrails/placeholderLiteralGuard.test.ts. + * + * Gate: "Guard proven to fail on a reintroduced literal by a negative test, + * and proven NOT to fire on a registered mapping's declared literals or on + * liquiditySwept; the D-DEM-4(2) inventory empty; zero scored-value movement + * by this slot alone; no compatibility shim, alias, dual-run mode, fallback + * flag, or commented-out copy remains." + * + * This bound is where the retired `BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB` pin + * lived (laneView.ts:79) — a constant IMPORTED from another package, whose + * initializer a syntax-only scan cannot see. The guard decides constancy with + * the type checker, so that exact form is caught; the negative test below + * proves it on a reconstruction of the retired line. + */ +import { describe, it, expect } from "@jest/globals"; +import path from "node:path"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +// @ts-ignore — afi-core subpath types resolve via package exports; jest maps to source +import { scanForPlaceholderLiterals } from "afi-core/validators/PlaceholderLiteralGuard.js"; + +const REPO_ROOT = process.cwd(); +const BOUNDS = ["src/pipeline/nodes"]; // D-DEM-4(5)'s afi-reactor bound +const FIXTURE_MAPPING = path.join( + REPO_ROOT, + "test/pipeline/fixtures/afi-config/registries/enrichment-mappings" +); + +/** The registered mapping's declared defaults (D-DEM-4(5)'s stated exemption). */ +function registeredDefaults(): Record { + const newest = readdirSync(FIXTURE_MAPPING) + .filter((f) => f.endsWith(".json")) + .sort() + .pop()!; + const doc = JSON.parse(readFileSync(path.join(FIXTURE_MAPPING, newest), "utf-8")) as { + bindings: Record; + }; + const out: Record = {}; + for (const [target, b] of Object.entries(doc.bindings)) { + if (b.optionality?.default !== undefined) out[target] = b.optionality.default; + } + return out; +} + +const scan = (extraSources?: Array<{ fileName: string; content: string }>) => + scanForPlaceholderLiterals({ + tsconfigPath: path.join(REPO_ROOT, "tsconfig.json"), + bounds: BOUNDS, + repoRoot: REPO_ROOT, + mappingDeclaredDefaults: registeredDefaults(), + extraSources, + }); + +const fixture = (body: string) => [ + { fileName: path.join(REPO_ROOT, "src/pipeline/nodes/__guard_fixture__.ts"), content: body }, +]; + +describe("DEM-PLACEHOLDER-GUARD: the reactor's production path carries no placeholder literal", () => { + it("src/pipeline/nodes/** is CLEAN — the D-DEM-4(2) inventory is empty", async () => { + expect(await scan()).toEqual([]); + }, 120_000); + + it("NEGATIVE TEST: the retired laneView pin is caught — a constant imported ACROSS the package boundary", async () => { + // A faithful reconstruction of afi-reactor/src/pipeline/nodes/laneView.ts:79 + // as it stood before DEM-PRODUCER-CANDLE. The initializer lives in another + // package's declarations, so only the type checker can see it is constant. + const findings = await scan([ + { + fileName: path.join(REPO_ROOT, "src/pipeline/nodes/__guard_stub__.ts"), + content: `export const BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB = false as const;\n`, + }, + { + fileName: path.join(REPO_ROOT, "src/pipeline/nodes/__guard_fixture__.ts"), + content: + `import { BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB } from "./__guard_stub__.js";\n` + + `export function viewTechnicalLike() {\n` + + ` return { brokeEmaWithBody: BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB };\n` + + `}\n`, + }, + ]); + const hit = findings.find((f) => f.field === "brokeEmaWithBody"); + expect(hit).toBeDefined(); + expect(hit!.reason).toMatch(/literal type/); + }, 120_000); + + it.each([ + ["the retired haFlatBackConfirmed literal", `export const a = { haFlatBackConfirmed: false };`, "haFlatBackConfirmed"], + ["the retired neutral bias literal", `export const b = { weeklyBias: "neutral" as const };`, "weeklyBias"], + ["the retired R:R synthesis", `declare const p: boolean; export const c = { rrMultiplePlanned: p ? 2 : 1 };`, "rrMultiplePlanned"], + ])("NEGATIVE TEST: catches %s", async (_l, body, field) => { + expect((await scan(fixture(body))).map((f) => f.field)).toContain(field); + }, 120_000); + + it("does NOT fire on a projected lane fact, on liquiditySwept, or on a registered default", async () => { + const findings = await scan( + fixture( + `declare const payload: { brokeEmaWithBody?: boolean; haFlatBackConfirmed?: boolean; emaDistancePct?: number };\n` + + `export const view = {\n` + + ` brokeEmaWithBody: payload.brokeEmaWithBody,\n` + + ` haFlatBackConfirmed: payload.haFlatBackConfirmed,\n` + + ` liquiditySwept: false,\n` + + ` distanceFromDailyEmaPct: payload.emaDistancePct ?? 0,\n` + + `};\n` + ) + ); + expect(findings).toEqual([]); + }, 120_000); +}); + +describe("DEM-PLACEHOLDER-GUARD: the D-DEM-4(2) inventory is empty and no residue remains", () => { + const NODES = path.join(REPO_ROOT, "src/pipeline/nodes"); + + function sources(dir: string): string[] { + return readdirSync(dir).flatMap((entry) => { + const abs = path.join(dir, entry); + if (statSync(abs).isDirectory()) return sources(abs); + return abs.endsWith(".ts") ? [abs] : []; + }); + } + + /** Comments naming a retired constant are documentation, not code. */ + const code = (file: string): string => + readFileSync(file, "utf-8").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " "); + + it("every named item of the D-DEM-4(2) inventory is gone from the reactor's node code", () => { + const RETIRED = [ + /BROKE_EMA_WITH_BODY_UNIMPLEMENTED_STUB/, + /haFlatBackConfirmed\s*:\s*(false|true)\b/, + /(weekly|daily)Bias\s*[:=]\s*"(neutral|long|short)"/, + /rrMultiplePlanned\s*[:=][^,;}]*\?[^:]*:/, + ]; + const offenders = sources(NODES).filter((f) => RETIRED.some((re) => re.test(code(f)))); + expect(offenders.map((f) => path.relative(REPO_ROOT, f))).toEqual([]); + }); + + it("no compatibility shim, alias, dual-run mode, fallback flag, or commented-out copy remains (D-FLPR-6)", () => { + const BANNED = /\b(dualRun|legacyPath|fallbackFlag|LEGACY_[A-Z_]+|_deprecatedCopy)\b/; + const offenders = sources(NODES).filter((f) => BANNED.test(readFileSync(f, "utf-8"))); + expect(offenders.map((f) => path.relative(REPO_ROOT, f))).toEqual([]); + }); + + it("the guard is wired into a suite jest actually runs (the gate is only real if CI runs it)", () => { + const cfg = readFileSync(path.join(REPO_ROOT, "jest.config.js"), "utf-8"); + // test/pipeline/** is a matched glob, so this file runs without any + // testMatch amendment (DEM-GOV §8: an amendment is authorized only for a + // slot that adds a test requiring one — this slot does not). + expect(cfg).toContain("**/test/pipeline/**/*.test.ts"); + }); +});