Instructions to use poolside-laguna-hackathon/trade-pool with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use poolside-laguna-hackathon/trade-pool with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| """Causal replay backtester β the RL verifier's beating heart. | |
| Ports tradewatch's paper_ledger.py accounting into a vectorised, deterministic, | |
| LEAK-PROOF backtest. A strategy is a pure function: | |
| def strategy(features_up_to_t: dict[str, float], position: float) -> float | |
| # returns target position in [-1, 1] (fraction of equity; <0 = short) | |
| The engine calls it bar-by-bar, passing ONLY features at indices <= t. The strategy | |
| can never see the future because future bars are not in the dict it receives. Fills | |
| happen at the NEXT bar's open-equivalent (close[t] used as decision price, close[t+1] | |
| as fill realisation) with slippage + fees, so a decision at t cannot be filled at t's | |
| hindsight-perfect price. | |
| Metrics: Sharpe (primary), CAGR, max drawdown, equity/cash exposure, turnover, win rate. | |
| Benchmarks: buy-and-hold, MA-crossover, z-score mean-reversion. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| import numpy as np | |
| from .features import feature_frame, sma, zscore | |
| class BacktestResult: | |
| equity_curve: np.ndarray | |
| returns: np.ndarray | |
| sharpe: float | |
| cagr: float | |
| max_drawdown: float | |
| turnover: float | |
| win_rate: float | |
| final_equity: float | |
| avg_exposure: float | |
| n_trades: int | |
| error: str | None = None | |
| meta: dict = field(default_factory=dict) | |
| def to_dict(self) -> dict: | |
| return { | |
| "sharpe": round(self.sharpe, 4), | |
| "cagr": round(self.cagr, 4), | |
| "max_drawdown": round(self.max_drawdown, 4), | |
| "turnover": round(self.turnover, 4), | |
| "win_rate": round(self.win_rate, 4), | |
| "final_equity": round(self.final_equity, 4), | |
| "avg_exposure": round(self.avg_exposure, 4), | |
| "n_trades": self.n_trades, | |
| "error": self.error, | |
| } | |
| # Periods-per-year for annualisation (daily bars by default; 24*365 for hourly). | |
| def _ann_factor(bars_per_year: float) -> float: | |
| return float(np.sqrt(bars_per_year)) | |
| def _metrics(equity: np.ndarray, exposures: np.ndarray, bars_per_year: float, | |
| error: str | None = None) -> BacktestResult: | |
| equity = np.asarray(equity, dtype=float) | |
| if len(equity) < 2 or np.any(~np.isfinite(equity)) or np.any(equity <= 0): | |
| return BacktestResult(equity, np.zeros(1), 0.0, 0.0, 1.0, 0.0, 0.0, | |
| float(equity[-1]) if len(equity) else 0.0, 0.0, 0, | |
| error=error or "degenerate equity") | |
| rets = np.diff(equity) / equity[:-1] | |
| sd = rets.std() | |
| sharpe = float(rets.mean() / sd * _ann_factor(bars_per_year)) if sd > 1e-12 else 0.0 | |
| n = len(equity) | |
| years = n / bars_per_year | |
| cagr = float((equity[-1] / equity[0]) ** (1 / years) - 1) if years > 0 and equity[0] > 0 else 0.0 | |
| peak = np.maximum.accumulate(equity) | |
| max_dd = float(np.max((peak - equity) / peak)) if len(peak) else 0.0 | |
| exp = np.asarray(exposures, dtype=float) | |
| turnover = float(np.sum(np.abs(np.diff(np.insert(exp, 0, 0.0))))) | |
| n_trades = int(np.sum(np.abs(np.diff(np.insert(exp, 0, 0.0))) > 1e-9)) | |
| win_rate = float(np.mean(rets > 0)) if len(rets) else 0.0 | |
| return BacktestResult(equity, rets, sharpe, cagr, max_dd, turnover, win_rate, | |
| float(equity[-1]), float(np.mean(np.abs(exp))), n_trades, error=error) | |
| def run_backtest( | |
| close: np.ndarray, | |
| strategy_fn, | |
| *, | |
| fee_bps: float = 30.0, # 0.30% per unit turnover (DEX-realistic) | |
| slippage_bps: float = 20.0, # 0.20% adverse slippage on fills | |
| bars_per_year: float = 365.0, | |
| allow_short: bool = True, | |
| warmup: int = 50, # bars reserved for feature warmup (no trading) | |
| ) -> BacktestResult: | |
| """Step the strategy causally over `close`, return performance metrics. | |
| Fill model: decision made on info up to t, position realised against return | |
| from t -> t+1, minus fees/slippage proportional to position change. | |
| """ | |
| close = np.asarray(close, dtype=float) | |
| if len(close) < warmup + 5: | |
| return _metrics(np.array([1.0, 1.0]), np.zeros(1), bars_per_year, error="too few bars") | |
| feats = feature_frame(close) | |
| rets = np.zeros(len(close)) | |
| rets[1:] = np.diff(close) / np.where(close[:-1] == 0, np.nan, close[:-1]) | |
| rets = np.nan_to_num(rets) | |
| equity = [1.0] | |
| exposures = [] | |
| pos = 0.0 | |
| cost_rate = (fee_bps + slippage_bps) / 1e4 | |
| for t in range(warmup, len(close) - 1): | |
| # features visible at t (scalar snapshot, causal by construction) | |
| view = {k: (float(v[t]) if np.isfinite(v[t]) else 0.0) for k, v in feats.items()} | |
| try: | |
| target = float(strategy_fn(view, pos)) | |
| except Exception as e: # noqa: BLE001 - strategy bugs => penalised, not crash | |
| return _metrics(np.array(equity + [equity[-1]]), np.array(exposures + [pos]), | |
| bars_per_year, error=f"strategy raised: {type(e).__name__}: {e}") | |
| if not np.isfinite(target): | |
| target = 0.0 | |
| lo = -1.0 if allow_short else 0.0 | |
| target = max(lo, min(1.0, target)) | |
| trade_cost = abs(target - pos) * cost_rate | |
| # equity evolves with NEXT bar's realised return at the chosen exposure | |
| pnl = target * rets[t + 1] | |
| new_equity = equity[-1] * (1.0 + pnl - trade_cost) | |
| equity.append(new_equity) | |
| exposures.append(target) | |
| pos = target | |
| return _metrics(np.array(equity), np.array(exposures), bars_per_year) | |
| # ββ Benchmarks (the anti-overfit baselines a strategy must beat) ββββββββββββββ | |
| def _buy_and_hold(view, pos): | |
| return 1.0 | |
| def _ma_crossover(view, pos): | |
| return 1.0 if view.get("sma_10", 0) > view.get("sma_20", 0) else 0.0 | |
| def _zscore_meanrev(view, pos): | |
| z = view.get("zscore_20", 0.0) | |
| if z < -1.5: | |
| return 1.0 | |
| if z > 1.5: | |
| return -1.0 | |
| return pos # hold | |
| def benchmarks(close: np.ndarray, **kw) -> dict[str, BacktestResult]: | |
| return { | |
| "buy_and_hold": run_backtest(close, _buy_and_hold, **kw), | |
| "ma_crossover": run_backtest(close, _ma_crossover, **kw), | |
| "zscore_meanrev": run_backtest(close, _zscore_meanrev, **kw), | |
| } | |