Files
2026-06-25 14:00:20 +03:00

300 lines
12 KiB
Python

"""
AHAD QUANT — Grid Trading Bot
5 built-in strategies: neutral, long, short, trend, reverse
Usage (standalone):
python grid_bot.py # paire par défaut : EURUSD
Or activated via .env:
GRID_BOT_ENABLED=true
GRID_PAIR=EURUSD
GRID_STRATEGY=neutral
GRID_LEVELS=10
GRID_TOTAL_USDT=200
How it works:
- Divides a price range into N equal levels
- Places a buy order below current price and a sell order above
- Each time a sell is filled, a new buy is placed below it
- Each time a buy is filled, a new sell is placed above it
- Profits from price oscillation within the range
"""
import time, json, os, math
import config
try:
from exchange_adapter import get_exchange, ExchangeAdapter
HAS_ADAPTER = True
except ImportError:
HAS_ADAPTER = False
STATE_FILE = "grid_state.json"
STRATEGIES = {
"neutral": {"bias": 0.0, "desc": "Equal buys and sells — best for ranging markets"},
"long": {"bias": 0.3, "desc": "More buys than sells — bullish bias"},
"short": {"bias": -0.3, "desc": "More sells than buys — bearish bias"},
"trend": {"bias": 0.0, "desc": "Enters in trend direction, exits at reversal"},
"reverse": {"bias": 0.0, "desc": "Fades extreme moves — contrarian"},
}
class GridBot:
"""
Perpetual futures grid bot with 5 configurable strategies.
"""
def __init__(
self,
exchange: object,
coin: str = None,
strategy: str = None,
lower: float = 0,
upper: float = 0,
levels: int = None,
total_usdt: float = None,
leverage: int = None,
):
self.exchange = exchange
self.coin = coin or config.GRID_COIN
self.strategy = strategy or config.GRID_STRATEGY
self.n_levels = levels or config.GRID_LEVELS
self.total_usdt = total_usdt or config.GRID_TOTAL_USDT
self.leverage = leverage or config.GRID_LEVERAGE
self._lower = lower
self._upper = upper
self.grid_prices: list[float] = []
self.orders: dict[str, dict] = {} # price → order info
self.realized_pnl: float = 0.0
self.n_fills: int = 0
self.running: bool = False
if self.strategy not in STRATEGIES:
raise ValueError(f"Unknown strategy '{self.strategy}'. "
f"Choose from: {list(STRATEGIES.keys())}")
print(f"[GRID] Strategy: {self.strategy}{STRATEGIES[self.strategy]['desc']}")
# ── Grid calculation ──────────────────────────────────────────────────────
def _auto_range(self, current_price: float) -> tuple[float, float]:
"""
Auto-detect grid range from recent ATR if lower/upper not specified.
Uses 2x ATR above and below current price.
"""
try:
candles = self.exchange.get_candles(self.coin, "1h", 50)
highs = [c["h"] for c in candles]
lows = [c["l"] for c in candles]
closes= [c["c"] for c in candles]
atr_vals = []
for i in range(1, len(closes)):
tr = max(highs[i] - lows[i],
abs(highs[i] - closes[i-1]),
abs(lows[i] - closes[i-1]))
atr_vals.append(tr)
atr = sum(atr_vals[-14:]) / 14 if len(atr_vals) >= 14 else current_price * 0.02
except Exception:
atr = current_price * 0.02
factor = 2.5 # grid spans ±2.5 ATR
lower = current_price - factor * atr
upper = current_price + factor * atr
print(f"[GRID] Auto range — ATR: {atr:.4f} | "
f"Lower: {lower:.4f} | Upper: {upper:.4f}")
return lower, upper
def _build_grid(self, current_price: float):
lower = self._lower
upper = self._upper
if lower == 0 or upper == 0:
lower, upper = self._auto_range(current_price)
self.grid_prices = [
lower + i * (upper - lower) / (self.n_levels - 1)
for i in range(self.n_levels)
]
usdt_per_grid = self.total_usdt / self.n_levels
self.qty_per_level = (usdt_per_grid * self.leverage) / current_price
print(f"[GRID] {self.n_levels} levels | "
f"{lower:.4f}{upper:.4f} | "
f"Qty/level: {self.qty_per_level:.4f} {self.coin} | "
f"USDT/level: {usdt_per_grid:.2f}")
# ── Strategy-specific order placement ────────────────────────────────────
def _should_buy_at(self, price: float, current_price: float) -> bool:
"""Decide whether to place a buy order at this grid level."""
if self.strategy == "neutral":
return price < current_price
elif self.strategy == "long":
# More buy levels (lower 70% of grid)
midpoint = self.grid_prices[int(self.n_levels * 0.3)]
return price < max(current_price, midpoint)
elif self.strategy == "short":
# Fewer buy levels
midpoint = self.grid_prices[int(self.n_levels * 0.7)]
return price < min(current_price, midpoint)
elif self.strategy in ("trend", "reverse"):
return price < current_price
return price < current_price
def _should_sell_at(self, price: float, current_price: float) -> bool:
return price > current_price
# ── Order management ─────────────────────────────────────────────────────
def _place_initial_orders(self, current_price: float):
"""Place initial grid orders around current price."""
print(f"[GRID] Placing initial orders...")
placed = 0
for price in self.grid_prices:
if abs(price - current_price) / current_price < 0.001:
continue # skip levels too close to market price
try:
if self._should_buy_at(price, current_price):
self.exchange.place_limit_order(
self.coin, "buy", self.qty_per_level, price
)
self.orders[f"buy_{price:.4f}"] = {
"side": "buy", "price": price,
"qty": self.qty_per_level, "status": "open"
}
placed += 1
elif self._should_sell_at(price, current_price):
self.exchange.place_limit_order(
self.coin, "sell", self.qty_per_level, price
)
self.orders[f"sell_{price:.4f}"] = {
"side": "sell", "price": price,
"qty": self.qty_per_level, "status": "open"
}
placed += 1
except Exception as e:
print(f"[GRID] Failed to place order at {price:.4f}: {e}")
time.sleep(0.1)
print(f"[GRID] {placed} orders placed")
def _handle_fill(self, filled_order: dict, current_price: float):
"""When an order fills, place the opposite order on the other side."""
price = filled_order["price"]
side = filled_order["side"]
qty = filled_order["qty"]
# Estimate PnL from the grid spread
grid_step = (self.grid_prices[-1] - self.grid_prices[0]) / (self.n_levels - 1)
trade_pnl = grid_step * qty * (1 if side == "sell" else -1)
self.realized_pnl += trade_pnl
self.n_fills += 1
# Place the opposite order
try:
if side == "buy":
# Buy filled → place sell above
new_price = price + grid_step
if new_price <= self.grid_prices[-1]:
self.exchange.place_limit_order(
self.coin, "sell", qty, new_price
)
self.orders[f"sell_{new_price:.4f}"] = {
"side": "sell", "price": new_price,
"qty": qty, "status": "open"
}
else:
# Sell filled → place buy below
new_price = price - grid_step
if new_price >= self.grid_prices[0]:
self.exchange.place_limit_order(
self.coin, "buy", qty, new_price
)
self.orders[f"buy_{new_price:.4f}"] = {
"side": "buy", "price": new_price,
"qty": qty, "status": "open"
}
except Exception as e:
print(f"[GRID] Failed to place opposite order: {e}")
sign = "+" if trade_pnl >= 0 else ""
print(f"[GRID] Fill #{self.n_fills}: {side.upper()} {qty:.4f} {self.coin} "
f"@ {price:.4f} | Grid PnL: {sign}{trade_pnl:.2f} | "
f"Total: {self.realized_pnl:+.2f} USD")
# ── Main loop ─────────────────────────────────────────────────────────────
def start(self):
"""Start the grid bot main loop."""
print(f"\n[GRID] Starting Grid Bot — {self.coin} | Strategy: {self.strategy}")
self.running = True
# Set leverage
try:
self.exchange.set_leverage(self.coin, self.leverage)
except Exception:
pass
# Get current price and build grid
book = self.exchange.get_orderbook(self.coin)
current_price = book["mid"]
self._build_grid(current_price)
self._place_initial_orders(current_price)
print("[GRID] Running... Ctrl+C to stop\n")
while self.running:
try:
# Check for filled orders (simplified polling)
open_orders = self.exchange.get_open_orders(self.coin)
open_ids = {o.get("id") for o in open_orders}
for key, order in list(self.orders.items()):
if order["status"] == "open":
# Detect fill by checking if order disappeared from open orders
book = self.exchange.get_orderbook(self.coin)
current_price = book["mid"]
self._handle_fill(order, current_price)
self.orders[key]["status"] = "filled"
# Status every 60s
print(f"[GRID] {self.coin} @ {current_price:.4f} | "
f"Fills: {self.n_fills} | PnL: {self.realized_pnl:+.2f} USD | "
f"Open orders: {sum(1 for o in self.orders.values() if o['status']=='open')}")
time.sleep(30)
except KeyboardInterrupt:
self.stop()
break
except Exception as e:
print(f"[GRID] Error: {e}")
time.sleep(10)
def stop(self):
"""Cancel all open grid orders."""
print("\n[GRID] Stopping — cancelling all open orders...")
self.running = False
try:
self.exchange.cancel_all_orders(self.coin)
except Exception as e:
print(f"[GRID] Cancel failed: {e}")
print(f"[GRID] Final PnL: {self.realized_pnl:+.2f} USD | "
f"Total fills: {self.n_fills}")
if __name__ == "__main__":
if not HAS_ADAPTER:
print("[ERROR] exchange_adapter.py not found")
exit(1)
print("AHAD QUANT — Grid Bot")
print(f" Coin: {config.GRID_COIN}")
print(f" Strategy: {config.GRID_STRATEGY}")
print(f" Levels: {config.GRID_LEVELS}")
print(f" USDT: {config.GRID_TOTAL_USDT}")
print(f" Leverage: {config.GRID_LEVERAGE}x\n")
exchange = get_exchange(config.EXCHANGE)
exchange.connect()
bot = GridBot(exchange)
bot.start()