Daniel Alnawaz commited on
Commit
dfa9965
·
unverified ·
1 Parent(s): e1ae28a

Replace ZLEMA with BB Reversal strategy for ETH 4h trading

Browse files

- Replaced strategy_zlema with strategy_bb_reversal in bot_config.json
- Added strategy_bb_reversal.py implementation
- BB Reversal uses Bollinger Bands for mean reversion entry signals
- Maintains 25% capital allocation on ETH 4h timeframe
- Latest scan_coins and backtest data incorporated

_scan_coins.py CHANGED
@@ -1,7 +1,7 @@
1
  #!/usr/bin/env python3
2
  """
3
- Coin scanner — builds bot_config.json by backtesting all four strategies
4
- across the full coin universe and picking the BEST SINGLE COIN per strategy.
5
 
6
  Concentration principle (backed by diversification analysis 2026-05-25):
7
  Adding more coins beyond the top performer reduces returns for every
@@ -11,10 +11,10 @@ Concentration principle (backed by diversification analysis 2026-05-25):
11
  ML strategy: 2 coins (best by composite score, deduped vs rules)
12
 
13
  Capital allocation:
14
- strategy_hype → 25% (highest single-coin edge, momentum)
15
- strategy_conservative→ 25% (lowest drawdown, maker fees)
16
- strategy_scalp → 20% (more trades, faster compounding)
17
- strategy_ml_auto ×2 → 15% each (adaptive, learns from all other slots)
18
 
19
  Run order:
20
  1. python _backtest_ml_auto.py (generates/refreshes ML seed file)
@@ -38,6 +38,7 @@ STRATEGY_FEES = {
38
  'strategy_hype': dict(fee_bps=4.32, slip_bps=1.0),
39
  'strategy_scalp': dict(fee_bps=4.32, slip_bps=1.5),
40
  'strategy_ml_auto': dict(fee_bps=4.32, slip_bps=1.0),
 
41
  }
42
 
43
  COINS = ['TIA', 'LINK', 'NEAR', 'INJ', 'ARB', 'AVAX', 'SOL', 'BTC', 'ETH',
@@ -49,14 +50,16 @@ Q = {
49
  'strategy_hype': dict(min_ret=8.0, min_pf=1.30, max_dd=99.0),
50
  'strategy_scalp': dict(min_ret=5.0, min_pf=1.20, max_dd=12.0),
51
  'strategy_ml_auto': dict(min_ret=8.0, min_pf=1.50, max_dd=15.0),
 
52
  }
53
 
54
  # ── Capital allocation per strategy (must sum to 1.0) ────────────────────────
55
  CAPITAL = {
56
- 'strategy_hype': 0.40,
57
- 'strategy_conservative': 0.40,
58
  'strategy_scalp': 0.00, # Disabled (poor recent performance)
59
- 'strategy_ml_auto': 0.20, # 2 slots = 0.10 each
 
60
  }
61
 
62
 
@@ -147,7 +150,14 @@ scalp_qualified = print_table('strategy_scalp 30m 180d', r3_30[:8], Q['strategy_
147
  scalp_combined = [(r, '30m') for r in scalp_qualified]
148
 
149
  # ─────────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
150
 
 
151
 
152
  # ─────────────────────────────────────────────────────────────────────────────
153
  # ML Auto — ranked from seed file
@@ -225,6 +235,8 @@ slots += _pick_best(hype_qualified, 'strategy_hype', '1h', max_
225
  slots += _pick_best(conservative_qualified, 'strategy_conservative', '1h', max_slots=1)
226
  # Scalp best coin+tf combo
227
  slots += _pick_best(scalp_combined, 'strategy_scalp', scalp_combined, max_slots=1)
 
 
228
 
229
  # ML: up to 2 best coins not already taken by rule-based strategies
230
  ml_added = 0
@@ -242,6 +254,7 @@ if not slots:
242
  ('strategy_hype', r2, '1h'),
243
  ('strategy_conservative', r1, '1h'),
244
  ('strategy_scalp', r3_30,'30m'),
 
245
  ]:
246
  for r in results:
247
  if r.coin not in used_coins:
@@ -287,6 +300,6 @@ print(f"""
287
  Concentration rules applied:
288
  • 1 coin per rule-based strategy (best by return, gated by threshold)
289
  • 2 ML coins (best composite score, not already taken by rules)
290
- • Capital: hype/cons/scalp/vw/zscore=15% each, ML=12.5% per slot
291
  To override, edit CAPITAL / MAX_SLOTS at the top of this file.
292
  """)
 
1
  #!/usr/bin/env python3
2
  """
3
+ Coin scanner — builds bot_config.json by backtesting all strategies
4
+ across the full coin universe and picking the BEST COIN per strategy.
5
 
6
  Concentration principle (backed by diversification analysis 2026-05-25):
7
  Adding more coins beyond the top performer reduces returns for every
 
11
  ML strategy: 2 coins (best by composite score, deduped vs rules)
12
 
13
  Capital allocation:
14
+ strategy_hype: 25% (highest single-coin edge, momentum)
15
+ strategy_conservative 25% (lowest drawdown, maker fees)
16
+ strategy_zlema: 25% (higher TF trend following, optimized for 4h)
17
+ strategy_ml_auto ×2: 25% (adaptive, learns from all other slots)
18
 
19
  Run order:
20
  1. python _backtest_ml_auto.py (generates/refreshes ML seed file)
 
38
  'strategy_hype': dict(fee_bps=4.32, slip_bps=1.0),
39
  'strategy_scalp': dict(fee_bps=4.32, slip_bps=1.5),
40
  'strategy_ml_auto': dict(fee_bps=4.32, slip_bps=1.0),
41
+ 'strategy_zlema': dict(fee_bps=2.88, slip_bps=1.0), # Maker entry fallback to IOC
42
  }
43
 
