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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions validators/SignalDecay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,34 @@
import { decay } from "@afi-protocol/afi-math";

/**
* Default half-life for signal decay (in hours).
*
* Signals lose 50% of their value after this many hours.
* Default: 24 hours (1 day)
*
* TODO: Source from governance config (afi-config)
* DLC-GOV D-DLC-3(2): the silent 24h default (`DEFAULT_SIGNAL_HALF_LIFE_HOURS`)
* is RETIRED — a half-life a caller never chose is never quietly substituted.
* Every entry point below requires the half-life explicitly. The canonical,
* minutes-based production derivation is `applyTimeDecay` in `src/decay`
* (KAT-bound to afi-config's governed 32-vector apply-time-decay KAT);
* this hour-unit wrapper survives only for the dormant validator surface
* (ValidatorDecision), which always passes explicit parameters.
*/
export const DEFAULT_SIGNAL_HALF_LIFE_HOURS = 24;

/**
* Apply time-based decay to a UWR score.
*
*
* Formula: decayed_score = uwr_score * e^(-λ * age)
* where λ = ln(2) / half_life
*
*
* SUPERSEDED for production use by the minutes-based `applyTimeDecay`
* (`src/decay`, DLC-GOV D-DLC-1) — hour-unit, explicit-parameter wrapper
* kept for the dormant validator surface only (D-DLC-3(2)).
*
* @param uwrScore - Base UWR score in [0, 1]
* @param ageHours - Signal age in hours
* @param halfLifeHours - Half-life in hours (default: 24)
* @param halfLifeHours - Half-life in hours (required — no default)
* @returns Time-decayed score in [0, 1]
*/
export function applyTimeDecayToUwrScore(
uwrScore: number,
ageHours: number,
halfLifeHours: number = DEFAULT_SIGNAL_HALF_LIFE_HOURS
halfLifeHours: number
): number {
return decay.timeWeightedScore({
baseScore: uwrScore,
Expand Down Expand Up @@ -81,15 +85,18 @@ export function calculateAdjustedHalfLife(
* @param ageHours - Signal age in hours
* @param volatility - Volatility factor (1.0 = normal)
* @param conviction - Conviction factor (1.0 = normal)
* @param baseHalfLifeHours - Base half-life in hours
* @param baseHalfLifeHours - Base half-life in hours (required — no default,
* D-DLC-3(2); note this adjusted-half-life family is expressly NOT wired
* by DLC-GOV D-DLC-3(4) — an adjusted half-life is a different declared
* parameter, introducible only by a future filing)
* @returns Volatility-adjusted decayed score in [0, 1]
*/
export function applyVolatilityAdjustedDecay(
uwrScore: number,
ageHours: number,
volatility: number,
conviction: number,
baseHalfLifeHours: number = DEFAULT_SIGNAL_HALF_LIFE_HOURS
baseHalfLifeHours: number
): number {
const adjustedHalfLife = calculateAdjustedHalfLife(
baseHalfLifeHours,
Expand Down
7 changes: 6 additions & 1 deletion validators/ValidatorDecision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ export function computeValidatorScore(

// Calculate adjusted half-life based on conviction
// Higher conviction → longer half-life (signal stays relevant longer)
const baseHalfLifeHours = 24; // Default 24 hours
// DLC-GOV D-DLC-3(2) conformance: every half-life here is an EXPLICIT
// parameter of this dormant surface — no silent library default is
// reachable. The canonical live derivation is src/decay applyTimeDecay,
// driven by the determination's stamped decayParams (D-DLC-1); the
// adjusted-half-life family stays unwired per D-DLC-3(4).
const baseHalfLifeHours = 24; // this module's own declared base (hours)
const adjustedHalfLife = calculateAdjustedHalfLife(
baseHalfLifeHours,
volatility,
Expand Down
26 changes: 13 additions & 13 deletions validators/__tests__/SignalDecay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import {
applyTimeDecayToUwrScore,
calculateAdjustedHalfLife,
applyVolatilityAdjustedDecay,
remainingAfterHalfLives,
DEFAULT_SIGNAL_HALF_LIFE_HOURS
remainingAfterHalfLives
} from "../SignalDecay.js";

describe("SignalDecay - Time-based UWR Decay", () => {
describe("applyTimeDecayToUwrScore", () => {
it("should not decay a fresh signal (age = 0)", () => {
const uwrScore = 0.8;
const decayed = applyTimeDecayToUwrScore(uwrScore, 0);
const decayed = applyTimeDecayToUwrScore(uwrScore, 0, 24);

expect(decayed).toBe(0.8);
});
Expand Down Expand Up @@ -54,13 +53,13 @@ describe("SignalDecay - Time-based UWR Decay", () => {
expect(decayed).toBeGreaterThan(uwrScore * 0.5);
});

it("should use default half-life of 24 hours", () => {
it("requires an explicit half-life — the silent 24h default is retired (DLC-GOV D-DLC-3(2))", () => {
const uwrScore = 0.8;
const ageHours = 24;
const decayed = applyTimeDecayToUwrScore(uwrScore, ageHours);
// After 24 hours with default half-life, should be 0.8 * 0.5 = 0.4

const decayed = applyTimeDecayToUwrScore(uwrScore, ageHours, 24);

// After 24 hours with an EXPLICIT 24h half-life: 0.8 * 0.5 = 0.4
expect(decayed).toBeCloseTo(0.4, 6);
});
});
Expand Down Expand Up @@ -98,8 +97,8 @@ describe("SignalDecay - Time-based UWR Decay", () => {
const uwrScore = 0.8;
const ageHours = 24;

const normalDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 1.0, 1.0);
const highVolDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 2.0, 1.0);
const normalDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 1.0, 1.0, 24);
const highVolDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 2.0, 1.0, 24);

// High volatility should cause more decay
expect(highVolDecay).toBeLessThan(normalDecay);
Expand All @@ -109,8 +108,8 @@ describe("SignalDecay - Time-based UWR Decay", () => {
const uwrScore = 0.8;
const ageHours = 24;

const normalDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 1.0, 1.0);
const highConvictionDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 1.0, 2.0);
const normalDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 1.0, 1.0, 24);
const highConvictionDecay = applyVolatilityAdjustedDecay(uwrScore, ageHours, 1.0, 2.0, 24);

// High conviction should cause less decay
expect(highConvictionDecay).toBeGreaterThan(normalDecay);
Expand All @@ -126,7 +125,8 @@ describe("SignalDecay - Time-based UWR Decay", () => {
uwrScore,
ageHours,
volatility,
conviction
conviction,
24
);

// Should be between 0 and original score
Expand Down
Loading