Download crypto_eval/grid2.py from Avi-L/crypto-quant-eval: direct link, hf CLI and curl.
- Browser
- Download file 3.42 kB
-
https://huggingface.co/Avi-L/crypto-quant-eval/resolve/main/crypto_eval/grid2.py
- Command line
-
hf download hf://Avi-L/crypto-quant-eval/crypto_eval/grid2.py
-
curl -L -o grid2.py https://huggingface.co/Avi-L/crypto-quant-eval/resolve/main/crypto_eval/grid2.py
3.42 kB
| """Daily multi-asset strategy families for the panel sweep. | |
| All signals use data through t-1: `panel.shift(1)` everywhere. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pandas as pd | |
| def _vol_scale(panel: pd.DataFrame, target: float, window: int) -> pd.DataFrame: | |
| ret = panel.pct_change() | |
| rv = ret.rolling(window).std().shift(1) * np.sqrt(365) | |
| return (target / rv).clip(upper=1.0).fillna(0.0) | |
| def tsmom_panel(panel: pd.DataFrame, lookback: int, long_only: bool = False, | |
| vol_target: float | None = None, | |
| vol_window: int = 60) -> pd.DataFrame: | |
| c = panel.shift(1) | |
| mom = c / c.shift(lookback) - 1 | |
| sig = pd.DataFrame(np.where(mom > 0, 1.0, -1.0), | |
| index=panel.index, columns=panel.columns) | |
| if long_only: | |
| sig = sig.clip(lower=0.0) | |
| if vol_target is not None: | |
| sig = sig * _vol_scale(panel, vol_target, vol_window) | |
| # pre-listing: no data -> flat | |
| return sig.where(panel.notna(), 0.0).fillna(0.0) | |
| def donchian_panel(panel: pd.DataFrame, window: int, long_only: bool = False, | |
| vol_target: float | None = None) -> pd.DataFrame: | |
| # channel uses highs/lows we don't have on the index panel; approximate | |
| # with rolling max/min of closes through t-2 (close-based breakout) | |
| c = panel.shift(1) | |
| upper = panel.rolling(window).max().shift(2) | |
| lower = panel.rolling(window).min().shift(2) | |
| pos = pd.DataFrame(np.nan, index=panel.index, columns=panel.columns) | |
| pos[c > upper] = 1.0 | |
| if long_only: | |
| pos[c < lower] = 0.0 | |
| else: | |
| pos[c < lower] = -1.0 | |
| pos = pos.ffill().fillna(0.0) | |
| if vol_target is not None: | |
| pos = pos * _vol_scale(panel, vol_target, 60) | |
| return pos.where(panel.notna(), 0.0) | |
| def xs_momentum(panel: pd.DataFrame, lookback: int, top_k: int = 2, | |
| short_bottom: bool = False, rebalance: str = "W") -> pd.DataFrame: | |
| """Cross-sectional momentum: long top-k by trailing return, optional | |
| short bottom-k, rebalanced weekly (Mondays) or monthly.""" | |
| c = panel.shift(1) | |
| mom = c / c.shift(lookback) - 1 | |
| ranks = mom.rank(axis=1, ascending=False) | |
| n = panel.shape[1] | |
| tgt = pd.DataFrame(0.0, index=panel.index, columns=panel.columns) | |
| tgt[ranks <= top_k] = 1.0 / top_k | |
| if short_bottom: | |
| tgt[ranks > n - top_k] = -1.0 / top_k | |
| idx = panel.index | |
| if rebalance == "W": | |
| mask = pd.Series(idx.dayofweek == 0, index=idx) | |
| else: # monthly: first listed trading day of each month | |
| mask = idx.to_series().groupby(idx.to_period("M")).transform("min").eq(idx) | |
| pos = tgt.where(mask, np.nan).ffill().fillna(0.0) | |
| return pos.where(panel.notna(), 0.0) | |
| def xs_momentum_tsmom_filter(panel: pd.DataFrame, lookback: int, top_k: int = 2, | |
| trend_lookback: int = 100, | |
| rebalance: str = "W") -> pd.DataFrame: | |
| """XS momentum gated by per-asset trend: only hold top-k names that are | |
| also above their own trend_lookback MA.""" | |
| pos = xs_momentum(panel, lookback, top_k, False, rebalance) | |
| c = panel.shift(1) | |
| above = c > c.rolling(trend_lookback).mean() | |
| return pos.where(above, 0.0).fillna(0.0).where(panel.notna(), 0.0) | |
| def buy_and_hold_btc(panel: pd.DataFrame) -> pd.DataFrame: | |
| pos = pd.DataFrame(0.0, index=panel.index, columns=panel.columns) | |
| pos["BTC"] = 1.0 | |
| return pos | |