"""blindlib — the ONLY module a blind-signal agent imports. It hands you anonymized OVERLAID price curves ("Series A", "Series B") and an HONEST, leak-free evaluator. You never touch the real-data loaders, you never learn the tickers. Your job: write a CAUSAL `signal(df) -> position[]` that anticipates the move, tune it on the TRAIN view, and report PnL + max drawdown. THE CONTRACT (read carefully — the orchestrator enforces it automatically): * `signal(df)` returns a float array len(df). position[i] in [-1, +1] is the fraction of equity you want to hold during the NEXT bar (sign = long/short, 0 = flat). The evaluator SHIFTS it for you (held during bar i+1), so you can NEVER leak by multiplying a weight by the same bar's return. * It must be ONLINE / CAUSAL: position[i] may use ONLY rows 0..i of df. No `.shift(-k)`, no centered windows, no fitting a model on the whole df then predicting the whole df (at test time that df CONTAINS the held-out future). -> Verified by `causality_ok()`: we call signal on a truncated prefix and require the tail to match signal on the full array. A leaky signal is DISQUALIFIED. * Fees are real (Deribit 0.10% round-trip = 0.0005/side) and charged on turnover. The metrics that decide validity (orchestrator ranks on these): * pnl = total net return over the period (final/initial - 1) <- "PNL" * maxdd = worst peak-to-trough drawdown of the equity curve <- "DD max" (sharpe / cagr / turnover reported for context.) Toolkit: causal indicators are re-exported from the project's vetted altlib so you don't reinvent (or mis-implement) them. All are causal (value at i uses data <= i). Typical agent usage: import blindlib as bl df = bl.load("A", "train") # anonymized training curve for Series A def signal(df): c = df["close"].values mom = c / bl.sma(c, 50) - 1.0 # causal return np.tanh(3.0 * mom) # position in [-1,1] print(bl.evaluate(signal, "A", "train")) # {pnl, maxdd, sharpe, ...} """ from __future__ import annotations import json import sys from pathlib import Path import numpy as np import pandas as pd _BLIND_DIR = Path("/opt/docker/PythagorasGoal/data/blind") sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") # Re-export causal indicators + the vol-targeting helper + the net->metrics core. # (These are pure math; they reveal nothing about the underlying asset.) from altlib import ( # noqa: E402 simple_returns, log_returns, ema, sma, rolling_std, zscore, rsi, atr, realized_vol, donchian, bbands, vol_target, bars_per_day, bars_per_year, _metrics_from_net, ) FEE_SIDE = 0.0005 # 0.05%/side = 0.10% round-trip (Deribit taker) SERIES = ("A", "B") # --------------------------------------------------------------------------- # DATA — anonymized loaders. "train" = agent-visible. "full"/"test" = orchestrator. # --------------------------------------------------------------------------- def _meta() -> dict: return json.loads((_BLIND_DIR / "blind_meta.json").read_text()) def load(series: str, split: str = "train") -> pd.DataFrame: """Anonymized OHLCV curve. split: 'train' (first 70%, what you tune on) | 'full' (whole series) | 'test' (held-out tail only — for inspection; you should NOT tune on it). datetime is synthetic daily.""" series = series.upper() if series not in SERIES: raise ValueError(f"Unknown series {series}; pick from {SERIES}") if split == "train": df = pd.read_parquet(_BLIND_DIR / f"blind_{series}_train.parquet") else: df = pd.read_parquet(_BLIND_DIR / f"blind_{series}_full.parquet") if split == "test": cut = int(len(df) * _meta()["split_frac"]) df = df.iloc[cut:].reset_index(drop=True) return df.reset_index(drop=True) def split_cut(series: str) -> int: df = pd.read_parquet(_BLIND_DIR / f"blind_{series.upper()}_full.parquet") return int(len(df) * _meta()["split_frac"]) # --------------------------------------------------------------------------- # EVALUATION — leak-free (position shifted), fee on turnover, PnL + maxDD. # --------------------------------------------------------------------------- def eval_target(df: pd.DataFrame, target: np.ndarray, fee_side: float = FEE_SIDE, metric_mask: np.ndarray | None = None) -> dict: """Backtest a per-bar position series on df. target[i] decided at close[i] is HELD during bar i+1 (shift done here). Fee on |Δposition|. If metric_mask is given, metrics are computed only on those bars (used for OOS = test slice).""" c = df["close"].values.astype(float) target = np.nan_to_num(np.asarray(target, float), nan=0.0) target = np.clip(target, -1.0, 1.0) r = simple_returns(c) pos = np.zeros(len(target)) pos[1:] = target[:-1] # held during bar t = decided at t-1 gross = pos * r turn = np.abs(np.diff(pos, prepend=0.0)) net = gross - fee_side * turn net[0] = 0.0 idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) if metric_mask is not None: net_m, idx_m = net[metric_mask], idx[metric_mask] else: net_m, idx_m = net, idx m = _metrics_from_net(net_m, idx_m) bpy_d = bars_per_day(df) * 365.25 tin = float(np.mean(pos[metric_mask] != 0)) if metric_mask is not None else float(np.mean(pos != 0)) turn_m = turn[metric_mask].sum() if metric_mask is not None else turn.sum() span = max(len(net_m) / bpy_d, 1e-9) return dict(pnl=round(m["ret"], 4), maxdd=round(m["maxdd"], 4), sharpe=round(m["sharpe"], 3), cagr=round(m["cagr"], 4), n_bars=int(len(net_m)), time_in_market=round(tin, 3), turnover_per_year=round(float(turn_m / span), 1), net=net, idx=idx) def evaluate(signal_fn, series: str, split: str = "train", fee_side: float = FEE_SIDE) -> dict: """Run signal_fn on the chosen view and return {pnl, maxdd, sharpe, ...}. train: signal sees only train rows, metrics over train. test : signal sees the FULL series (proper warmup) but metrics ONLY on the held-out tail -> the honest out-of-sample PnL/DD. (orchestrator use) full : signal + metrics over the whole series. """ if split == "train": df = load(series, "train") tgt = np.asarray(signal_fn(df), float) rep = eval_target(df, tgt, fee_side) else: df = load(series, "full") tgt = np.asarray(signal_fn(df), float) mask = None if split == "test": cut = split_cut(series) mask = np.zeros(len(df), bool); mask[cut:] = True rep = eval_target(df, tgt, fee_side, metric_mask=mask) rep.pop("net", None); rep.pop("idx", None) return rep # --------------------------------------------------------------------------- # CAUSALITY GUARD — disqualifies look-ahead. Online-consistency: signal on a # prefix must agree (on its tail) with signal on the full array. A function that # uses future rows, centered windows, or fits globally on the input will diverge. # --------------------------------------------------------------------------- def causality_ok(signal_fn, series: str = "A", split: str = "full", tail: int = 60, tol: float = 1e-4) -> dict: """Returns {ok, max_diff, frac_bad, checked_at}. We truncate the input at two late cut points and require signal(df[:cut]) to match signal(df)[:cut] over the last `tail` bars before each cut (the bars a deployable signal would have emitted in real time).""" df = load(series, split) full = np.nan_to_num(np.asarray(signal_fn(df), float), nan=0.0) n = len(df) cuts = [int(n * 0.80), int(n * 0.92)] max_diff = 0.0; frac_bad = 0.0; checked = [] for cut in cuts: if cut <= tail + 5 or cut >= n: continue sub = np.nan_to_num(np.asarray(signal_fn(df.iloc[:cut].reset_index(drop=True)), float), nan=0.0) if len(sub) != cut: return dict(ok=False, reason=f"signal returned len {len(sub)} != {cut} on prefix", max_diff=9.99, frac_bad=1.0, checked_at=cut) a = sub[cut - tail:cut] b = full[cut - tail:cut] d = np.abs(a - b) max_diff = max(max_diff, float(np.max(d)) if len(d) else 0.0) frac_bad = max(frac_bad, float(np.mean(d > tol)) if len(d) else 0.0) checked.append(cut) ok = (max_diff <= max(tol * 10, 1e-3)) and (frac_bad <= 0.02) return dict(ok=bool(ok), max_diff=round(max_diff, 6), frac_bad=round(frac_bad, 4), checked_at=checked) __all__ = [ "load", "split_cut", "evaluate", "eval_target", "causality_ok", "FEE_SIDE", "SERIES", "simple_returns", "log_returns", "ema", "sma", "rolling_std", "zscore", "rsi", "atr", "realized_vol", "donchian", "bbands", "vol_target", "bars_per_day", "bars_per_year", ]