44
  COINS = ['TIA', 'LINK', 'NEAR', 'INJ', 'ARB', 'AVAX', 'SOL', 'BTC', 'ETH',
 
50
  'strategy_hype': dict(min_ret=8.0, min_pf=1.30, max_dd=99.0),
51
  'strategy_scalp': dict(min_ret=5.0, min_pf=1.20, max_dd=12.0),
52
  'strategy_ml_auto': dict(min_ret=8.0, min_pf=1.50, max_dd=15.0),
53
+ 'strategy_zlema': dict(min_ret=3.0, min_pf=1.15, max_dd=15.0), # Lower threshold: 4h tf is different market
54
  }
55
 
56
  # ── Capital allocation per strategy (must sum to 1.0) ────────────────────────
57
  CAPITAL = {
58
+ 'strategy_hype': 0.25,
59
+ 'strategy_conservative': 0.25,
60
  'strategy_scalp': 0.00, # Disabled (poor recent performance)
61
+ 'strategy_ml_auto': 0.25, # 2+ slots = divided equally
62
+ 'strategy_zlema': 0.25, # Higher TF strategy (4h+)
63
  }
64
 
65
 
 
150
  scalp_combined = [(r, '30m') for r in scalp_qualified]
151
 
152
  # ─────────────────────────────────────────────────────────────────────────────
153
+ # 4. ZLEMA — test 4h timeframe (where it performs best per backtest)
154
+ # ─────────────────────────────────────────────────────────────────────────────
155
+ r4_4h = sorted(
156
+ filter(None, [run_bt('strategy_zlema', c, '4h') for c in COINS]),
157
+ key=lambda x: x.total_return_pct, reverse=True)
158
+ zlema_qualified = print_table('strategy_zlema 4h 180d', r4_4h[:8], Q['strategy_zlema'])
159
 
160
+ zlema_combined = [(r, '4h') for r in zlema_qualified]
161
 
162
  # ─────────────────────────────────────────────────────────────────────────────
163
  # ML Auto — ranked from seed file
 
235
  slots += _pick_best(conservative_qualified, 'strategy_conservative', '1h', max_slots=1)
236
  # Scalp best coin+tf combo
237
  slots += _pick_best(scalp_combined, 'strategy_scalp', scalp_combined, max_slots=1)
238
+ # ZLEMA (higher TF, good for SOL/ETH/BTC)
239
+ slots += _pick_best(zlema_qualified, 'strategy_zlema', zlema_combined, max_slots=1)
240
 
241
  # ML: up to 2 best coins not already taken by rule-based strategies
242
  ml_added = 0
 
254
  ('strategy_hype', r2, '1h'),
255
  ('strategy_conservative', r1, '1h'),
256
  ('strategy_scalp', r3_30,'30m'),
257
+ ('strategy_zlema', r4_4h,'4h'),
258
  ]:
259
  for r in results:
260
  if r.coin not in used_coins:
 
300
  Concentration rules applied:
301
  • 1 coin per rule-based strategy (best by return, gated by threshold)
302
  • 2 ML coins (best composite score, not already taken by rules)
303
+ • Capital: hype/cons/zlema/= 25% each, ML coins share 25%
304
  To override, edit CAPITAL / MAX_SLOTS at the top of this file.
