Deribit Execution, Operations and Deployment
1. Deribit API — Overview
1.1 API Structure
Deribit offers WebSocket for real-time trading and REST as an
alternative. The project's client uses REST
(src/core/deribit_client.py) for simplicity and reliability.
Base URLs:
- Testnet: https://test.deribit.com/api/v2
- Live: https://www.deribit.com/api/v2
Authentication: client_credentials with client_id and
client_secret. The token expires every 15 minutes; the client
auto-refreshes.
def authenticate(self) -> bool:
response = self._request("public/auth", {
"grant_type": "client_credentials",
"client_id": self.api_key,
"client_secret": self.api_secret,
})
self._access_token = response.get("access_token")
return bool(self._access_token)
1.2 Futures Instruments
Deribit offers two kinds of futures:
- Perpetual (e.g. BTC-PERPETUAL): no expiry, funding rate every 8h
- Dated futures (e.g. BTC-28MAR25): settle at the index price on expiry
The project uses perpetuals exclusively (more liquid, no rollover risk).
Funding rate for perpetuals:
Funding_payment = Position_size * mark_price * funding_rate
funding_rate = clamp(interest_rate - premium_rate, -0.05%, +0.05%) per 8h
Example: long 0.1 BTC, mark=$64500, funding=0.01%:
A marginal cost, but it accumulates on positions held for days.1.3 Available Orders
# Market order (taker, immediate, 0.05% fee)
client.buy(instrument="BTC-PERPETUAL", amount=0.01, order_type="market")
# Limit order (maker if it rests in the book, 0.03% fee)
client.buy(instrument="BTC-PERPETUAL", amount=0.01,
order_type="limit", price=64000.0)
# Stop order (triggers when price touches the trigger level)
client.buy(instrument="BTC-PERPETUAL", amount=0.01,
order_type="stop_limit", stop_price=63500.0, price=63480.0)
# Cancel a single order
client.cancel(order_id="ETH-12345")
# Cancel everything (emergency)
client.cancel_all()
1.4 Position Close
# Close with a market order
client.close_position(instrument="BTC-PERPETUAL", type_="market")
# Or sell/buy an equal and opposite size
2. OrderManager — Execution Flow
2.1 execute_generic_trade()
The core method called by every strategy:
def execute_generic_trade(
self,
instrument_name: str,
direction: str, # "buy" or "sell"
quantity: float,
entry_type: str = "market",
price: float = None,
stop_loss: float = None,
take_profit: float = None,
label: str = "",
) -> Tuple[bool, str]:
# 1. Entry order
# 2. Stop Loss (opposite reduce-only stop order, with retries)
# 3. Take Profit (opposite reduce-only limit order)
# 4. Register SL/TP in the OrderRegistry for orphan cleanup
...
The current implementation (see 05_risk_sizing.md) enters at market, places the SL as a reduce-only stop_market with 3× retry and, if the stop cannot be placed, emergency-closes the just-opened position — a position is never left without a stop on the venue.
2.2 The Orphan Order Problem and Its Solution
The problem: Deribit automatically executes the SL or TP when the price reaches them. But the companion order stays open (orphaned).
Typical scenario:
t=0: Buy 0.01 BTC-PERPETUAL @ 64500, SL @ 63800 (order-124), TP @ 65800 (order-125)
t=100: Price drops to 63800 → Deribit executes the SL (order-124) → position closed
t=101: order-125 (TP @ 65800) stays open ← ORPHAN
It locks margin and risks an accidental fill if the price bounces
Solution with the OrderRegistry:
# PositionMonitor._check_orphan_orders() runs every 30s
def check_orphan_orders(self, currencies=["BTC", "ETH"]):
# 1. Currently open positions
open_positions = self.get_open_futures_positions()
open_trade_ids = {p["label"] for p in open_positions}
# 2. Registered trades no longer in a position = orphans
registered_trades = self.registry.get_all()
for trade_id, companions in registered_trades.items():
if trade_id not in open_trade_ids:
# Position closed → cancel companion orders
if companions.get("sl_id"):
self.client.cancel(companions["sl_id"])
if companions.get("tp_id"):
self.client.cancel(companions["tp_id"])
self.registry.unregister(trade_id)
3. PositionMonitor — Position Monitoring
3.1 get_open_futures_positions()
def get_open_futures_positions(self) -> List[dict]:
"""
Fetches open positions across all futures/perpetual pairs.
Automatically filters out options (recognizable by the name format,
e.g. BTC-28MAR25-60000-C).
"""
positions = []
for currency in ["BTC", "ETH"]:
result = self.client._request("private/get_positions", {
"currency": currency,
"kind": "future" # excludes options
})
for pos in result.get("result", []):
if pos.get("size", 0) != 0: # non-zero positions only
positions.append({
"instrument_name": pos["instrument_name"],
"size": pos["size"], # positive=long, negative=short
"direction": "buy" if pos["size"] > 0 else "sell",
"mark_price": pos["mark_price"],
"pnl": pos.get("floating_profit_loss", 0),
"entry_price": pos.get("average_price", 0),
"label": pos.get("label", ""),
})
return positions
3.2 Portfolio Summary
def get_portfolio_summary(self) -> dict:
positions = self.get_open_futures_positions()
total_exposure = sum(abs(p["size"]) * p["mark_price"] for p in positions)
total_pnl = sum(p["pnl"] for p in positions)
return {
"total_positions": len(positions),
"total_notional": total_exposure,
"total_pnl_usd": total_pnl,
"positions": positions,
}
4. Monitoring and Alerting
4.1 TelegramAlerts
The alerting system uses a background thread with a queue to: - Never block the trading loop (even if Telegram is slow/down) - Rate-limit to avoid spam
class TelegramAlerts:
def __init__(self, bot_token, chat_id):
self._queue = queue.Queue()
self._thread = threading.Thread(target=self._worker, daemon=True)
self._thread.start()
self._rate_limits = {} # {event_type: last_sent_ts}
def _worker(self):
while True:
try:
msg_type, message = self._queue.get(timeout=1.0)
self._send_telegram(message)
except queue.Empty:
continue
def _send_telegram(self, message: str):
url = f"https://api.telegram.org/bot{self._bot_token}/sendMessage"
requests.post(url, json={
"chat_id": self._chat_id,
"text": message,
"parse_mode": "Markdown",
}, timeout=10)
Alert types:
| Method | Trigger | Rate limit |
|---|---|---|
send_trade_open() |
Every trade opened | 1/trade |
send_trade_close() |
Every trade closed | 1/trade |
send_daily_pnl() |
Daily report (23:50) | 1/hour |
send_regime_change() |
Regime change | 1/min |
send_api_error() |
Deribit API error | 1/min |
send_emergency() |
Emergency close | Never (CRITICAL bypass) |
send_kill_switch() |
Kill switch activation | Never (CRITICAL bypass) |
4.2 Streamlit Dashboard
Six pages: Live trades (with order reconciliation), Risk & Exposure, Trade history, Market context, Settings (guard-railed .env editor), Actions (kill switch, manual closes) — details in 01_architecture.md.
5. Deployment
5.1 Docker
The reference deployment uses the repository's docker-compose.yml:
three services from the same image — bot (async trading),
dashboard (Streamlit on localhost:8501, to be exposed only behind a
tunnel + authentication) and collector (positioning-data archive).
Key design points:
- ./data, ./logs and ./.env are bind mounts: data survives
container re-creation
- no env_file: in the compose — the bot reads the .env FILE with
load_dotenv() at every start, so dashboard-editor changes take
effect on restart
- restart: unless-stopped acts as the supervisor for the
"edit .env → restart request → restart" flow
docker compose up -d # start the 3 services
docker compose ps # status + healthchecks
docker compose logs -f bot # follow the bot logs
docker compose down # stop (data stays on the host)
5.2 Pre-Live Checklist
Before switching from testnet to live, verify:
- [ ]
dry_run_strategies.py --backtestpasses without errors - [ ]
dry_run_full.py --duration 120generates at least 1 signal - [ ]
.envhasDERIBIT_ENV=prod(not testnet) - [ ] Live API keys created on Deribit with correct permissions (Trade, Read)
- [ ]
INITIAL_EQUITYset to the real account balance - [ ]
MAX_DAILY_LOSS_PCT=0.02(2%) for the first days - [ ]
MAX_OPEN_TRADES=1for the first days - [ ] Telegram bot active and receiving a test message
- [ ] Host with automatic backup of the
.envfile - [ ] Monitoring alert: if the bot is unresponsive for >5min → manual notification
5.3 Historical Data Collection
To collect historical tick data (needed for real backtests):
# Collects data for 24 hours (leave running in the background)
python scripts/dry_run_data.py --duration 86400 --symbol BTCUSDT
# Data is saved to data/raw/binance_ticks.duckdb
# Roughly 50MB/day for BTCUSDT
# Verify collected data
python -c "
import duckdb
conn = duckdb.connect('data/raw/binance_ticks.duckdb')
print(conn.execute('SELECT COUNT(*) FROM agg_trades').fetchone())
print(conn.execute('SELECT MIN(timestamp_ms), MAX(timestamp_ms) FROM agg_trades').fetchone())
"
5.4 Log Management
Logs are written to logs/ with a RotatingFileHandler:
- logs/async_bot.log (max 5MB, 5 backups)
- logs/dry_run_full.log
- logs/dry_run_orderflow.log
Quick scan of recent errors:
6. Troubleshooting
| Issue | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: websockets |
Dependency not installed | pip install websockets>=12.0 |
[OrderBook] sequence gap |
No initial REST snapshot | Normal at startup; the book stabilizes in ~60s |
Authentication failed |
Wrong or expired API key | Regenerate on Deribit, update .env |
| Kill switch active | Daily loss limit reached | Wait for midnight (automatic reset) or deactivate manually from the dashboard |
| Trades not executed | can_open_new_position() = False |
Check the logs for the specific reason |
| Persistent orphan orders | Registry out of sync | Restart the bot; the PositionMonitor cleans up at startup |
UnicodeEncodeError on Windows |
cp1252 terminal | set PYTHONIOENCODING=utf-8 before starting |
| Few signals | Scoring threshold too high | Lower min_score_threshold in the ScoringEngine or wait for warmup |
| Strategy always BLOCKED | Wrong regime or low score | Check the scoring state + regime status |