Avi-L commited on
Commit
3ea2315
·
verified ·
1 Parent(s): bdc6050

daily multi-asset strategy grid

Browse files
Files changed (1) hide show
  1. crypto_eval/grid2.py +86 -0
crypto_eval/grid2.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Daily multi-asset strategy families for the panel sweep.
2
+
3
+ All signals use data through t-1: `panel.shift(1)` everywhere.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+
11
+ def _vol_scale(panel: pd.DataFrame, target: float, window: int) -> pd.DataFrame:
12
+ ret = panel.pct_change()
13
+ rv = ret.rolling(window).std().shift(1) * np.sqrt(365)
14
+ return (target / rv).clip(upper=1.0).fillna(0.0)
15
+
16
+
17
+ def tsmom_panel(panel: pd.DataFrame, lookback: int, long_only: bool = False,
18
+ vol_target: float | None = None,
19
+ vol_window: int = 60) -> pd.DataFrame:
20
+ c = panel.shift(1)
21
+ mom = c / c.shift(lookback) - 1
22
+ sig = pd.DataFrame(np.where(mom > 0, 1.0, -1.0),
23
+ index=panel.index, columns=panel.columns)
24
+ if long_only:
25
+ sig = sig.clip(lower=0.0)
26
+ if vol_target is not None:
27
+ sig = sig * _vol_scale(panel, vol_target, vol_window)
28
+ # pre-listing: no data -> flat
29
+ return sig.where(panel.notna(), 0.0).fillna(0.0)
30
+
31
+
32
+ def donchian_panel(panel: pd.DataFrame, window: int, long_only: bool = False,
33
+ vol_target: float | None = None) -> pd.DataFrame:
34
+ # channel uses highs/lows we don't have on the index panel; approximate
35
+ # with rolling max/min of closes through t-2 (close-based breakout)
36
+ c = panel.shift(1)
37
+ upper = panel.rolling(window).max().shift(2)
38
+ lower = panel.rolling(window).min().shift(2)
39
+ pos = pd.DataFrame(np.nan, index=panel.index, columns=panel.columns)
40
+ pos[c > upper] = 1.0
41
+ if long_only:
42
+ pos[c < lower] = 0.0
43
+ else:
44
+ pos[c < lower] = -1.0
45
+ pos = pos.ffill().fillna(0.0)
46
+ if vol_target is not None:
47
+ pos = pos * _vol_scale(panel, vol_target, 60)
48
+ return pos.where(panel.notna(), 0.0)
49
+
50
+
51
+ def xs_momentum(panel: pd.DataFrame, lookback: int, top_k: int = 2,
52
+ short_bottom: bool = False, rebalance: str = "W") -> pd.DataFrame:
53
+ """Cross-sectional momentum: long top-k by trailing return, optional
54
+ short bottom-k, rebalanced weekly (Mondays) or monthly."""
55
+ c = panel.shift(1)
56
+ mom = c / c.shift(lookback) - 1
57
+ ranks = mom.rank(axis=1, ascending=False)
58
+ n = panel.shape[1]
59
+ tgt = pd.DataFrame(0.0, index=panel.index, columns=panel.columns)
60
+ tgt[ranks <= top_k] = 1.0 / top_k
61
+ if short_bottom:
62
+ tgt[ranks > n - top_k] = -1.0 / top_k
63
+ idx = panel.index
64
+ if rebalance == "W":
65
+ mask = pd.Series(idx.dayofweek == 0, index=idx)
66
+ else: # monthly: first listed trading day of each month
67
+ mask = idx.to_series().groupby(idx.to_period("M")).transform("min").eq(idx)
68
+ pos = tgt.where(mask, np.nan).ffill().fillna(0.0)
69
+ return pos.where(panel.notna(), 0.0)
70
+
71
+
72
+ def xs_momentum_tsmom_filter(panel: pd.DataFrame, lookback: int, top_k: int = 2,
73
+ trend_lookback: int = 100,
74
+ rebalance: str = "W") -> pd.DataFrame:
75
+ """XS momentum gated by per-asset trend: only hold top-k names that are
76
+ also above their own trend_lookback MA."""
77
+ pos = xs_momentum(panel, lookback, top_k, False, rebalance)
78
+ c = panel.shift(1)
79
+ above = c > c.rolling(trend_lookback).mean()
80
+ return pos.where(above, 0.0).fillna(0.0).where(panel.notna(), 0.0)
81
+
82
+
83
+ def buy_and_hold_btc(panel: pd.DataFrame) -> pd.DataFrame:
84
+ pos = pd.DataFrame(0.0, index=panel.index, columns=panel.columns)
85
+ pos["BTC"] = 1.0
86
+ return pos