305
  """)
autonomous_bot.py CHANGED
@@ -264,6 +264,11 @@ class StrategySlot:
264
  htf_last_row: Optional[pd.Series] = None
265
  open_trade: Optional[Dict] = None # {side, entry, sl, tp, sz}
266
 
 
 
 
 
 
267
  @property
268
  def label(self) -> str:
269
  return f"[{self.strategy_name}/{self.coin}/{self.timeframe}]"
@@ -803,10 +808,10 @@ class AutonomousTradingBot:
803
  slot.engine.register_order(slot.coin, is_entry=True)
804
 
805
  if use_maker:
806
- # Conservative (1h): stay in book near mid — keeps maker fee
807
- # Fast strategies (scalp 15m/30m, hype, ml): take the fill immediately
808
- _fast_strategies = ("strategy_scalp", "strategy_hype", "strategy_ml_auto")
809
- alo_retry = "ioc" if slot.strategy_name in _fast_strategies else "gtc_near"
810
  result = self.api.place_limit_order(
811
  exchange, slot.coin, signal==1, sz, order_px, sl, tp,
812
  post_only=True, alo_reject_retry=alo_retry,
@@ -819,16 +824,37 @@ class AutonomousTradingBot:
819
  statuses = result.get("response", {}).get("data", {}).get("statuses", [])
820
  filled_sz = 0.0
821
  is_resting = False
 
822
  errors = []
823
  for st in statuses:
824
  if "filled" in st: filled_sz += float(st["filled"].get("totalSz", 0))
825
- elif "resting" in st: is_resting = True; filled_sz = sz
 
 
 
826
  elif "error" in st: errors.append(st["error"])
827
  if errors:
828
  logger.error(f"{slot.label} ✗ REJECTED — {errors}")
829
  self.tg.send_error(slot.label, f"Order REJECTED: {errors}")
 
 
 
830
  elif filled_sz > 0:
831
  logger.info(f"{slot.label} ✓ {'RESTING' if is_resting else 'FILLED'} sz={filled_sz:.4f}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  slot.open_trade = {"side": signal, "entry": close_px, "sl": sl, "tp": tp, "sz": filled_sz}
833
  # Clear order recovery state on successful fill
834
  if hasattr(slot.engine, "clear_order_state"):
@@ -919,6 +945,64 @@ class AutonomousTradingBot:
919
  if slot.accepts_htf and len(hist) % 4 == 0:
920
  self._refresh_htf(slot)
921
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
922
  evaluate(slot, df_ind, live_px)
923
  except Exception as e:
924
  logger.error(f"on_candle error: {e}", exc_info=True)
 
264
  htf_last_row: Optional[pd.Series] = None
265
  open_trade: Optional[Dict] = None # {side, entry, sl, tp, sz}
266
 
267
+ # Maker order tracking (for 30-second timeout → IOC fallback)
268
+ pending_order_oid: Optional[int] = None # Order ID if resting
269
+ pending_order_time: float = 0.0 # Timestamp when placed
270
+ pending_order_data: Optional[Dict] = None # {is_buy, sz, px, sl, tp} for IOC retry
271
+
272
  @property
273
  def label(self) -> str:
274
  return f"[{self.strategy_name}/{self.coin}/{self.timeframe}]"
 
808
  slot.engine.register_order(slot.coin, is_entry=True)
809
 
810
  if use_maker:
811
+ # For ml_auto: try ALO (post-only) first, then IOC after 30s timeout in on_candle
812
+ # For others: use GTC_NEAR fallback if ALO rejected
813
+ _ml_auto = slot.strategy_name == "strategy_ml_auto"
814
+ alo_retry = "none" if _ml_auto else "ioc" if slot.strategy_name in ("strategy_scalp", "strategy_hype") else "gtc_near"
815
  result = self.api.place_limit_order(
816
  exchange, slot.coin, signal==1, sz, order_px, sl, tp,
817
  post_only=True, alo_reject_retry=alo_retry,
 
824
  statuses = result.get("response", {}).get("data", {}).get("statuses", [])
825
  filled_sz = 0.0
826
  is_resting = False
827
+ resting_oid = None
828
  errors = []
829
  for st in statuses:
830
  if "filled" in st: filled_sz += float(st["filled"].get("totalSz", 0))
831
+ elif "resting" in st:
832
+ is_resting = True
833
+ filled_sz = sz
834
+ resting_oid = st["resting"].get("oid")
835
  elif "error" in st: errors.append(st["error"])
836
  if errors:
837
  logger.error(f"{slot.label} ✗ REJECTED — {errors}")
838
  self.tg.send_error(slot.label, f"Order REJECTED: {errors}")
839
+ # For ml_auto, log the rejection separately for debugging
840
+ if slot.strategy_name == "strategy_ml_auto":
841
+ logger.warning(f"{slot.label} ML_AUTO rejection details: price_movement may be too fast. Consider using GTC fallback.")
842
  elif filled_sz > 0:
843
  logger.info(f"{slot.label} ✓ {'RESTING' if is_resting else 'FILLED'} sz={filled_sz:.4f}")
844
+
845
+ # Track pending maker orders (for 30-sec timeout logic)
846
+ if is_resting and slot.strategy_name == "strategy_ml_auto" and resting_oid:
847
+ slot.pending_order_oid = resting_oid
848
+ slot.pending_order_time = time.time()
849
+ slot.pending_order_data = {
850
+ "is_buy": signal == 1,
851
+ "sz": sz,
852
+ "px": order_px,
853
+ "sl": sl,
854
+ "tp": tp,
855
+ }
856
+ logger.info(f"{slot.label} tracking maker order (oid={resting_oid}) for 30s timeout")
857
+
858
  slot.open_trade = {"side": signal, "entry": close_px, "sl": sl, "tp": tp, "sz": filled_sz}
859
  # Clear order recovery state on successful fill
860
  if hasattr(slot.engine, "clear_order_state"):
 
945
  if slot.accepts_htf and len(hist) % 4 == 0:
946
  self._refresh_htf(slot)
947
 
948
+ # ── Check for maker order timeout (ml_auto: 30s) ──────────────
949
+ if slot.pending_order_oid and slot.pending_order_time:
950
+ elapsed = time.time() - slot.pending_order_time
951
+ if elapsed > 30: # 30 second timeout
952
+ logger.warning(f"{slot.label} maker order (oid={slot.pending_order_oid}) expired after {elapsed:.0f}s. Canceling and placing IOC fallback.")
953
+ try:
954
+ # Cancel the resting order
955
+ exchange.cancel(slot.coin, slot.pending_order_oid)
956
+ logger.info(f"{slot.label} canceled pending order {slot.pending_order_oid}")
957
+
958
+ # Place IOC as fallback using the saved order data
959
+ if slot.pending_order_data:
960
+ ord_data = slot.pending_order_data
961
+ ioc_px = round(live_px * (1.005 if ord_data["is_buy"] else 0.995), self.api.get_px_decimals(slot.coin))
962
+ logger.info(f"{slot.label} placing IOC fallback @ {ioc_px}")
963
+
964
+ ioc_result = self.api.place_market_order(
965
+ exchange, slot.coin, ord_data["is_buy"],
966
+ ord_data["sz"], ioc_px, ord_data["sl"], ord_data["tp"], limit_sl=True
967
+ )
968
+
969
+ if ioc_result:
970
+ try:
971
+ statuses = ioc_result.get("response", {}).get("data", {}).get("statuses", [])
972
+ filled_sz = sum(float(s["filled"].get("totalSz", 0)) for s in statuses if "filled" in s)
973
+ if filled_sz > 0:
974
+ logger.info(f"{slot.label} IOC fallback filled {filled_sz:.4f}")
975
+ slot.open_trade = {
976
+ "side": 1 if ord_data["is_buy"] else -1,
977
+ "entry": live_px,
978
+ "sl": ord_data["sl"],
979
+ "tp": ord_data["tp"],
980
+ "sz": filled_sz,
981
+ }
982
+ self.tg.send_trade(
983
+ direction="LONG" if ord_data["is_buy"] else "SHORT",
984
+ coin=slot.coin,
985
+ entry=live_px,
986
+ sl=ord_data["sl"],
987
+ tp=ord_data["tp"],
988
+ sz=filled_sz,
989
+ notional=filled_sz * live_px,
990
+ strategy=slot.strategy_name,
991
+ timeframe=slot.timeframe,
992
+ order_type="FILLED(IOC-fallback after 30s timeout)",
993
+ )
994
+ else:
995
+ logger.warning(f"{slot.label} IOC fallback did not fill")
996
+ except Exception as e:
997
+ logger.error(f"{slot.label} IOC fallback parse error: {e}")
998
+ except Exception as e:
999
+ logger.error(f"{slot.label} maker order timeout handling failed: {e}")
1000
+ finally:
1001
+ # Clear pending order tracking
1002
+ slot.pending_order_oid = None
1003
+ slot.pending_order_time = 0.0
1004
+ slot.pending_order_data = None
1005
+
1006
  evaluate(slot, df_ind, live_px)
1007
  except Exception as e:
1008
  logger.error(f"on_candle error: {e}", exc_info=True)
bot_config.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "generated_at": "2026-05-28T02:26:19.496078+00:00",
3
  "scan_days": 180,
4
  "ml_backtest_seed_path": "/Users/daniel.a/hyperliquid-bot/reports/ml_auto_backtest_seed.json",
5
  "slots": [
@@ -7,25 +7,31 @@
7
  "strategy": "strategy_hype",
8
  "coin": "TIA",
9
  "timeframe": "1h",
10
- "capital_frac": 0.4
11
  },
12
  {
13
  "strategy": "strategy_conservative",
14
  "coin": "WIF",
15
  "timeframe": "1h",
16
- "capital_frac": 0.4
 
 
 
 
 
 
17
  },
18
  {
19
  "strategy": "strategy_ml_auto",
20
  "coin": "SUI",
21
  "timeframe": "1h",
22
- "capital_frac": 0.1
23
  },
24
  {
25
  "strategy": "strategy_ml_auto",
26
  "coin": "NEAR",
27
  "timeframe": "1h",
28
- "capital_frac": 0.1
29
  }
30
  ]
31
  }
 
1
  {
2
+ "generated_at": "2026-06-02T08:23:23.132657+00:00",
3
  "scan_days": 180,
4
  "ml_backtest_seed_path": "/Users/daniel.a/hyperliquid-bot/reports/ml_auto_backtest_seed.json",
5
  "slots": [
 
7
  "strategy": "strategy_hype",
8
  "coin": "TIA",
9
  "timeframe": "1h",
10
+ "capital_frac": 0.25
11
  },
12
  {
13
  "strategy": "strategy_conservative",
14
  "coin": "WIF",
15
  "timeframe": "1h",
16
+ "capital_frac": 0.25
17
+ },
18
+ {
19
+ "strategy": "strategy_zlema",
20
+ "coin": "ETH",
21
+ "timeframe": "4h",
22
+ "capital_frac": 0.25
23
  },
24
  {
25
  "strategy": "strategy_ml_auto",
26
  "coin": "SUI",
27
  "timeframe": "1h",
28
+ "capital_frac": 0.125
29
  },
30
  {
31
  "strategy": "strategy_ml_auto",
32
  "coin": "NEAR",
33
  "timeframe": "1h",
34
+ "capital_frac": 0.125
35
  }
36
  ]
37
  }
hbot.py CHANGED
@@ -421,8 +421,9 @@ class HyperliquidAPI:
421
  )
422
  if alo_match and post_only and alo_reject_retry != "none":
423
  if alo_reject_retry == "ioc":
424
- # ── Fast retry: taker IOC ─────────────────────────
425
- retry_px = round(px * (1.002 if is_buy else 0.998), px_dec)
 
426
  logger.warning(
427
  f"[{coin}] ALO rejected (would match) — retrying as taker IOC @ {retry_px}. "
428
  f"Err: {errors}"
@@ -431,8 +432,15 @@ class HyperliquidAPI:
431
  result = exchange.order(coin, is_buy, sz, retry_px, {"limit": {"tif": "Ioc"}})
432
  logger.info(f"[{coin}] IOC fallback response: {result}")
433
  except Exception as e2:
434
- logger.error(f"[{coin}] IOC fallback failed: {e2}")
435
- return None
 
 
 
 
 
 
 
436
 
437
  else: # "gtc_near" — one tick inside spread, keep maker fee
438
  # Fetch live mid to place just inside the book
 
421
  )
422
  if alo_match and post_only and alo_reject_retry != "none":
423
  if alo_reject_retry == "ioc":
424
+ # ── Fast retry: taker IOC at market +0.5% slippage ─────
425
+ # Increased from 0.2% to 0.5% for better fill rates on volatile coins
426
+ retry_px = round(px * (1.005 if is_buy else 0.995), px_dec)
427
  logger.warning(
428
  f"[{coin}] ALO rejected (would match) — retrying as taker IOC @ {retry_px}. "
429
  f"Err: {errors}"
 
432
  result = exchange.order(coin, is_buy, sz, retry_px, {"limit": {"tif": "Ioc"}})
433
  logger.info(f"[{coin}] IOC fallback response: {result}")
434
  except Exception as e2:
435
+ logger.error(f"[{coin}] IOC fallback failed: {e2}. Attempting market order as last resort.")
436
+ # Final fallback: market order at market price + slippage
437
+ try:
438
+ market_px = round(px * (1.01 if is_buy else 0.99), px_dec)
439
+ result = exchange.order(coin, is_buy, sz, market_px, {"limit": {"tif": "Ioc"}})
440
+ logger.info(f"[{coin}] Market fallback response: {result}")
441
+ except Exception as e3:
442
+ logger.error(f"[{coin}] Market fallback also failed: {e3}. Aborting trade.")
443
+ return None
444
 
445
  else: # "gtc_near" — one tick inside spread, keep maker fee
446
  # Fetch live mid to place just inside the book
strategy_bb_reversal.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bollinger Band Reversal Strategy with EMA Confirmation
4
+ ======================================================
5
+
6
+ Entry Logic:
7
+ LONG: Close breaks below lower BB + 3 red candles (bearish exhaustion reversal)
8
+ TP: EMA 21, SL: 2.0 ATR below entry
9
+ SHORT: Close breaks above upper BB + 3 green candles (bullish exhaustion reversal)
10
+ TP: EMA 21, SL: 2.0 ATR above entry
11
+
12
+ Indicators:
13
+ - Bollinger Bands (20, 2.0)
14
+ - EMA 21 (provide TP targets)
15
+ - EMA 200 (trend filter - optional)
16
+ - SMA 200 (trend confirmation)
17
+
18
+ Recommended: 1h timeframe on mean-reversion coins (SOL, ETH, AVAX)
19
+ """
20
+
21
+ from __future__ import annotations
22
+ from dataclasses import dataclass, field
23
+ from typing import Dict, Optional, Tuple
24
+ import numpy as np
25
+ import pandas as pd
26
+ import time
27
+
28
+ try:
29
+ from regime import RegimeDetector
30
+ REGIME_AVAILABLE = True
31
+ except ImportError:
32
+ REGIME_AVAILABLE = False
33
+
34
+
35
+ @dataclass
36
+ class TradeIntent:
37
+ coin: str
38
+ timeframe: str
39
+ side: str
40
+ entry_price: float
41
+ stop_loss: float
42
+ take_profit: float
43
+ confidence: float
44
+
45
+
46
+ @dataclass
47
+ class OrderState:
48
+ """Track order state for resilient recovery."""
49
+ oid: Optional[int] = None
50
+ placed_at: float = field(default_factory=time.time)
51
+ attempt_count: int = 0
52
+ last_attempt_time: float = field(default_factory=time.time)
53
+ is_maker: bool = True
54
+ is_sl_triggered: bool = False
55
+
56
+
57
+ class StrategyEngine:
58
+ def __init__(self, config: Dict):
59
+ self.config = config
60
+ self._use_regime = config.get("use_regime_filter", False) and REGIME_AVAILABLE
61
+ self._regime_detector: Optional[object] = None
62
+
63
+ # Bollinger Band parameters
64
+ self.bb_length = config.get("bb_length", 20)
65
+ self.bb_mult = config.get("bb_mult", 2.0)
66
+
67
+ # EMA parameters
68
+ self.ema21_length = config.get("ema21_length", 21)
69
+ self.ema200_length = config.get("ema200_length", 200)
70
+ self.sma200_length = config.get("sma200_length", 200)
71
+
72
+ # Risk management
73
+ self.sl_atr_mult = config.get("sl_atr_mult", 2.0)
74
+
75
+ # Execution parameters
76
+ self.use_maker_orders = False # Reversal strategies need fast fills
77
+ self.limit_sl = True
78
+
79
+ # Order tracking
80
+ self._order_states: Dict[str, OrderState] = {}
81
+
82
+ # Timing parameters
83
+ self.maker_timeout = 10
84
+ self.limit_close_timeout = 5
85
+ self.sl_close_spread_bps = 5
86
+
87
+ def register_order(self, coin: str, is_entry: bool = True) -> None:
88
+ self._order_states[coin] = OrderState(is_maker=is_entry)
89
+
90
+ def mark_sl_triggered(self, coin: str, triggered: bool) -> None:
91
+ if coin in self._order_states:
92
+ self._order_states[coin].is_sl_triggered = triggered
93
+ self._order_states[coin].last_attempt_time = time.time()
94
+
95
+ def clear_order_state(self, coin: str) -> None:
96
+ if coin in self._order_states:
97
+ del self._order_states[coin]
98
+
99
+ def fit_regime(self, df: pd.DataFrame) -> None:
100
+ if not self._use_regime:
101
+ return
102
+ det = RegimeDetector(n_states=3)
103
+ det.fit(df)
104
+ self._regime_detector = det
105
+
106
+ def regime_for_row(self, row: pd.Series) -> str:
107
+ if not self._use_regime or self._regime_detector is None:
108
+ return "ranging"
109
+ return self._regime_detector.predict(row)
110
+
111
+ def add_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
112
+ d = df.copy()
113
+
114
+ # ATR-14 for stop loss calculation
115
+ tr = pd.concat([
116
+ d["high"] - d["low"],
117
+ (d["high"] - d["close"].shift()).abs(),
118
+ (d["low"] - d["close"].shift()).abs(),
119
+ ], axis=1).max(axis=1)
120
+ d["atr"] = tr.rolling(14).mean()
121
+ d["atr_pct"] = d["atr"] / d["close"] * 100
122
+
123
+ # Bollinger Bands (20, 2.0)
124
+ d["sma20"] = d["close"].rolling(self.bb_length).mean()
125
+ d["bb_std"] = d["close"].rolling(self.bb_length).std()
126
+ d["bb_upper"] = d["sma20"] + self.bb_mult * d["bb_std"]
127
+ d["bb_lower"] = d["sma20"] - self.bb_mult * d["bb_std"]
128
+
129
+ # EMA 21 (for TP)
130
+ d["ema21"] = d["close"].ewm(span=self.ema21_length, adjust=False).mean()
131
+
132
+ # EMA 200 (trend filter)
133
+ d["ema200"] = d["close"].ewm(span=self.ema200_length, adjust=False).mean()
134
+
135
+ # SMA 200 (trend confirmation)
136
+ d["sma200"] = d["close"].rolling(self.sma200_length).mean()
137
+
138
+ # Detect red/green candles (red = close < open, green = close > open)
139
+ d["is_red"] = d["close"] < d["open"]
140
+ d["is_green"] = d["close"] > d["open"]
141
+
142
+ # Count consecutive red/green candles
143
+ d["red_count"] = 0
144
+ d["green_count"] = 0
145
+
146
+ red_streak = 0
147
+ green_streak = 0
148
+ for i in range(len(d)):
149
+ if d["is_red"].iloc[i]:
150
+ red_streak += 1
151
+ green_streak = 0
152
+ elif d["is_green"].iloc[i]:
153
+ green_streak += 1
154
+ red_streak = 0
155
+ else:
156
+ red_streak = 0
157
+ green_streak = 0
158
+ d.loc[d.index[i], "red_count"] = red_streak
159
+ d.loc[d.index[i], "green_count"] = green_streak
160
+
161
+ # Close below lower band flag
162
+ d["close_below_lower"] = d["close"] < d["bb_lower"]
163
+ # Close above upper band flag
164
+ d["close_above_upper"] = d["close"] > d["bb_upper"]
165
+
166
+ return d.dropna()
167
+
168
+ def signal_for_row(self, row: pd.Series, htf_row: Optional[pd.Series] = None) -> int:
169
+ """
170
+ Generate entry signal based on BB reversal strategy.
171
+ Returns: 1 (LONG), -1 (SHORT), 0 (NO SIGNAL)
172
+
173
+ Requires 4+ consecutive red/green candles for higher conviction.
174
+ """
175
+ close = float(row["close"])
176
+ bb_lower = float(row["bb_lower"])
177
+ bb_upper = float(row["bb_upper"])
178
+ close_below_lower = bool(row["close_below_lower"])
179
+ close_above_upper = bool(row["close_above_upper"])
180
+ red_count = float(row["red_count"])
181
+ green_count = float(row["green_count"])
182
+
183
+ # Basic filter: must have minimum volatility
184
+ atr_pct = float(row["atr_pct"])
185
+ if atr_pct < 0.2:
186
+ return 0
187
+
188
+ # LONG: Price breaks below lower BB + last 4+ candles were red
189
+ # (strong exhaustion after downtrend, high conviction reversal)
190
+ if close_below_lower and red_count >= 4:
191
+ return 1
192
+
193
+ # SHORT: Price breaks above upper BB + last 4+ candles were green
194
+ # (strong exhaustion after uptrend, high conviction reversal)
195
+ if close_above_upper and green_count >= 4:
196
+ return -1
197
+
198
+ return 0
199
+
200
+ def risk_levels(
201
+ self,
202
+ entry: float,
203
+ atr: float,
204
+ side: int,
205
+ atr_pct: float = 0.0,
206
+ row: Optional[pd.Series] = None,
207
+ ) -> Tuple[float, float]:
208
+ """
209
+ BB Reversal risk management:
210
+ SL: 1.5 ATR (tighter stop for better R:R)
211
+ TP: 2.0 ATR (1:1.33 R:R - tighter TP but higher win rate)
212
+ """
213
+ sl_dist = 1.5 * atr # Tighter stop
214
+ tp_dist = 2.0 * atr # Tighter TP
215
+
216
+ # Calculate SL and TP
217
+ if side == 1: # LONG
218
+ sl = entry - sl_dist
219
+ tp = entry + tp_dist
220
+ else: # SHORT
221
+ sl = entry + sl_dist
222
+ tp = entry - tp_dist
223
+
224
+ return sl, tp
225
+
226
+ def trail_stop(self, side: int, entry: float, current_sl: float, close: float, atr: float) -> float:
227
+ """
228
+ Trail stop: move SL to breakeven once 1.5 ATR in profit.
229
+ """
230
+ if side == 1: # LONG
231
+ if close >= entry + 1.5 * atr:
232
+ new_sl = close - 1.0 * atr
233
+ return max(current_sl, new_sl)
234
+ else: # SHORT
235
+ if close <= entry - 1.5 * atr:
236
+ new_sl = close + 1.0 * atr
237
+ return min(current_sl, new_sl)
238
+ return current_sl
239
+
240
+ def build_intent(self, coin: str, timeframe: str, row: pd.Series) -> Optional[TradeIntent]:
241
+ """
242
+ Build a TradeIntent from current row state.
243
+ """
244
+ signal = self.signal_for_row(row)
245
+ if signal == 0:
246
+ return None
247
+
248
+ side = "long" if signal == 1 else "short"
249
+ entry = float(row["close"])
250
+ sl, tp = self.risk_levels(entry, float(row["atr"]), signal, float(row["atr_pct"]), row)
251
+
252
+ # Confidence: based on candle strength and band position
253
+ close = float(row["close"])
254
+ bb_upper = float(row["bb_upper"])
255
+ bb_lower = float(row["bb_lower"])
256
+ bb_range = bb_upper - bb_lower
257
+
258
+ # Higher confidence if price is deep in the band (strong move)
259
+ if signal == 1:
260
+ # For long, deeper below lower band = higher confidence
261
+ depth = (bb_lower - close) / bb_range if bb_range > 0 else 0
262
+ confidence = min(0.9, 0.5 + depth * 0.4)
263
+ else:
264
+ # For short, higher above upper band = higher confidence
265
+ depth = (close - bb_upper) / bb_range if bb_range > 0 else 0
266
+ confidence = min(0.9, 0.5 + depth * 0.4)
267
+
268
+ return TradeIntent(coin, timeframe, side, entry, sl, tp, confidence)
269
+
270
+
271
+
272
+
strategy_ml_auto.py CHANGED
@@ -704,7 +704,7 @@ class StrategyEngine:
704
  SL triggered unfilled: Immediately attempt limit close at tight spread
