feat: show FIP path-smoothness in ticker technicals
Display-only Da/Gurun/Warachka information discreteness on the ticker indicator panel. Shared compute with the backtest harness; not wired into gate or rank.
This commit is contained in:
@@ -819,39 +819,14 @@ def _realized_vol_6m(closes: list[float], i: int) -> float | None:
|
||||
|
||||
|
||||
def _fip_id(closes: list[float], i: int) -> float | None:
|
||||
"""Da/Gurun/Warachka information discreteness over the 12-1 formation window.
|
||||
"""Point-in-time FIP ID for the signal harness; delegates to indicator_service."""
|
||||
from app.services.indicator_service import compute_fip_id
|
||||
from app.exceptions import ValidationError
|
||||
|
||||
Formation matches ``mom_12_1``: cumulative return from close[i-252] to
|
||||
close[i-21] (231 daily returns ending one month before as-of).
|
||||
|
||||
ID = sign(PRET) × (%neg − %pos)
|
||||
|
||||
where %pos / %neg are fractions of up / down days over the formation window
|
||||
(zero-return days count in neither numerator, but remain in the denominator).
|
||||
Lower ID = smoother / more continuous path → expect negative cross-sectional
|
||||
IC (continuous-information winners outperform).
|
||||
"""
|
||||
if i - 252 < 0 or closes[i - 252] <= 0 or closes[i - 21] <= 0:
|
||||
try:
|
||||
return float(compute_fip_id(closes, as_of_index=i)["fip_id"])
|
||||
except (ValidationError, KeyError, TypeError, ValueError):
|
||||
return None
|
||||
pret = closes[i - 21] / closes[i - 252] - 1.0
|
||||
rets: list[float] = []
|
||||
for k in range(i - 251, i - 20):
|
||||
prev = closes[k - 1]
|
||||
if prev <= 0:
|
||||
return None
|
||||
rets.append(closes[k] / prev - 1.0)
|
||||
if len(rets) < 200:
|
||||
return None
|
||||
n = len(rets)
|
||||
pct_pos = sum(1 for r in rets if r > 0) / n
|
||||
pct_neg = sum(1 for r in rets if r < 0) / n
|
||||
if pret > 0:
|
||||
sign = 1.0
|
||||
elif pret < 0:
|
||||
sign = -1.0
|
||||
else:
|
||||
sign = 0.0
|
||||
return sign * (pct_neg - pct_pos)
|
||||
|
||||
|
||||
def _signal_values(
|
||||
|
||||
@@ -28,6 +28,7 @@ MIN_BARS: dict[str, int] = {
|
||||
"atr": 15,
|
||||
"volume_profile": 20,
|
||||
"pivot_points": 5,
|
||||
"fip_id": 253, # 12-1 formation: need index i-252
|
||||
}
|
||||
|
||||
DEFAULT_PERIODS: dict[str, int] = {
|
||||
@@ -407,6 +408,71 @@ def compute_pivot_points(
|
||||
}
|
||||
|
||||
|
||||
def compute_fip_id(closes: list[float], as_of_index: int | None = None) -> dict[str, Any]:
|
||||
"""Da/Gurun/Warachka information discreteness over the 12-1 formation window.
|
||||
|
||||
Display / research context only — **not** used by the activation gate or
|
||||
production rank. Same window as residual 12-1 momentum: cumulative return
|
||||
from close[i-252] to close[i-21] (skip last month).
|
||||
|
||||
ID = sign(PRET) × (%neg − %pos)
|
||||
|
||||
Lower ID ⇒ smoother / more continuous path (for a winner: many small up days).
|
||||
Higher ID ⇒ jumpy / discrete path (few large moves). Zero-return days count
|
||||
in neither numerator but remain in the denominator.
|
||||
"""
|
||||
i = len(closes) - 1 if as_of_index is None else as_of_index
|
||||
if i < 252 or closes[i - 252] <= 0 or closes[i - 21] <= 0:
|
||||
raise ValidationError(
|
||||
f"FIP ID requires at least 253 bars with positive formation closes, "
|
||||
f"got {len(closes)}"
|
||||
)
|
||||
pret = closes[i - 21] / closes[i - 252] - 1.0
|
||||
rets: list[float] = []
|
||||
for k in range(i - 251, i - 20):
|
||||
prev = closes[k - 1]
|
||||
if prev <= 0:
|
||||
raise ValidationError("FIP ID requires positive closes in the formation window")
|
||||
rets.append(closes[k] / prev - 1.0)
|
||||
if len(rets) < 200:
|
||||
raise ValidationError(
|
||||
f"FIP ID requires ≥200 daily returns in formation, got {len(rets)}"
|
||||
)
|
||||
n = len(rets)
|
||||
pct_pos = sum(1 for r in rets if r > 0) / n
|
||||
pct_neg = sum(1 for r in rets if r < 0) / n
|
||||
if pret > 0:
|
||||
sign = 1.0
|
||||
elif pret < 0:
|
||||
sign = -1.0
|
||||
else:
|
||||
sign = 0.0
|
||||
fip = sign * (pct_neg - pct_pos)
|
||||
# Map [-1, 1] → score where lower ID (smoother) is higher for display only.
|
||||
score = max(0.0, min(100.0, 50.0 * (1.0 - fip)))
|
||||
if fip <= -0.25:
|
||||
path = "continuous"
|
||||
path_label = "smooth grind (continuous information)"
|
||||
elif fip >= 0.25:
|
||||
path = "discrete"
|
||||
path_label = "jumpy path (discrete information)"
|
||||
else:
|
||||
path = "mixed"
|
||||
path_label = "mixed path"
|
||||
return {
|
||||
"fip_id": round(fip, 4),
|
||||
"pret_12_1": round(pret, 4),
|
||||
"pct_up_days": round(pct_pos * 100.0, 1),
|
||||
"pct_down_days": round(pct_neg * 100.0, 1),
|
||||
"formation_days": n,
|
||||
"path": path,
|
||||
"path_label": path_label,
|
||||
"display_only": True,
|
||||
"note": "Not used by the production gate or rank — context only.",
|
||||
"score": round(score, 4),
|
||||
}
|
||||
|
||||
|
||||
def compute_ema_cross(
|
||||
closes: list[float],
|
||||
short_period: int = 20,
|
||||
@@ -451,7 +517,15 @@ def compute_ema_cross(
|
||||
# Supported indicator types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
INDICATOR_TYPES = {"adx", "ema", "rsi", "atr", "volume_profile", "pivot_points"}
|
||||
INDICATOR_TYPES = {
|
||||
"adx",
|
||||
"ema",
|
||||
"rsi",
|
||||
"atr",
|
||||
"volume_profile",
|
||||
"pivot_points",
|
||||
"fip_id",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -514,6 +588,8 @@ async def get_indicator(
|
||||
result = compute_volume_profile(highs, lows, closes, volumes)
|
||||
elif indicator_type == "pivot_points":
|
||||
result = compute_pivot_points(highs, lows, closes)
|
||||
elif indicator_type == "fip_id":
|
||||
result = compute_fip_id(closes)
|
||||
else:
|
||||
raise ValidationError(f"Unknown indicator type: {indicator_type}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user