This repository was archived by the owner on Jul 14, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsignals.py
More file actions
356 lines (293 loc) · 14.4 KB
/
Copy pathsignals.py
File metadata and controls
356 lines (293 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
"""
signals.py — V2: Funding Rate Mean Reversion
Edge: When perpetual swap funding is extreme, the market is asymmetrically
positioned. High positive funding = over-leveraged longs paying shorts.
A small adverse move triggers cascading liquidations → violent mean reversion.
Strategy:
SETUP — Funding rate exceeds a threshold (structural leverage imbalance)
TIME — Candle falls within settlement proximity window (00/08/16 UTC)
TRIGGER — 15m price action confirms the squeeze has started
Setup:
funding_rate > +FUNDING_THRESHOLD → SHORT setup (longs over-leveraged)
funding_rate < -FUNDING_THRESHOLD → LONG setup (shorts over-leveraged)
Time filter:
Trades only fire within SETTLEMENT_WINDOW_H hours before each 8h settlement.
This is when over-leveraged traders close to avoid paying, and when
liquidation cascades cluster. Outside this window, the structural mechanic
that drives mean reversion is dormant.
Trigger (must occur while setup + time filter are active):
SHORT trigger: 15m candle closes below previous candle's low + volume spike
LONG trigger: 15m candle closes above previous candle's high + volume spike
Exit:
Fixed R:R target (aggressive — mean reversion is violent and fast).
SL placed at recent swing high/low.
This module has NO lagging indicators. It trades a structural market mechanic.
"""
import pandas as pd
# ---------------------------------------------------------------------------
# Strategy constants
# ---------------------------------------------------------------------------
# Funding threshold — 0.05% per 8h = 0.15%/day annualized ~55%.
# This is extreme. Normal funding oscillates around 0.01%.
FUNDING_THRESHOLD = 0.0005 # 0.05% — setup activates above this
# Cumulative 24h funding threshold (sum of last 3 × 8h rates).
# Persistent extreme funding is a stronger signal than a single spike.
FUNDING_24H_THRESH = 0.001 # 0.10% cumulative over 24h
# Volume spike: current candle volume must be >= this multiple of
# the rolling 20-candle average volume. Confirms participation.
VOLUME_SPIKE_MULT = 1.5
# Price breakdown: candle must close beyond prev candle's range.
# No additional buffer — the close itself is the confirmation.
# Risk parameters
SL_LOOKBACK = 5 # candles back for swing high/low SL
SL_BUFFER_PCT = 0.001 # 0.1% buffer beyond swing point
SL_MIN_DIST_PCT = 0.0015 # 0.15% minimum SL distance (noise filter)
TARGET_RR = 1.5 # fixed reward:risk ratio for TP
MIN_RR = 1.0 # minimum acceptable R:R
# Settlement time proximity filter
# BitMEX funding settles at 00:00, 08:00, 16:00 UTC.
# Over-leveraged traders close BEFORE settlement to avoid paying.
# Liquidation cascades cluster around settlement.
# Only trade within this window before each settlement.
SETTLEMENT_HOURS = [0, 8, 16] # UTC hours when funding settles
SETTLEMENT_WINDOW_H = 3 # hours before settlement to allow trades
USE_SETTLEMENT_FILTER = True # structural rule — do not toggle based on small samples
# Volume lookback for the rolling average
VOLUME_LOOKBACK = 20
# ---------------------------------------------------------------------------
# Settlement proximity check
# ---------------------------------------------------------------------------
def _in_settlement_window(timestamp: pd.Timestamp) -> bool:
"""
Returns True if the candle timestamp falls within SETTLEMENT_WINDOW_H
hours before any funding settlement time (00:00, 08:00, 16:00 UTC).
Example with SETTLEMENT_WINDOW_H=3:
08:00 settlement → window is 05:00–08:00
16:00 settlement → window is 13:00–16:00
00:00 settlement → window is 21:00–00:00
"""
hour = timestamp.hour
minute = timestamp.minute
candle_minutes = hour * 60 + minute
for settle_h in SETTLEMENT_HOURS:
settle_minutes = settle_h * 60
window_start = (settle_minutes - SETTLEMENT_WINDOW_H * 60) % (24 * 60)
if window_start < settle_minutes:
# Normal case: window doesn't wrap midnight
if window_start <= candle_minutes < settle_minutes:
return True
else:
# Wraps midnight (e.g., 21:00–00:00)
if candle_minutes >= window_start or candle_minutes < settle_minutes:
return True
return False
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def get_signal(df: pd.DataFrame, current_funding: dict = None) -> dict:
"""
Evaluate funding rate mean reversion signal.
Parameters
----------
df : DataFrame
15m OHLCV with columns: open, high, low, close, volume.
If 'funding_rate' and 'funding_24h' columns exist (from merge_funding),
they are used directly. Otherwise, current_funding dict is used.
current_funding : dict, optional
Live funding data: {"rate": float, "funding_24h": float}.
Used when df doesn't contain merged funding columns.
Returns
-------
dict with keys: signal, reason, entry_price, sl_price, tp_price, rr,
funding_rate, funding_24h
"""
no_trade = lambda reason: {
"signal": "NO_TRADE", "reason": reason,
"entry_price": 0, "sl_price": 0, "tp_price": 0,
"rr": 0, "funding_rate": None, "funding_24h": None,
}
if len(df) < max(SL_LOOKBACK, VOLUME_LOOKBACK) + 2:
return no_trade(f"Insufficient data: {len(df)} candles (need {VOLUME_LOOKBACK + 2})")
# ------------------------------------------------------------------
# Extract latest candle and previous candle
# ------------------------------------------------------------------
curr = df.iloc[-1]
prev = df.iloc[-2]
close = float(curr["close"])
opn = float(curr["open"])
high = float(curr["high"])
low = float(curr["low"])
vol = float(curr["volume"])
prev_high = float(prev["high"])
prev_low = float(prev["low"])
# ------------------------------------------------------------------
# Get funding rate
# ------------------------------------------------------------------
if "funding_rate" in df.columns:
funding_rate = float(curr["funding_rate"]) if pd.notna(curr.get("funding_rate")) else None
funding_24h = float(curr.get("funding_24h", 0)) if pd.notna(curr.get("funding_24h")) else None
elif current_funding:
funding_rate = current_funding.get("rate")
funding_24h = current_funding.get("funding_24h")
else:
return no_trade("No funding data available")
if funding_rate is None:
return no_trade("Funding rate is None")
# ------------------------------------------------------------------
# Diagnostics
# ------------------------------------------------------------------
print(f"\n -- Funding Rate Signal Diagnostics --")
print(f" Candle O={opn:.2f} H={high:.2f} L={low:.2f} C={close:.2f} V={vol:.0f}")
print(f" Prev H={prev_high:.2f} L={prev_low:.2f}")
print(f" Funding rate: {funding_rate:+.6f} ({funding_rate*100:+.4f}%)")
if funding_24h is not None:
print(f" Funding 24h: {funding_24h:+.6f} ({funding_24h*100:+.4f}%)")
# ------------------------------------------------------------------
# SETUP — Is funding extreme?
# ------------------------------------------------------------------
# Primary: single-rate threshold
setup_short = funding_rate > FUNDING_THRESHOLD
setup_long = funding_rate < -FUNDING_THRESHOLD
# Secondary: cumulative 24h threshold (stronger signal)
if funding_24h is not None:
cum_short = funding_24h > FUNDING_24H_THRESH
cum_long = funding_24h < -FUNDING_24H_THRESH
else:
cum_short = False
cum_long = False
# Either threshold triggers the setup
short_setup = setup_short or cum_short
long_setup = setup_long or cum_long
setup_reasons = []
if setup_short:
setup_reasons.append(f"rate {funding_rate*100:+.4f}% > +{FUNDING_THRESHOLD*100:.2f}%")
if cum_short:
setup_reasons.append(f"24h {funding_24h*100:+.4f}% > +{FUNDING_24H_THRESH*100:.2f}%")
if setup_long:
setup_reasons.append(f"rate {funding_rate*100:+.4f}% < -{FUNDING_THRESHOLD*100:.2f}%")
if cum_long:
setup_reasons.append(f"24h {funding_24h*100:+.4f}% < -{FUNDING_24H_THRESH*100:.2f}%")
print(f"\n Setup conditions:")
print(f" [{'PASS' if short_setup else 'FAIL'}] SHORT setup (longs over-leveraged): "
f"rate={funding_rate*100:+.4f}% threshold=±{FUNDING_THRESHOLD*100:.2f}%")
print(f" [{'PASS' if long_setup else 'FAIL'}] LONG setup (shorts over-leveraged): "
f"rate={funding_rate*100:+.4f}% threshold=±{FUNDING_THRESHOLD*100:.2f}%")
if not short_setup and not long_setup:
return no_trade(
f"No funding extreme: rate={funding_rate*100:+.4f}% "
f"(threshold=±{FUNDING_THRESHOLD*100:.2f}%)"
)
# ------------------------------------------------------------------
# TIME FILTER — Settlement proximity (optional)
# ------------------------------------------------------------------
if USE_SETTLEMENT_FILTER:
candle_ts = df.index[-1]
in_window = _in_settlement_window(candle_ts)
next_settle = None
for sh in sorted(SETTLEMENT_HOURS):
if candle_ts.hour < sh:
next_settle = sh
break
if next_settle is None:
next_settle = SETTLEMENT_HOURS[0] # wraps to 00:00
print(f"\n Time filter:")
print(f" [{'PASS' if in_window else 'FAIL'}] Settlement window: "
f"candle {candle_ts.strftime('%H:%M')} UTC, "
f"next settlement {next_settle:02d}:00 UTC, "
f"window={SETTLEMENT_WINDOW_H}h before")
if not in_window:
setup_dir = "SHORT" if short_setup else "LONG"
return no_trade(
f"{setup_dir} setup active ({', '.join(setup_reasons)}) "
f"but outside settlement window: "
f"{candle_ts.strftime('%H:%M')} UTC "
f"(next settle {next_settle:02d}:00, window={SETTLEMENT_WINDOW_H}h)"
)
# ------------------------------------------------------------------
# TRIGGER — Price action confirms the squeeze
# ------------------------------------------------------------------
# Volume spike check
vol_series = df["volume"].iloc[-(VOLUME_LOOKBACK + 1):-1].astype(float)
avg_vol = vol_series.mean()
vol_ratio = vol / avg_vol if avg_vol > 0 else 0
vol_ok = vol_ratio >= VOLUME_SPIKE_MULT
# Price breakdown
bearish_break = close < prev_low # closes below previous candle's low
bullish_break = close > prev_high # closes above previous candle's high
# Body confirmation (not a doji)
body_pct = abs(close - opn) / close if close > 0 else 0
has_body = body_pct > 0.0005 # > 0.05% body
print(f"\n Trigger conditions:")
print(f" [{'PASS' if vol_ok else 'FAIL'}] Volume spike: "
f"{vol:.0f} / avg {avg_vol:.0f} = {vol_ratio:.2f}x (min {VOLUME_SPIKE_MULT}x)")
print(f" [{'PASS' if bearish_break else 'FAIL'}] Bearish break: "
f"close {close:.2f} < prev_low {prev_low:.2f}")
print(f" [{'PASS' if bullish_break else 'FAIL'}] Bullish break: "
f"close {close:.2f} > prev_high {prev_high:.2f}")
print(f" [{'PASS' if has_body else 'FAIL'}] Body: {body_pct*100:.3f}% (min 0.05%)")
# SHORT trigger: funding extreme positive + bearish breakdown
short_trigger = short_setup and bearish_break and vol_ok and has_body
# LONG trigger: funding extreme negative + bullish breakdown
long_trigger = long_setup and bullish_break and vol_ok and has_body
if not short_trigger and not long_trigger:
if short_setup:
reason = (f"SHORT setup active ({', '.join(setup_reasons)}) "
f"but no trigger: break={'Y' if bearish_break else 'N'} "
f"vol={vol_ratio:.2f}x body={body_pct*100:.3f}%")
else:
reason = (f"LONG setup active ({', '.join(setup_reasons)}) "
f"but no trigger: break={'Y' if bullish_break else 'N'} "
f"vol={vol_ratio:.2f}x body={body_pct*100:.3f}%")
return no_trade(reason)
# ------------------------------------------------------------------
# Determine direction
# ------------------------------------------------------------------
if short_trigger:
direction = "SHORT"
else:
direction = "LONG"
# ------------------------------------------------------------------
# SL + TP calculation
# ------------------------------------------------------------------
entry = close
lookback = df.iloc[-(SL_LOOKBACK + 1):-1]
if direction == "SHORT":
swing_high = float(lookback["high"].max())
sl_price = swing_high * (1 + SL_BUFFER_PCT)
sl_dist = sl_price - entry
# Min distance check
if sl_dist / entry < SL_MIN_DIST_PCT:
return no_trade(
f"SHORT SL too tight: {sl_dist:.2f} ({sl_dist/entry*100:.3f}%) "
f"< min {SL_MIN_DIST_PCT*100:.2f}%"
)
tp_price = entry - (sl_dist * TARGET_RR)
rr = (entry - tp_price) / sl_dist
else: # LONG
swing_low = float(lookback["low"].min())
sl_price = swing_low * (1 - SL_BUFFER_PCT)
sl_dist = entry - sl_price
if sl_dist / entry < SL_MIN_DIST_PCT:
return no_trade(
f"LONG SL too tight: {sl_dist:.2f} ({sl_dist/entry*100:.3f}%) "
f"< min {SL_MIN_DIST_PCT*100:.2f}%"
)
tp_price = entry + (sl_dist * TARGET_RR)
rr = (tp_price - entry) / sl_dist
if rr < MIN_RR:
return no_trade(f"R:R {rr:.2f} < minimum {MIN_RR}")
reason = (f"Funding {direction}: {', '.join(setup_reasons)} | "
f"Trigger: {'bearish' if direction == 'SHORT' else 'bullish'} break "
f"+ vol {vol_ratio:.1f}x")
print(f"\n [{direction}] entry={entry:.2f} SL={sl_price:.2f}"
f" TP={tp_price:.2f} R:R={rr:.2f}")
return {
"signal": direction,
"reason": reason,
"entry_price": entry,
"sl_price": sl_price,
"tp_price": tp_price,
"rr": rr,
"funding_rate": funding_rate,
"funding_24h": funding_24h,
}