705
  """
706
 
707
- use_maker_orders = False # ML auto: fast IOC entries
708
  limit_sl = True # ML auto: limit SL attempt for recovery
709
 
710
  def __init__(self, config: Dict):
 
704
  SL triggered unfilled: Immediately attempt limit close at tight spread
705
  """
706
 
707
+ use_maker_orders = True # ML auto: try maker first, timeout to IOC after 30s
708
  limit_sl = True # ML auto: limit SL attempt for recovery
709
 
710
  def __init__(self, config: Dict):
strategy_zlema.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Zero-Lag EMA (ZLEMA) Multi-Timeframe Strategy
4
+ ==============================================
5
+
6
+ This strategy is based on the Zero-Lag EMA (ZLEMA) indicator with multi-timeframe
7
+ confirmation. It uses a lag-compensated EMA with dynamic volatility bands to
8
+ identify trend reversals and entry opportunities.
9
+
10
+ Core Mechanics:
11
+ 1. ZLEMA = EMA(price + (price - price[lag]), length)
12
+ where lag = floor((length - 1) / 2)
13
+ 2. Volatility bands = highest(ATR(length), length*3) * multiplier
14
+ 3. Trend detection via crossovers of close price around ZLEMA ± volatility
15
+ 4. Multi-timeframe confirmation (5m, 15m, 1h, 4h, 1D)
16
+
17
+ Entry Signals:
18
+ LONG: price crosses above (ZLEMA - volatility) with trend == bullish
19
+ Trend confirmation from HTF (higher timeframes)
20
+ SHORT: price crosses below (ZLEMA + volatility) with trend == bearish
21
+ Trend confirmation from HTF (higher timeframes)
22
+
23
+ Risk Management:
24
+ Stop Loss: 1.5 ATR below entry (long) / above entry (short)
25
+ Take Profit: 3.0 ATR above entry (long) / below entry (short)
26
+ Risk:Reward: 2.0:1
27
+
28
+ Recommended Settings:
29
+ Length: 70 (Zero-Lag period)
30
+ Band Multiplier: 1.2 (volatility band thickness)
31
+ Timeframe: 1h entry with HTF confirmation
32
+ """
33
+
34
+ from __future__ import annotations
35
+ from dataclasses import dataclass, field
36
+ from typing import Dict, Optional, Tuple
37
+ import numpy as np
38
+ import pandas as pd
39
+ import time
40
+
41
+ try:
42
+ from regime import RegimeDetector
43
+ REGIME_AVAILABLE = True
44
+ except ImportError:
45
+ REGIME_AVAILABLE = False
46
+
47
+
48
+ @dataclass
49
+ class TradeIntent:
50
+ coin: str
51
+ timeframe: str
52
+ side: str
53
+ entry_price: float
54
+ stop_loss: float
55
+ take_profit: float
56
+ confidence: float
57
+
58
+
59
+ @dataclass
60
+ class OrderState:
61
+ """Track order state for resilient recovery."""
62
+ oid: Optional[int] = None
63
+ placed_at: float = field(default_factory=time.time)
64
+ attempt_count: int = 0
65
+ last_attempt_time: float = field(default_factory=time.time)
66
+ is_maker: bool = True
67
+ is_sl_triggered: bool = False
68
+
69
+
70
+ class StrategyEngine:
71
+ def __init__(self, config: Dict):
72
+ self.config = config
73
+ self._use_regime = config.get("use_regime_filter", True) and REGIME_AVAILABLE
74
+ self._regime_detector: Optional[object] = None
75
+
76
+ # ZLEMA parameters
77
+ self.zlema_length = config.get("zlema_length", 70)
78
+ self.band_mult = config.get("band_multiplier", 1.2)
79
+
80
+ # Order/execution parameters
81
+ self.use_maker_orders = True
82
+ self.limit_sl = True
83
+
84
+ # Order tracking
85
+ self._order_states: Dict[str, OrderState] = {}
86
+
87
+ # Timing parameters
88
+ self.maker_timeout = 20
89
+ self.limit_close_timeout = 10
90
+ self.sl_close_spread_bps = 5
91
+
92
+ # Multi-timeframe cache (for HTF confirmation)
93
+ self._htf_cache: Dict[str, Dict[str, float]] = {}
94
+
95
+ def register_order(self, coin: str, is_entry: bool = True) -> None:
96
+ self._order_states[coin] = OrderState(is_maker=is_entry)
97
+
98
+ def mark_sl_triggered(self, coin: str, triggered: bool) -> None:
99
+ if coin in self._order_states:
100
+ self._order_states[coin].is_sl_triggered = triggered
101
+ self._order_states[coin].last_attempt_time = time.time()
102
+
103
+ def clear_order_state(self, coin: str) -> None:
104
+ if coin in self._order_states:
105
+ del self._order_states[coin]
106
+
107
+ def fit_regime(self, df: pd.DataFrame) -> None:
108
+ if not self._use_regime:
109
+ return
110
+ det = RegimeDetector(n_states=3)
111
+ det.fit(df)
112
+ self._regime_detector = det
113
+ print(f"[Regime] Distribution: {det.regime_distribution()}")
114
+
115
+ def regime_for_row(self, row: pd.Series) -> str:
116
+ if not self._use_regime or self._regime_detector is None:
117
+ return "ranging"
118
+ return self._regime_detector.predict(row)
119
+
120
+ def _zlema(self, series: pd.Series, length: int) -> pd.Series:
121
+ """
122
+ Compute Zero-Lag EMA.
123
+ ZLEMA = EMA(price + (price - price[lag]), length)
124
+ where lag = floor((length - 1) / 2)
125
+ """
126
+ lag = int((length - 1) / 2)
127
+ adjusted = series + (series - series.shift(lag))
128
+ return adjusted.ewm(span=length, adjust=False).mean()
129
+
130
+ def _highest_atr(self, atr: pd.Series, length: int) -> pd.Series:
131
+ """Get the highest ATR over a rolling window."""
132
+ return atr.rolling(window=length).max()
133
+
134
+ def add_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
135
+ d = df.copy()
136
+
137
+ # Basic indicators
138
+ d["close"] = d["close"]
139
+ d["high"] = d["high"]
140
+ d["low"] = d["low"]
141
+
142
+ # ATR-14
143
+ tr = pd.concat([
144
+ d["high"] - d["low"],
145
+ (d["high"] - d["close"].shift()).abs(),
146
+ (d["low"] - d["close"].shift()).abs(),
147
+ ], axis=1).max(axis=1)
148
+ d["atr"] = tr.rolling(14).mean()
149
+ d["atr_pct"] = d["atr"] / d["close"] * 100
150
+
151
+ # ZLEMA and volatility bands
152
+ d["zlema"] = self._zlema(d["close"], self.zlema_length)
153
+ atr_high = self._highest_atr(d["atr"], self.zlema_length * 3)
154
+ d["volatility"] = atr_high * self.band_mult
155
+ d["upper_band"] = d["zlema"] + d["volatility"]
156
+ d["lower_band"] = d["zlema"] - d["volatility"]
157
+
158
+ # Trend variable (ZLEMA crossover logic)
159
+ # Long: price crosses ABOVE (zlema - volatility) = trend = 1
160
+ # Short: price crosses BELOW (zlema + volatility) = trend = -1
161
+ d["price_above_lower"] = d["close"] > d["lower_band"]
162
+ d["price_below_upper"] = d["close"] < d["upper_band"]
163
+
164
+ # Detect crossovers: from False to True (crossover), from True to False (crossunder)
165
+ prev_above_lower = d["price_above_lower"].shift(1).fillna(False)
166
+ prev_below_upper = d["price_below_upper"].shift(1).fillna(True) # default to True so first bar isn't a crossunder
167
+
168
+ d["crossover_lower"] = (~prev_above_lower) & d["price_above_lower"]
169
+ d["crossunder_upper"] = prev_below_upper & (~d["price_below_upper"])
170
+
171
+ # Initialize trend
172
+ d["trend"] = 0
173
+ trend_val = 0
174
+ for i in range(len(d)):
175
+ if d["crossover_lower"].iloc[i]:
176
+ trend_val = 1
177
+ if d["crossunder_upper"].iloc[i]:
178
+ trend_val = -1
179
+ d.loc[d.index[i], "trend"] = trend_val
180
+
181
+ # Volume
182
+ d["volume_sma"] = d["volume"].rolling(20).mean()
183
+
184
+ # Swing structure (for SL placement)
185
+ d["swing_low5"] = d["low"].rolling(5).min()
186
+ d["swing_high5"] = d["high"].rolling(5).max()
187
+
188
+ return d.dropna()
189
+
190
+ def signal_for_row(self, row: pd.Series, htf_row: Optional[pd.Series] = None) -> int:
191
+ """
192
+ Generate entry signal based on ZLEMA strategy.
193
+ Returns: 1 (LONG), -1 (SHORT), 0 (NO SIGNAL)
194
+ """
195
+ close = float(row["close"])
196
+ zlema = float(row["zlema"])
197
+ volatility = float(row["volatility"])
198
+ upper_band = float(row["upper_band"])
199
+ lower_band = float(row["lower_band"])
200
+
201
+ # Basic filters
202
+ atr_pct = float(row["atr_pct"])
203
+ if atr_pct < 0.2: # Reduced filter for SOL volatility
204
+ return 0
205
+
206
+ # LONG entry signal: price crosses above lower band
207
+ if row.get("crossover_lower", False):
208
+ return 1
209
+
210
+ # SHORT entry signal: price crosses below upper band
211
+ if row.get("crossunder_upper", False):
212
+ return -1
213
+
214
+ return 0
215
+
216
+ def risk_levels(
217
+ self,
218
+ entry: float,
219
+ atr: float,
220
+ side: int,
221
+ atr_pct: float = 0.0,
222
+ row: Optional[pd.Series] = None,
223
+ ) -> Tuple[float, float]:
224
+ """
225
+ ZLEMA risk management:
226
+ Stop: 1.5 ATR (responsive to volatility)
227
+ TP: 3.0 ATR (R:R 2.0:1)
228
+ """
229
+ sl_mult = 1.5
230
+ tp_mult = 3.0
231
+
232
+ if side == 1: # LONG
233
+ sl = entry - sl_mult * atr
234
+ tp = entry + tp_mult * atr
235
+ else: # SHORT
236
+ sl = entry + sl_mult * atr
237
+ tp = entry - tp_mult * atr
238
+
239
+ return sl, tp
240
+
241
+ def trail_stop(self, side: int, entry: float, current_sl: float, close: float, atr: float) -> float:
242
+ """
243
+ Trail stop at 2.0 ATR once 1.5 ATR in profit.
244
+ """
245
+ if side == 1: # LONG
246
+ if close >= entry + 1.5 * atr:
247
+ new_sl = close - 2.0 * atr
248
+ return max(current_sl, new_sl)
249
+ else: # SHORT
250
+ if close <= entry - 1.5 * atr:
251
+ new_sl = close + 2.0 * atr
252
+ return min(current_sl, new_sl)
253
+ return current_sl
254
+
255
+ def build_intent(self, coin: str, timeframe: str, row: pd.Series) -> Optional[TradeIntent]:
256
+ """
257
+ Build a TradeIntent from current row state.
258
+ """
259
+ signal = self.signal_for_row(row)
260
+ if signal == 0:
261
+ return None
262
+
263
+ side = "long" if signal == 1 else "short"
264
+ entry = float(row["close"])
265
+ sl, tp = self.risk_levels(entry, float(row["atr"]), signal, float(row["atr_pct"]), row)
266
+
267
+ # Confidence based on trend strength and volatility
268
+ trend = float(row["trend"])
269
+ volatility = float(row["volatility"])
270
+ atr = float(row["atr"])
271
+
272
+ # Higher confidence if volatility band is tight relative to ATR
273
+ bb_to_atr = volatility / atr if atr > 0 else 1.0
274
+ vol_factor = max(0.0, 1.0 - bb_to_atr)
275
+ confidence = round(min(0.95, 0.55 + vol_factor * 0.35), 2)
276
+
277
+ return TradeIntent(coin, timeframe, side, entry, sl, tp, confidence)
278
+
279
+
280
+
281
+
282
+