demo strategies
Browse files- crypto_eval/strategies.py +58 -0
crypto_eval/strategies.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Demo strategies for the crypto-quant-eval harness.
|
| 2 |
+
|
| 3 |
+
Each strategy is a function (bars, funding) -> positions: pd.Series of
|
| 4 |
+
units of base asset indexed by bar start time. Strategies MUST only use
|
| 5 |
+
information available at decision time: a bar starting at t is closed at
|
| 6 |
+
t+freq, so signals at t use bars shifted by 1 (the closed bar is at t-1).
|
| 7 |
+
The `shift_bars` helper enforces this.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def shift_bars(bars: pd.DataFrame, n: int = 1) -> pd.DataFrame:
|
| 17 |
+
"""Return bars as known at decision time t: last closed bar is t-1."""
|
| 18 |
+
return bars.shift(n)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def funding_carry(bars: pd.DataFrame, funding: pd.DataFrame,
|
| 22 |
+
threshold: float = 0.0001,
|
| 23 |
+
max_leverage: float = 1.0) -> pd.Series:
|
| 24 |
+
"""Short the perp when funding is persistently positive (carry trade).
|
| 25 |
+
|
| 26 |
+
Signal: rolling 3-tick mean funding. If mean funding > threshold, hold
|
| 27 |
+
short notional -max_leverage; if < -threshold, hold long; else flat.
|
| 28 |
+
"""
|
| 29 |
+
# funding at tick t is announced at t; a bar starting at t can use
|
| 30 |
+
# the tick at t-1 (announced) but not the one accruing over [t, t+8h)
|
| 31 |
+
# accept either a Series or a one-column DataFrame with funding_rate
|
| 32 |
+
if isinstance(funding, pd.DataFrame):
|
| 33 |
+
funding = funding["funding_rate"]
|
| 34 |
+
f = funding.shift(1)
|
| 35 |
+
signal = f.rolling(3, min_periods=1).mean()
|
| 36 |
+
pos = pd.Series(0.0, index=bars.index)
|
| 37 |
+
# expand tick-level signal to bar-level, decision at bar start t uses
|
| 38 |
+
# the last tick announced strictly before t
|
| 39 |
+
tick_signal = signal.reindex(bars.index, method="ffill")
|
| 40 |
+
pos[tick_signal > threshold] = -max_leverage
|
| 41 |
+
pos[tick_signal < -threshold] = max_leverage
|
| 42 |
+
return pos
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def ma_crossover(bars: pd.DataFrame, funding: pd.DataFrame,
|
| 46 |
+
fast: int = 24, slow: int = 168) -> pd.Series:
|
| 47 |
+
"""Classic fast/slow MA crossover on 1h close (7d slow)."""
|
| 48 |
+
close = shift_bars(bars)["close"]
|
| 49 |
+
fast_ma = close.rolling(fast).mean()
|
| 50 |
+
slow_ma = close.rolling(slow).mean()
|
| 51 |
+
pos = pd.Series(np.where(fast_ma > slow_ma, 1.0, -1.0), index=bars.index)
|
| 52 |
+
return pos
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
STRATEGIES = {
|
| 56 |
+
"funding_carry": funding_carry,
|
| 57 |
+
"ma_crossover": ma_crossover,
|
| 58 |
+
}
|