bb_squeeze_strategy.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. from __future__ import annotations
  2. from dataclasses import dataclass
  3. import pandas as pd
  4. from okx_codex_trader.time_rules import entry_allowed, is_us_open_window
  5. BAND_LENGTH = 96
  6. BANDWIDTH_LOOKBACK = 960
  7. BANDWIDTH_QUANTILE = 0.25
  8. STOP_LOSS_PCT = 0.01
  9. TAKE_PROFIT_PCT = 0.03
  10. MIDDLE_EXIT_BUFFER_PCT = 0.001
  11. MIDDLE_EXIT_CONFIRM_BARS = 1
  12. ETH_VOL_CAP = 0.006
  13. COOLDOWN_BARS = 24
  14. BTC_TREND_SMA = 480
  15. BTC_MOMENTUM_LOOKBACK = 96
  16. ENTRY_TIME_FILTER = "weekday"
  17. US_OPEN_EXIT_MODE = "skip"
  18. BREAKEVEN_TRIGGER_PCT = 0.008
  19. BREAKEVEN_LOCK_PCT = 0.001
  20. @dataclass(frozen=True)
  21. class StrategyState:
  22. last_candle_ts: int | None
  23. active_side: str | None
  24. entry_price: float | None
  25. entry_candle_ts: int | None
  26. cooldown_until_ts: int | None
  27. middle_exit_streak: int
  28. max_favorable_move_pct: float | None
  29. EMPTY_STATE = StrategyState(None, None, None, None, None, 0, None)
  30. def strategy_name() -> str:
  31. return (
  32. f"bb-rr-time-l{BAND_LENGTH}-bw{BANDWIDTH_LOOKBACK}"
  33. f"-q{BANDWIDTH_QUANTILE:g}-sl{STOP_LOSS_PCT:g}-rr{TAKE_PROFIT_PCT / STOP_LOSS_PCT:g}"
  34. f"-hybrid_signal_rr-both-btc-up-vc{ETH_VOL_CAP:g}-ddnone"
  35. f"-cd{COOLDOWN_BARS}-mxbuf{MIDDLE_EXIT_BUFFER_PCT:g}-mxc{MIDDLE_EXIT_CONFIRM_BARS}"
  36. f"-entry{ENTRY_TIME_FILTER}-openexit{US_OPEN_EXIT_MODE}"
  37. f"-be{BREAKEVEN_TRIGGER_PCT:g}-{BREAKEVEN_LOCK_PCT:g}"
  38. )
  39. def signal_from_frame(frame: pd.DataFrame, state: StrategyState) -> tuple[StrategyState, dict[str, object]]:
  40. if len(frame) < BANDWIDTH_LOOKBACK + 2 or "btc_close" not in frame.columns:
  41. raise ValueError("not enough candles")
  42. close = frame["close"].astype(float)
  43. btc_close = frame["btc_close"].astype(float)
  44. middle = close.rolling(BAND_LENGTH).mean()
  45. stdev = close.rolling(BAND_LENGTH).std(ddof=0)
  46. upper = middle + (2.0 * stdev)
  47. lower = middle - (2.0 * stdev)
  48. bandwidth = (upper - lower) / middle
  49. threshold = bandwidth.rolling(BANDWIDTH_LOOKBACK).quantile(BANDWIDTH_QUANTILE)
  50. eth_vol = close.pct_change().rolling(96).std(ddof=0)
  51. btc_sma = btc_close.rolling(BTC_TREND_SMA).mean()
  52. btc_momentum = btc_close / btc_close.shift(BTC_MOMENTUM_LOOKBACK) - 1.0
  53. decision_index = len(frame) - 1
  54. row = frame.iloc[decision_index]
  55. candle_ts = int(row["ts"])
  56. candle_time = pd.Timestamp(row["time"]).isoformat().replace("+00:00", "Z")
  57. indicators = {
  58. "eth_close": float(row["close"]),
  59. "middle": float(middle.iloc[decision_index]),
  60. "upper": float(upper.iloc[decision_index]),
  61. "lower": float(lower.iloc[decision_index]),
  62. "bandwidth": float(bandwidth.iloc[decision_index]),
  63. "bandwidth_threshold": float(threshold.iloc[decision_index]),
  64. "eth_vol_96": float(eth_vol.iloc[decision_index]),
  65. "btc_close": float(btc_close.iloc[decision_index]),
  66. "btc_sma_480": float(btc_sma.iloc[decision_index]),
  67. "btc_momentum_96": float(btc_momentum.iloc[decision_index]),
  68. }
  69. if state.last_candle_ts is not None and candle_ts <= state.last_candle_ts:
  70. return state, {
  71. "decision_candle_ts": candle_ts,
  72. "decision_candle_time": candle_time,
  73. "signal": "state_replay",
  74. "target_side": state.active_side or "flat",
  75. "indicators": indicators,
  76. }
  77. next_state = StrategyState(
  78. candle_ts,
  79. state.active_side,
  80. state.entry_price,
  81. state.entry_candle_ts,
  82. state.cooldown_until_ts,
  83. state.middle_exit_streak,
  84. state.max_favorable_move_pct,
  85. )
  86. signal = "hold"
  87. target_side = state.active_side or "flat"
  88. if state.active_side is not None:
  89. entry_price = float(state.entry_price)
  90. stop = entry_price * (1.0 - STOP_LOSS_PCT if state.active_side == "long" else 1.0 + STOP_LOSS_PCT)
  91. if state.max_favorable_move_pct is not None and state.max_favorable_move_pct >= BREAKEVEN_TRIGGER_PCT:
  92. breakeven_stop = entry_price * (1.0 + BREAKEVEN_LOCK_PCT if state.active_side == "long" else 1.0 - BREAKEVEN_LOCK_PCT)
  93. stop = max(stop, breakeven_stop) if state.active_side == "long" else min(stop, breakeven_stop)
  94. take = entry_price * (1.0 + TAKE_PROFIT_PCT if state.active_side == "long" else 1.0 - TAKE_PROFIT_PCT)
  95. stop_hit = (state.active_side == "long" and float(row["low"]) <= stop) or (state.active_side == "short" and float(row["high"]) >= stop)
  96. take_hit = (state.active_side == "long" and float(row["high"]) >= take) or (state.active_side == "short" and float(row["low"]) <= take)
  97. middle_exit = (state.active_side == "long" and float(row["close"]) < indicators["middle"] * (1.0 - MIDDLE_EXIT_BUFFER_PCT)) or (
  98. state.active_side == "short" and float(row["close"]) > indicators["middle"] * (1.0 + MIDDLE_EXIT_BUFFER_PCT)
  99. )
  100. if middle_exit and is_us_open_window(candle_ts) and US_OPEN_EXIT_MODE == "skip":
  101. middle_exit = False
  102. middle_exit_streak = state.middle_exit_streak + 1 if middle_exit else 0
  103. favorable_move = (float(row["high"]) / entry_price - 1.0) if state.active_side == "long" else (entry_price / float(row["low"]) - 1.0)
  104. max_favorable_move_pct = max(float(state.max_favorable_move_pct or 0.0), favorable_move)
  105. next_state = StrategyState(
  106. candle_ts,
  107. state.active_side,
  108. state.entry_price,
  109. state.entry_candle_ts,
  110. state.cooldown_until_ts,
  111. middle_exit_streak,
  112. max_favorable_move_pct,
  113. )
  114. if stop_hit or take_hit or middle_exit_streak >= MIDDLE_EXIT_CONFIRM_BARS:
  115. signal = (
  116. "exit_breakeven"
  117. if stop_hit and state.max_favorable_move_pct is not None and state.max_favorable_move_pct >= BREAKEVEN_TRIGGER_PCT
  118. else "exit_stop"
  119. if stop_hit
  120. else "exit_take_profit"
  121. if take_hit
  122. else "exit_middle"
  123. )
  124. target_side = "flat"
  125. next_state = StrategyState(
  126. candle_ts,
  127. None,
  128. None,
  129. None,
  130. candle_ts + (COOLDOWN_BARS * 900_000),
  131. 0,
  132. None,
  133. )
  134. else:
  135. cooldown_ok = state.cooldown_until_ts is None or candle_ts >= state.cooldown_until_ts
  136. compressed = indicators["bandwidth"] <= indicators["bandwidth_threshold"]
  137. vol_ok = indicators["eth_vol_96"] <= ETH_VOL_CAP
  138. btc_up = indicators["btc_close"] > indicators["btc_sma_480"]
  139. time_ok = entry_allowed(candle_ts, ENTRY_TIME_FILTER)
  140. if cooldown_ok and compressed and vol_ok and btc_up and time_ok and float(row["close"]) > indicators["upper"]:
  141. signal = "entry_long"
  142. target_side = "long"
  143. next_state = StrategyState(candle_ts, "long", float(row["close"]), candle_ts, state.cooldown_until_ts, 0, 0.0)
  144. elif cooldown_ok and compressed and vol_ok and btc_up and time_ok and float(row["close"]) < indicators["lower"]:
  145. signal = "entry_short"
  146. target_side = "short"
  147. next_state = StrategyState(candle_ts, "short", float(row["close"]), candle_ts, state.cooldown_until_ts, 0, 0.0)
  148. return next_state, {
  149. "decision_candle_ts": candle_ts,
  150. "decision_candle_time": candle_time,
  151. "signal": signal,
  152. "target_side": target_side,
  153. "indicators": indicators,
  154. "params": {
  155. "band_length": BAND_LENGTH,
  156. "bandwidth_lookback": BANDWIDTH_LOOKBACK,
  157. "bandwidth_quantile": BANDWIDTH_QUANTILE,
  158. "stop_loss_pct": STOP_LOSS_PCT,
  159. "take_profit_pct": TAKE_PROFIT_PCT,
  160. "middle_exit_buffer_pct": MIDDLE_EXIT_BUFFER_PCT,
  161. "middle_exit_confirm_bars": MIDDLE_EXIT_CONFIRM_BARS,
  162. "eth_vol_cap": ETH_VOL_CAP,
  163. "cooldown_bars": COOLDOWN_BARS,
  164. "side_mode": "both",
  165. "btc_filter": "btc-up",
  166. "entry_time_filter": ENTRY_TIME_FILTER,
  167. "us_open_exit_mode": US_OPEN_EXIT_MODE,
  168. "breakeven_trigger_pct": BREAKEVEN_TRIGGER_PCT,
  169. "breakeven_lock_pct": BREAKEVEN_LOCK_PCT,
  170. },
  171. }