LMEX.AI / Introduction
LMEX API
Live Markets
Real-time prices pulled directly from the LMEX API — Spot, Perpetual Futures, Commodities, and US Equities. Updates every 10 seconds.
Connecting…
Crypto Spot
BTC-USDT
Spot
ETH-USDT
Spot
SOL-USDT
Spot
Crypto Perpetuals
BTC-PERP
Perp
ETH-PERP
Perp
SOL-PERP
Perp
Commodities
OIL-PERP
WTI
BRENT-PERP
Brent
US Equities (Tokenised Perps)
AAPL-PERP
Equity
TSLA-PERP
Equity
NVDA-PERP
Equity
💡 Prices are fetched from the public LMEX REST API — no API key required. Some symbols may not be listed yet on LMEX; those cards will show "—" until the market is live.
Algo Academy  ·  Open Source  ·  Powered by LMEX

Algorithmic Trading
for LMEX Exchange

Build, backtest and deploy Python trading bots across spot, futures, commodities and equities — powered by LMEX.IO and AI.

Start Trading on LMEX.IO ↗
30+
Strategies
4
Asset Classes
REST
+ WebSocket API
Free
Open Source
What You'll Learn
Foundation
  • Connect to LMEX REST and WebSocket APIs
  • Authenticate, sign requests, handle rate limits
  • Stream live order book, trades, OHLCV data
  • Place, modify and cancel spot & futures orders
  • Manage positions, margin, leverage on futures
Advanced
  • Build production-grade algo bots in Python
  • Implement 30+ quantitative strategies with code
  • Backtest strategies on historical LMEX data
  • Deploy AI/LLM agents for autonomous trading
  • Apply professional risk management frameworks
Learning Path
01
API Setup
Keys + Auth
02
Connect
REST + WS
03
Strategy
Pick + Code
04
Backtest
Validate
05
Paper Trade
Simulate Live
06
Deploy
Go Live
Quick Facts — LMEX Markets
MarketVersionAPI Base URLMin NotionalMaker FeeTaker Fee
Spotv3.3api.lmex.io/spot/api/v3.3>5 USDTMakerTaker
Futures (Perp)v2.3api.lmex.io/futures/api/v2.3>5 USDTMakerTaker
WebSocketwss://ws.lmex.io/futures & /spot
💡 Always verify current endpoints and fees from the official LMEX API documentation at lmex.io/api-docs
Why LMEX.AI Academy
🆓
Free for Everyone
This entire library — every strategy, every code example, every API guide — is completely free. No paywalls, no premium tiers. Open access for all skill levels from day one.
Crypto — Spot & Perpetual Futures
Full coverage of LMEX spot markets (BTC-USDT, ETH-USDT) and perpetual futures (BTC-PERP, ETH-PERP) — including leverage, funding rates, TP/SL, and position management.
Commodities
Trade WTI crude and Brent crude directly as USDT-margined perpetuals on LMEX — OIL-PERP and BRENT-PERP. Includes full spread arbitrage strategies with working Python code.
US Equities
Access tokenised US equity perpetuals on LMEX — trade the price action of major stocks and indices alongside your crypto and commodities positions from a single unified account.
Professional Risk Disclosure
Educational Content Only. The strategies presented in this academy were developed and documented by professional algorithmic traders and quantitative analysts. They are provided for educational and informational purposes only and do not constitute financial advice, investment recommendations, or an offer to buy or sell any financial instrument.

Trading involves substantial risk of loss. All trading — including spot, futures, perpetuals, commodities, and equity derivatives — involves the risk of losing some or all of your invested capital. Leveraged products such as perpetual futures can amplify both gains and losses, and losses may exceed your initial deposit. Past performance of any strategy, signal, or model is not indicative of future results and may differ materially under live market conditions.

Due care and professional judgement are required. These strategies must not be deployed with real capital without thorough testing in a paper trading environment first, independent verification of all code and logic, a clear understanding of the risks involved, and appropriate position sizing relative to your personal risk tolerance and financial situation.

No liability. LMEX.AI, its contributors, and the LMEX exchange accept no responsibility or liability for any trading losses, damages, or adverse outcomes resulting from the use of, or reliance on, any content, code, or strategy published in this academy. Markets are dynamic and strategies may lose effectiveness without warning due to changing market conditions, reduced liquidity, or structural regime changes.
Not Financial Advice Capital at Risk Paper Trade First Past Performance ≠ Future Results Seek Professional Advice Use Risk Management
Environment Setup
Get Python, dependencies, and your project scaffold ready before writing a single API call.
Install Dependencies
Beginner
  1. Python 3.9+
    Install from python.org. Verify with python --version
  2. Create a virtual environment
    bash
    python -m venv lmex-env source lmex-env/bin/activate # Mac/Linux lmex-env\Scripts\activate # Windows
  3. Install core packages
    bash
    pip install requests websockets pandas numpy python-dotenv
  4. Create project structure
    text
    lmex-bot/ ├── .env # API keys (never commit) ├── lmex_client.py # REST client wrapper ├── lmex_ws.py # WebSocket client ├── strategies/ # Strategy modules │ ├── base.py │ ├── ema_cross.py │ └── grid.py ├── risk.py # Risk management ├── backtest.py # Backtester └── bot.py # Main entry point
  5. Store API keys securely
    .env
    LMEX_API_KEY=your_api_key_here LMEX_SECRET_KEY=your_secret_key_here LMEX_PAPER=true # set false for live trading
⚠️ NEVER commit your .env file. Add it to .gitignore immediately. API key leaks will drain your account.
API Authentication
LMEX uses HMAC-SHA384 signing. Every private request requires three headers: request-api, request-nonce, and request-sign. Public endpoints need no auth.
Getting API Keys
Beginner
  1. Log in to LMEX.IO
    Navigate to Account → API tab
  2. Click "New API"
    Set a label and permissions: Read for data, Trading for order execution. Leave Withdraw OFF.
  3. Save your passphrase immediately
    The passphrase is shown once only — copy it now. This is your secret key for signing.
  4. Whitelist your IP
    Restrict keys to known server IPs. Never skip this step for production bots.
  5. Store in .env
    Never hardcode keys in your source. Use environment variables.
⚠️ The passphrase (secret key) is shown only once during creation. If lost, you must delete and recreate the API key.
HMAC-SHA384 Signing — Correct Implementation
Intermediate
Signature = HMAC-SHA384(secretKey, path + nonce + body). Note: query parameters are excluded from the signed text. Body is empty string for GET requests.
python — lmex_client.py
import hmac, hashlib, time, requests, json import os from dotenv import load_dotenv load_dotenv() class LMEXClient: BASE_SPOT = "https://api.lmex.io/spot" BASE_FUTURES = "https://api.lmex.io/futures" def __init__(self): self.api_key = os.getenv("LMEX_API_KEY") self.secret = os.getenv("LMEX_SECRET_KEY") # passphrase = secret self.session = requests.Session() def _sign(self, path: str, nonce: str, body: str = "") -> str: """HMAC-SHA384(secret, path + nonce + body). Query params are excluded from the signed text.""" msg = path + nonce + body return hmac.new( self.secret.encode(), msg.encode(), hashlib.sha384 ).hexdigest() def _headers(self, path: str, body: str = "") -> dict: nonce = str(int(time.time() * 1000)) return { "request-api": self.api_key, "request-nonce": nonce, "request-sign": self._sign(path, nonce, body), "Content-Type": "application/json" } # ── Public Spot (no auth) ─────────────────────────────── def get_price(self, symbol="BTC-USDT"): """e.g. symbol="BTC-USDT" """ r = self.session.get(f"{self.BASE_SPOT}/api/v3.3/price", params={"symbol": symbol}) r.raise_for_status() return r.json() def get_orderbook(self, symbol="BTC-USDT", limit_bids=20, limit_asks=20): r = self.session.get(f"{self.BASE_SPOT}/api/v3.3/orderbook/L2", params={"symbol": symbol, "limit_bids": limit_bids, "limit_asks": limit_asks}) r.raise_for_status() return r.json() def get_ohlcv(self, symbol="BTC-USDT", resolution=60, start=None, end=None): """Resolution: 1,5,15,30,60,240,360,1440,10080,43200 (minutes).""" params = {"symbol": symbol, "resolution": resolution} if start: params["start"] = start if end: params["end"] = end r = self.session.get(f"{self.BASE_SPOT}/api/v3.3/ohlcv", params=params) r.raise_for_status() return r.json() def get_market_summary(self, symbol=None): params = {"symbol": symbol} if symbol else {} r = self.session.get(f"{self.BASE_SPOT}/api/v3.3/market_summary", params=params) r.raise_for_status() return r.json() # ── Private Spot (signed) ─────────────────────────────── def place_order(self, symbol, side, order_type, size, price=None, post_only=False, time_in_force="GTC", cl_order_id=None): path = "/api/v3.3/order" payload = { "symbol": symbol, "side": side.upper(), "type": order_type.upper(), "size": size, "time_in_force": time_in_force, "postOnly": post_only, "txType": "LIMIT" } if price: payload["price"] = price if cl_order_id: payload["clOrderId"] = cl_order_id body = json.dumps(payload, separators=(",",":")) r = self.session.post(f"{self.BASE_SPOT}{path}", data=body, headers=self._headers(path, body)) r.raise_for_status() return r.json() def cancel_order(self, symbol, order_id=None, cl_order_id=None): path = "/api/v3.3/order" payload = {"symbol": symbol} if order_id: payload["orderID"] = order_id if cl_order_id: payload["clOrderID"] = cl_order_id body = json.dumps(payload, separators=(",",":")) r = self.session.delete(f"{self.BASE_SPOT}{path}", data=body, headers=self._headers(path, body)) r.raise_for_status() return r.json() def get_open_orders(self, symbol): path = "/api/v3.3/user/open_orders" r = self.session.get(f"{self.BASE_SPOT}{path}", params={"symbol": symbol}, headers=self._headers(path)) r.raise_for_status() return r.json() def get_trade_history(self, symbol): path = "/api/v3.3/user/trade_history" r = self.session.get(f"{self.BASE_SPOT}{path}", params={"symbol": symbol}, headers=self._headers(path)) r.raise_for_status() return r.json() # Quick test client = LMEXClient() price = client.get_price("BTC-USDT") print(f"BTC-USDT: {price}")
💡 The signature algorithm is HMAC-SHA384 — not SHA256. Query parameters are excluded from the signed string. Only path + nonce + body are signed.
Rate Limits
CategoryPer API KeyPer User
Query endpoints15 req/s30 req/s
Order endpoints75 req/s75 req/s
⚠️ Orders below 5 USDT notional value are flagged as spam. Spam orders always pay taker fees, post-only spam orders are rejected. Repeated spam activity can get an account trading-disabled.
REST API — Spot
Base URL: https://api.lmex.io/spot  ·  Version: v3.3  ·  Symbol format: BTC-USDT
Public Endpoints (No Auth)
GET
/api/v3.3/market_summary?symbol=BTC-USDT
Full market summary — last price, 24h volume, bid/ask, open interest. Omit symbol for all markets.
GET
/api/v3.3/price?symbol=BTC-USDT
Current market price for a symbol
GET
/api/v3.3/ohlcv?symbol=BTC-USDT&resolution=60
Candlestick data. Resolution (minutes): 1, 5, 15, 30, 60, 240, 360, 1440, 10080, 43200. Default 300 candles.
GET
/api/v3.3/orderbook/L2?symbol=BTC-USDT&limit_bids=20&limit_asks=20
Level 2 orderbook snapshot with configurable depth on each side
GET
/api/v3.3/orderbook?symbol=BTC-USDT&group=0
Orderbook with grouping. group=0–9 controls price bucketing granularity.
GET
/api/v3.3/trades?symbol=BTC-USDT
Recent public trade fills
GET
/api/v3.3/time
Server time in epoch milliseconds — sync before signing requests
Private Endpoints (Signed)
POST
/api/v3.3/order
Create a new order — LIMIT, MARKET, OCO, Stop, Trigger, Peg/Algo
GET
/api/v3.3/order?symbol=BTC-USDT&orderID=xxx
Query a specific order by orderID or clOrderID
PUT
/api/v3.3/order
Amend an open order — update price, size, or trigger price
DEL
/api/v3.3/order
Cancel an order by orderID or clOrderID
POST
/api/v3.3/order/peg
Create algo/peg order (trailing stop, peg to mid, etc.)
POST
/api/v3.3/order/cancelAllAfter
Dead Man's Switch — cancel all orders after N milliseconds (safety net for bots)
GET
/api/v3.3/user/open_orders?symbol=BTC-USDT
All currently open orders for a symbol
GET
/api/v3.3/user/trade_history?symbol=BTC-USDT
Historical fills with fees, prices, and timestamps
GET
/api/v3.3/user/fees
Current maker/taker fee rates for your account
Order Placement — Full Reference
Intermediate
python — spot order examples
# ── MARKET BUY ────────────────────────────────────── client.place_order( symbol="BTC-USDT", side="BUY", order_type="MARKET", size=0.001 # in base asset (BTC) ) # ── LIMIT BUY (Post-Only maker) ───────────────────── client.place_order( symbol="BTC-USDT", side="BUY", order_type="LIMIT", size=0.001, price=60000.00, post_only=True, # maker fee — rejected if would match immediately time_in_force="GTC" ) # ── DEAD MAN'S SWITCH (bot safety net) ────────────── # Cancel all orders if bot goes silent for 60 seconds path = "/api/v3.3/order/cancelAllAfter" body = json.dumps({"timeout": 60000}, separators=(",",":")) client.session.post( f"{client.BASE_SPOT}{path}", data=body, headers=client._headers(path, body)) # ── AMEND ORDER ───────────────────────────────────── path = "/api/v3.3/order" body = json.dumps({ "symbol": "BTC-USDT", "orderID": "your-order-id", "price": 61000, # new price "size": 0.002 # new size }, separators=(",",":")) client.session.put(f"{client.BASE_SPOT}{path}", data=body, headers=client._headers(path, body))
Spot Order Fields Reference
FieldTypeRequiredDescription
symbolstringYesMarket symbol e.g. BTC-USDT
sidestringYesBUY or SELL
typestringYesLIMIT, MARKET, OCO
sizenumberYesOrder size in base asset
pricenumberLIMIT onlyLimit price
time_in_forcestringNoGTC, IOC, FOK, HALFMIN, FIVEMIN, HOUR, TWELVEHOUR, DAY, WEEK, MONTH
postOnlyboolNoMaker-only — rejected if would match
txTypestringNoLIMIT (default), STOP, TRIGGER
stopPricenumberSTOP onlyTrigger price for stop orders
clOrderIdstringNoYour custom order ID (for tracking)
💡 v3.3 deprecates the old size/filledSize/remainingSize fields. Use originalOrderBaseSize, currentOrderBaseSize, totalFilledBaseSize in order query responses.
REST API — Futures
Base URL: https://api.lmex.io/futures  ·  Version: v2.3  ·  Symbol format: BTC-PERP, OIL-PERP, BRENT-PERP
Public Endpoints (No Auth)
GET
/api/v2.3/market_summary?symbol=BTC-PERP
Market summary — mark price, index price, funding rate, open interest. Omit symbol for all markets.
GET
/api/v2.3/price?symbol=BTC-PERP
Current market price for a futures symbol
GET
/api/v2.3/ohlcv?symbol=BTC-PERP&resolution=60
Candlestick data. Resolution (minutes): 1, 5, 15, 30, 60, 240, 360, 1440, 10080, 43200
GET
/api/v2.3/orderbook/L2?symbol=BTC-PERP
L2 orderbook snapshot
GET
/api/v2.3/trades?symbol=BTC-PERP
Recent public trade fills
GET
/api/v2.3/funding_history?symbol=BTC-PERP
Historical funding rates (8-hour settlement records)
Private Endpoints (Signed)
POST
/api/v2.3/order
Create futures order — LIMIT, MARKET, OCO, Stop, Trigger, with optional TP/SL inline
GET
/api/v2.3/order?symbol=BTC-PERP&orderID=xxx
Query order status by orderID or clOrderID
PUT
/api/v2.3/order
Amend open order — price, size, or trigger price
DEL
/api/v2.3/order
Cancel an order by orderID or clOrderID
POST
/api/v2.3/order/cancelAllAfter
Dead Man's Switch — cancel all futures orders after timeout (bot safety)
POST
/api/v2.3/order/bind_tpsl
Attach Take Profit / Stop Loss to an existing position
GET
/api/v2.3/user/open_orders?symbol=BTC-PERP
All open futures orders
GET
/api/v2.3/user/positions?symbol=BTC-PERP
Current open position — size, entry price, unrealised PnL, liquidation price
POST
/api/v2.3/user/close_position
Close position at market price
GET
/api/v2.3/user/leverage?symbol=BTC-PERP
Get current leverage setting
POST
/api/v2.3/user/leverage
Set leverage for a symbol
GET
/api/v2.3/user/risk_limit?symbol=BTC-PERP
Query risk limit tier
GET
/api/v2.3/user/position_mode
Query current position mode: ONE_WAY or HEDGE
GET
/api/v2.3/user/fees
Current maker/taker fees for your futures account
POST
/api/v2.3/user/wallet/transfer
Transfer funds between spot and futures wallets
Futures Client — Full Implementation
Intermediate
python — lmex_futures.py
import json from lmex_client import LMEXClient class LMEXFutures(LMEXClient): """Futures-specific methods extending the base client.""" def _f(self, path, method="GET", params=None, body=None): """Helper: futures request with correct signing.""" body_str = json.dumps(body, separators=(",",":")) if body else "" headers = self._headers(path, body_str) url = f"{self.BASE_FUTURES}{path}" r = self.session.request(method, url, params=params, data=body_str if body else None, headers=headers) r.raise_for_status() return r.json() # ── Market Data ───────────────────────────────────── def get_funding_history(self, symbol="BTC-PERP"): return self._f("/api/v2.3/funding_history", params={"symbol": symbol}) def get_futures_price(self, symbol="BTC-PERP"): return self._f("/api/v2.3/price", params={"symbol": symbol}) def get_futures_ohlcv(self, symbol="BTC-PERP", resolution=60, start=None, end=None): params = {"symbol": symbol, "resolution": resolution} if start: params["start"] = start if end: params["end"] = end return self._f("/api/v2.3/ohlcv", params=params) # ── Order Management ──────────────────────────────── def place_futures_order(self, symbol, side, order_type, size, price=None, reduce_only=False, post_only=False, time_in_force="GTC", tx_type="LIMIT", stop_price=None, take_profit_price=None, stop_loss_price=None, cl_order_id=None): body = { "symbol": symbol, "side": side.upper(), "type": order_type.upper(), "size": size, "time_in_force": time_in_force, "postOnly": post_only, "reduceOnly": reduce_only, "txType": tx_type, "trailValue": 0 } if price: body["price"] = price if stop_price: body["stopPrice"] = stop_price if take_profit_price: body["takeProfitPrice"] = take_profit_price if stop_loss_price: body["stopLossPrice"] = stop_loss_price if cl_order_id: body["clOrderId"] = cl_order_id return self._f("/api/v2.3/order", "POST", body=body) def cancel_futures_order(self, symbol, order_id=None, cl_order_id=None): body = {"symbol": symbol} if order_id: body["orderID"] = order_id if cl_order_id: body["clOrderID"] = cl_order_id return self._f("/api/v2.3/order", "DELETE", body=body) # ── Position Management ───────────────────────────── def get_position(self, symbol="BTC-PERP"): return self._f("/api/v2.3/user/positions", params={"symbol": symbol}) def close_position(self, symbol, side=None, position_mode="ONE_WAY"): body = {"symbol": symbol, "positionMode": position_mode} if side: body["side"] = side return self._f("/api/v2.3/user/close_position", "POST", body=body) def set_leverage(self, symbol, leverage): body = {"symbol": symbol, "leverage": leverage} return self._f("/api/v2.3/user/leverage", "POST", body=body) def bind_tpsl(self, symbol, take_profit_price=None, stop_loss_price=None, tp_trigger="markPrice", sl_trigger="markPrice"): body = {"symbol": symbol} if take_profit_price: body["takeProfitPrice"] = take_profit_price body["takeProfitTrigger"] = tp_trigger if stop_loss_price: body["stopLossPrice"] = stop_loss_price body["stopLossTrigger"] = sl_trigger return self._f("/api/v2.3/order/bind_tpsl", "POST", body=body) def transfer(self, currency, amount, direction="SPOT_TO_FUTURES"): """Transfer between wallets. direction: SPOT_TO_FUTURES or FUTURES_TO_SPOT""" body = {"currency": currency, "amount": amount, "direction": direction} return self._f("/api/v2.3/user/wallet/transfer", "POST", body=body) # ── Usage examples ────────────────────────────────── fut = LMEXFutures() # Market order with TP/SL inline fut.place_futures_order( symbol="BTC-PERP", side="BUY", order_type="MARKET", size=1, take_profit_price=70000, stop_loss_price=58000) # Set leverage then place limit order fut.set_leverage("OIL-PERP", 3) fut.place_futures_order("OIL-PERP", "BUY", "LIMIT", 10, price=75.50) # Bind TP/SL to existing BRENT-PERP position fut.bind_tpsl("BRENT-PERP", take_profit_price=95.00, tp_trigger="markPrice", stop_loss_price=70.00, sl_trigger="lastPrice")
Futures Order Fields Reference
FieldTypeRequiredNotes
symbolstringYese.g. BTC-PERP, OIL-PERP, BRENT-PERP
sidestringYesBUY or SELL
typestringYesLIMIT, MARKET, OCO
sizeintegerYesContract size. Notional must exceed 5 USDT
pricenumberLIMIT onlyOrder price
time_in_forcestringNoGTC, IOC, FOK, HALFMIN, FIVEMIN, HOUR, TWELVEHOUR, DAY, WEEK, MONTH
txTypestringNoLIMIT (default), STOP, TRIGGER
stopPricenumberSTOP/OCOTrigger price for stop orders
triggerPricenumberTRIGGER/OCOTrigger price for trigger orders
postOnlyboolNoMaker-only — rejected if would match immediately
reduceOnlyboolNoOnly reduces existing position
takeProfitPricenumberNoInline TP trigger price (markPrice by default)
stopLossPricenumberNoInline SL trigger price (markPrice by default)
positionModestringNoONE_WAY (default), HEDGE, ISOLATED
clOrderIdstringNoCustom order ID for tracking
💡 v2.3 deprecates size/fillSize/originalSize. Use originalOrderSize, currentOrderSize, filledSize, totalFilledSize in query responses.
WebSocket Streams
Real-time data via persistent WebSocket connections. LMEX uses a subscribe/unsubscribe pattern with named topics. Auth required for private topics.
WebSocket Client — LMEX Native
Intermediate
python — lmex_ws.py
import asyncio, json, websockets, hmac, hashlib, time, os class LMEXWebSocket: """LMEX Futures WebSocket client. Subscribe format: {"op": "subscribe", "args": ["topic:symbol"]} Auth format: {"op": "authKeyExpires", "args": [api_key, nonce, sig]} """ WS_URL_FUTURES = "wss://ws.lmex.io/futures" WS_URL_SPOT = "wss://ws.lmex.io/spot" def __init__(self, on_message, market="futures"): self.on_message = on_message self.url = self.WS_URL_FUTURES if market == "futures" else self.WS_URL_SPOT self.ws = None self._subs = [] async def connect(self): while True: try: async with websockets.connect(self.url, ping_interval=20, ping_timeout=60) as ws: self.ws = ws await self._resubscribe() async for msg in ws: await self.on_message(json.loads(msg)) except Exception as e: print(f"WS disconnected: {e} — reconnecting in 5s") await asyncio.sleep(5) async def subscribe(self, topics: list): """topics e.g. ["orderBookL2Api:BTC-PERP_0", "tradeHistoryApi:BTC-PERP"]""" self._subs.extend(topics) if self.ws: await self.ws.send(json.dumps( {"op": "subscribe", "args": topics})) async def _resubscribe(self): if self._subs: await self.ws.send(json.dumps( {"op": "subscribe", "args": self._subs})) async def auth(self): """Authenticate for private topics (notificationApiV2, fills). Signature = HMAC-SHA384(secret, api_key + nonce) """ api_key = os.getenv("LMEX_API_KEY") secret = os.getenv("LMEX_SECRET_KEY") nonce = str(int(time.time() * 1000)) sig = hmac.new( secret.encode(), (api_key + nonce).encode(), hashlib.sha384 ).hexdigest() await self.ws.send(json.dumps({ "op": "authKeyExpires", "args": [api_key, nonce, sig] })) # ── Usage ────────────────────────────────────────── async def handle(msg): topic = msg.get("topic", "") if "tradeHistoryApi" in topic: for trade in msg.get("data", []): print(f"Trade: {trade['price']} x {trade['size']} {trade['side']}") elif "orderBookL2Api" in topic: data = msg.get("data", {}) print(f"Book: bids={len(data.get('bids',[]))} asks={len(data.get('asks',[]))}") elif topic == "notificationApiV2": print(f"Order update: {msg}") elif topic == "fills": print(f"Fill: {msg}") async def main(): wsc = LMEXWebSocket(on_message=handle, market="futures") # Public topics await wsc.subscribe([ "orderBookL2Api:BTC-PERP_0", # full L2 orderbook, depth=0 (all) "tradeHistoryApi:BTC-PERP", # public trade feed "tradeHistoryApi:OIL-PERP", "tradeHistoryApi:BRENT-PERP", "update:BTC-PERP", # incremental orderbook updates ]) # Private topics (auth first) await wsc.connect() # connects, then authenticates, then subscribes asyncio.run(main())
Futures WebSocket Topics
Topic PatternDescriptionAuthNotes
orderBookApi:BTC-PERP_0Orderbook grouped — format: symbol_grouping (0–9)Snapshot on subscribe
orderBookL2Api:BTC-PERP_0L2 orderbook — format: symbol_depth (0 = full book)First msg is snapshot, then deltas
update:BTC-PERPIncremental orderbook updatesFirst msg = snapshot, subsequent = delta. Re-subscribe if seqNum gap detected.
tradeHistoryApi:BTC-PERPPublic trade fills for a marketPushes on every trade
notificationApiV2Your order status updatesUse v2 — v1 deprecated
fillsYour trade execution fillsFires when your order matches
allPositionAll positions pushed periodicallyFor portfolio monitoring
⚠️ For update:BTC-PERP (incremental orderbook): check seqNum is always prevSeqNum + 1. If a gap is detected, or if a crossed orderbook occurs, unsubscribe and re-subscribe immediately to get a fresh snapshot.
💡 A price of 0 in an orderbook delta means that price level should be removed from your local orderbook copy. The size field in deltas is always the absolute new quantity, not a change.
Get New Strategies in Your Inbox
Weekly algo strategies, LMEX API updates and trading insights. No spam.
📈 Trend Following
Capture directional market moves. Works best in sustained trending environments. Struggles in ranging/choppy markets.
〰️
EMA Crossover
Golden/death cross on fast/slow EMAs
Beginner SpotFutures
Buy when fast EMA crosses above slow EMA (golden cross). Sell on death cross. Add volume filter to reduce false signals.
python
import pandas as pd def ema_cross_signal(df, fast=9, slow=21): df['ema_fast'] = df['close'].ewm(span=fast).mean() df['ema_slow'] = df['close'].ewm(span=slow).mean() df['signal'] = 0 df.loc[df.ema_fast > df.ema_slow, 'signal'] = 1 # LONG df.loc[df.ema_fast < df.ema_slow, 'signal'] = -1 # SHORT df['crossover'] = df['signal'].diff() # entry points return df # Typical params: (9,21), (20,50), (50,200)
📊
Supertrend
ATR-based dynamic trend filter
Intermediate SpotFutures
Uses Average True Range to set adaptive stop levels above/below price. Flips signal when price crosses the band.
python
def supertrend(df, period=10, multiplier=3): hl2 = (df['high'] + df['low']) / 2 atr = df['close'].rolling(period).std() # simplified ATR upper = hl2 + multiplier * atr lower = hl2 - multiplier * atr trend = pd.Series(0, index=df.index) for i in range(1, len(df)): if df['close'].iloc[i] > upper.iloc[i-1]: trend.iloc[i] = 1 # uptrend elif df['close'].iloc[i] < lower.iloc[i-1]: trend.iloc[i] = -1 # downtrend else: trend.iloc[i] = trend.iloc[i-1] return trend
💥
Donchian Breakout
Buy N-day highs, sell N-day lows
Beginner Futures
Classic Turtle Trader strategy. Enter long on N-day high breakout, exit on M-day low. Works best on daily/4H timeframes.
python
def donchian_signal(df, entry_n=20, exit_n=10): df['high_n'] = df['high'].rolling(entry_n).max() df['low_n'] = df['low'].rolling(entry_n).min() df['exit_low'] = df['low'].rolling(exit_n).min() df['entry_long'] = df['close'] >= df['high_n'].shift(1) df['exit_long'] = df['close'] <= df['exit_low'].shift(1) return df
📉
MACD Trend Filter
Histogram for momentum confirmation
Intermediate SpotFutures
MACD line crosses signal line above zero — long. Below zero — short. Use MACD histogram slope for early entries.
python
def macd(df, fast=12, slow=26, signal=9): e_fast = df['close'].ewm(span=fast).mean() e_slow = df['close'].ewm(span=slow).mean() macd_line = e_fast - e_slow sig_line = macd_line.ewm(span=signal).mean() histogram = macd_line - sig_line return macd_line, sig_line, histogram
📖 Deep Dive
Strategy Mechanics

Trend-following strategies exploit the statistical tendency for assets to continue moving in one direction. EMA crossovers detect momentum shifts — when the fast line crosses the slow, market consensus has shifted. Supertrend uses ATR to set adaptive trailing stops that widen during high volatility and tighten during calm periods. Donchian breakouts capture the "new high = continuation" pattern popularised by Turtle Traders. MACD histogram measures acceleration of a trend, not just direction — ideal for timing entries within an established move.

✅ Ideal Conditions

ADX > 25. Higher-than-average volume confirming the move. Best on 1H–4H–Daily timeframes. Avoid first 30 minutes of session open. BTC and ETH during trending periods are ideal.

📐 Position Sizing

Risk 1–2% per trade. Formula: Qty = (Account × 0.01) ÷ (Entry − Stop). For EMA cross, stop below the most recent swing low (longs). For Supertrend, the band IS the stop — size accordingly. Scale into winners: add a second unit when the trade moves 1R in your favour. Max 3 units. Reduce size 50% if the last 5 trades had fewer than 40% winners.

⚠️ Risk Parameters

Whipsaw is the primary risk in flat choppy markets. Apply ADX > 25 filter before taking signals. Trend strategies inherently have 40–55% win rates compensated by large R:R (1:2+) — never risk more than 2%. Gap risk on daily bars can blow through stops. Three consecutive losses may signal a regime change — pause and reassess.

🔄 Mean Reversion
Bet that price returns to its mean after extremes. Works best in ranging, sideways markets with clear support/resistance.
Bollinger Band Reversion
Beginner
Buy at lower band, sell at upper band. Use %B indicator and bandwidth filter to avoid false signals in trending conditions.
python
def bollinger_bands(df, period=20, std_dev=2.0): df['sma'] = df['close'].rolling(period).mean() df['std'] = df['close'].rolling(period).std() df['upper'] = df['sma'] + std_dev * df['std'] df['lower'] = df['sma'] - std_dev * df['std'] df['pct_b'] = (df['close'] - df['lower']) / (df['upper'] - df['lower']) df['bw'] = (df['upper'] - df['lower']) / df['sma'] # bandwidth # Signal: buy squeeze + touch lower, sell at upper df['buy'] = (df['close'] <= df['lower']) & (df['bw'] < 0.05) df['sell'] = (df['close'] >= df['upper']) & (df['bw'] < 0.05) return df
RSI Divergence Strategy
Intermediate
Oversold RSI (<30) for entries, overbought (>70) for exits. Add price/RSI divergence detection for high-probability setups.
python
def rsi(df, period=14): delta = df['close'].diff() gain = delta.clip(lower=0).rolling(period).mean() loss = (-delta).clip(lower=0).rolling(period).mean() rs = gain / loss.replace(0, 1e-10) return 100 - 100 / (1 + rs) def rsi_divergence(df): """Bullish divergence: lower price low, higher RSI low.""" df['rsi'] = rsi(df) price_lower = df['close'] < df['close'].shift(1) rsi_higher = df['rsi'] > df['rsi'].shift(1) df['bull_div'] = price_lower & rsi_higher & (df['rsi'] < 35) return df
Z-Score Mean Reversion
Advanced
Trade when price deviates more than N standard deviations from its rolling mean. Exit at mean reversion.
python
def zscore_strategy(df, window=30, entry_z=2.0, exit_z=0.5): rolling_mean = df['close'].rolling(window).mean() rolling_std = df['close'].rolling(window).std() df['zscore'] = (df['close'] - rolling_mean) / rolling_std df['entry_short'] = df['zscore'] > entry_z # overbought → short df['entry_long'] = df['zscore'] < -entry_z # oversold → long df['exit_signal'] = df['zscore'].abs() < exit_z # mean restored return df
📖 Deep Dive
Strategy Mechanics

Mean reversion exploits the statistical tendency for price to return to a historical average after extreme deviations. Bollinger Bands define the normal range as ±2 standard deviations — roughly 95% of price action stays within. RSI divergence catches hidden momentum shifts: price makes a new low but RSI makes a higher low, revealing exhausted selling pressure. Z-score analysis quantifies exactly how statistically extreme a price level is, allowing consistent entry thresholds across any asset.

✅ Ideal Conditions

Sideways/ranging markets. ADX < 20. Price oscillating between clear S/R. Avoid immediately after major news events. Best on 15m–1H for crypto. BB squeeze filter significantly improves signal quality.

📐 Position Sizing

Risk 0.5–1% per trade — smaller than trend strategies because reversions can fail and become trends. Size = (Account × 0.005) ÷ (Entry − Stop). BB stop: above upper band on short, below lower band on long. Consider scaling in: take 50% at first signal, add 50% if price extends further (improving average). Hard stop at Z = ±3.5.

⚠️ Risk Parameters

In trending markets price walks the bands — touching the lower BB repeatedly without reversing. Only take reversion trades when ADX < 20 and price is range-bound. RSI can stay oversold for extended periods during strong trends. Never average down beyond 2 adds. Three consecutive losers beyond plan — assume trend regime and pause.

💨 Momentum
Follow the strength. Assets moving strongly in one direction tend to continue. Buy strength, sell weakness.
Rate of Change (ROC) Momentum
Intermediate
python
def roc_momentum(df, period=14, threshold=2.0): """Rate of Change: percent change over N periods.""" df['roc'] = (df['close'] / df['close'].shift(period) - 1) * 100 df['roc_ma'] = df['roc'].rolling(5).mean() # smooth df['long'] = df['roc_ma'] > threshold df['short'] = df['roc_ma'] < -threshold return df def cross_asset_momentum(returns_dict: dict, top_n=3): """Rank assets by 20-day returns. Long top N, short bottom N.""" ranked = sorted(returns_dict.items(), key=lambda x: x[1], reverse=True) longs = [a[0] for a in ranked[:top_n]] shorts = [a[0] for a in ranked[-top_n:]] return longs, shorts
Stochastic RSI (StochRSI)
Intermediate
python
def stoch_rsi(df, rsi_period=14, stoch_period=14, smooth_k=3): rsi_vals = rsi(df, rsi_period) rsi_min = rsi_vals.rolling(stoch_period).min() rsi_max = rsi_vals.rolling(stoch_period).max() stoch = (rsi_vals - rsi_min) / (rsi_max - rsi_min + 1e-10) K = stoch.rolling(smooth_k).mean() # %K (smoothed) D = K.rolling(3).mean() # %D (signal) # Buy: K crosses above D from below 20 # Sell: K crosses below D from above 80 return K, D
📖 Deep Dive
Strategy Mechanics

Momentum strategies are built on the empirical observation that assets moving strongly in one direction tend to continue. Rate of Change (ROC) quantifies this as a pure percentage — no smoothing, no lag — making it honest about how much ground an asset has covered. Cross-asset momentum ranks all pairs by recent return and longs the top performers. StochRSI applies the stochastic oscillator to RSI itself, making it more sensitive — ideal as a precise entry trigger within an established trend rather than as a standalone system.

✅ Ideal Conditions

Strong directional trend with follow-through volume. ROC consistently above zero. Works best during momentum phases of market cycle. Avoid late-cycle blow-off tops. Best timeframes: 4H/Daily for ROC; 5m–1H for StochRSI triggers.

📐 Position Sizing

Single-asset momentum: risk 1–1.5% per trade, stop at the swing low predating the momentum move. Cross-asset portfolio: equal-weight allocations, never exceed 5% per position. Rebalance weekly. If ROC > 2× the asset's average ROC, reduce size by 25% — momentum is already extended. StochRSI stops: just below the crossover candle's low for longs.

⚠️ Risk Parameters

Momentum crashes are violent and sudden — when crowded momentum trades reverse, they reverse hard. Always maintain a hard stop. Momentum strategies underperform dramatically in ranging markets — use ADX or trend filter to disable signals when ADX < 20. Factor crowding: when many algos buy the same top-ranked assets, correlations spike and diversification disappears.

🔲 Grid Trading
Place buy and sell orders at preset price levels. Profits from price oscillation. No directional prediction needed.
Arithmetic Grid Bot
Beginner
Place orders at equal price intervals between a defined range. Each filled buy creates a sell order one level higher, and vice versa.
python — strategies/grid.py
class GridBot: def __init__(self, client, symbol, low, high, num_grids, total_usdt): self.client = client self.symbol = symbol self.low = low self.high = high self.grids = num_grids self.qty_each = total_usdt / num_grids # USDT per level self.levels = self._calc_levels() self.orders = {} # {price: order_id} def _calc_levels(self): step = (self.high - self.low) / self.grids return [round(self.low + i * step, 2) for i in range(self.grids + 1)] def setup(self, current_price): """Place initial grid orders above and below current price.""" for lvl in self.levels: qty = round(self.qty_each / lvl, 5) side = "BUY" if lvl < current_price else "SELL" order = self.client.place_order( self.symbol, side, "LIMIT", qty, lvl) self.orders[lvl] = order['orderId'] print(f"Grid set: {len(self.levels)} levels between {self.low}-{self.high}") def on_fill(self, filled_price, side): """When a level fills, place the opposite order one level away.""" step = (self.high - self.low) / self.grids if side == "BUY": counter_price = round(filled_price + step, 2) counter_side = "SELL" else: counter_price = round(filled_price - step, 2) counter_side = "BUY" if self.low <= counter_price <= self.high: qty = round(self.qty_each / counter_price, 5) order = self.client.place_order( self.symbol, counter_side, "LIMIT", qty, counter_price) self.orders[counter_price] = order['orderId'] # Deploy: BTC/USDT grid, $200 budget, 10 levels, $60k-$70k range bot = GridBot(client, "BTCUSDT", 60000, 70000, 10, 200) bot.setup(current_price=64500)
💡 Use geometric grids (equal % spacing) for volatile assets. Arithmetic grids work better for stable pairs like USDC/USDT.
📖 Deep Dive
Strategy Mechanics

Grid trading profits purely from price oscillation — no directional prediction required. The bot places buys below current price and sells above at fixed intervals. When a buy fills, it places a sell one level higher; when that fills, another buy one level lower. Each round trip earns one grid interval as profit. The strategy profits from any movement — up or down — as long as price stays within the defined range. Geometric grids space levels equally in percentage terms (better for volatile assets); arithmetic grids in dollar terms.

✅ Ideal Conditions

Ranging, oscillating market. Clear S/R boundaries. ADX < 20. Stable or gently trending pairs. Avoid during scheduled major events (FOMC, CPI) where breakouts are likely.

📐 Position Sizing

Total USDT ÷ Number of grids = USDT per level. Minimum per level must exceed $5 (LMEX minimum). Recommended: at least 20 grid levels, budget ≥ $200. Capital reserve rule: hold enough to fill ALL buy orders simultaneously if price drops to range bottom. Formula: Reserve = num_grids × qty_per_level × bottom_price. Start with ≤ 5% of total capital.

⚠️ Risk Parameters

Catastrophic risk: price breaks out of range — you end up holding a full directional position at the worst level. Always set a hard stop at the bottom of the grid range. Define range using 3–6 months of historical price data. Avoid deploying grids when BB squeeze signals a breakout is imminent. Review and reset the range at least monthly.

🏦 Market Making
Quote both sides of the order book. Earn the spread on every round trip. Requires tight risk controls and low-latency execution.
Basic Spread Market Maker
Advanced
python
class MarketMaker: def __init__(self, client, symbol, spread_bps=10, order_size=0.001, max_position=0.01, inventory_skew=True): self.client = client self.symbol = symbol self.spread = spread_bps / 10000 # 10bps → 0.001 self.size = order_size self.max_pos = max_position self.skew = inventory_skew self.position = 0 self.bid_id = None self.ask_id = None def get_mid(self): book = self.client.get_orderbook(self.symbol, limit=5) best_bid = float(book['bids'][0][0]) best_ask = float(book['asks'][0][0]) return (best_bid + best_ask) / 2 def calc_quotes(self, mid): half = mid * self.spread / 2 # Inventory skew: widen spread on the heavy side skew_offset = (self.position / self.max_pos) * half * 0.5 \ if self.skew else 0 bid = round(mid - half - skew_offset, 2) ask = round(mid + half - skew_offset, 2) return bid, ask def refresh_quotes(self): """Cancel old quotes, post fresh ones.""" if self.bid_id: self.client.cancel_order(self.symbol, self.bid_id) if self.ask_id: self.client.cancel_order(self.symbol, self.ask_id) mid = self.get_mid() bid, ask = self.calc_quotes(mid) if abs(self.position) < self.max_pos: b = self.client.place_order(self.symbol, "BUY", "LIMIT", self.size, bid) a = self.client.place_order(self.symbol, "SELL", "LIMIT", self.size, ask) self.bid_id = b['orderId'] self.ask_id = a['orderId'] print(f"Quoting {bid} / {ask} | pos={self.position:.4f} | mid={mid:.2f}")
Market making without inventory control will lead to large directional exposure. Always implement a hard position limit and emergency hedge mechanism.
📖 Deep Dive
Strategy Mechanics

Market making profits from the bid-ask spread on every completed round trip. The bot quotes a bid below mid-price and an ask above — when both fill, the difference minus fees is profit. Inventory skew is critical: as the bot accumulates a directional position it adjusts quotes, widening the spread on the heavy side to discourage further accumulation. On LMEX, the maker fee rebate means every filled limit order generates positive economics — earning from the exchange itself while capturing the spread.

✅ Ideal Conditions

Liquid markets with tight natural spreads (BTC-USDT, ETH-USDT spot). Normal volatility — not during news events. Active trading hours (London/NY overlap, 13:00–17:00 UTC). Avoid illiquid long-tail pairs where adverse selection is highest.

📐 Position Sizing

Quote size = 0.1–0.5% of capital per side. Start extremely small — infrastructure issues cause double-fills or missed cancellations. Max inventory: 1% of total capital. Hard inventory limit: if position reaches max, cancel all quotes on the heavy side immediately. Spread minimum must exceed 2 × taker fee (0.2% on LMEX). Recommended spread: 10–20 bps.

⚠️ Risk Parameters

Adverse selection — informed traders consistently trade against quotes, leaving losing inventory. If fills are consistently profitable for the counterparty, you're being adversely selected. Inventory risk: directional moves generate mark-to-market losses. Latency risk: slow cancel-replace means stale quotes fill at unfavourable prices after news. Quote refresh rate < 500ms recommended. Emergency shutdown: cancel all orders first, inventory second.

💰 DCA & Accumulation
Dollar-cost average and smart accumulation strategies. Reduce timing risk by spreading entries.
Intelligent DCA Bot
Beginner
Regular buys with RSI override — buy more aggressively on dips, pause buys on extreme overbought readings.
python
import schedule, time class SmartDCA: def __init__(self, client, symbol, base_usdt=50, interval_hours=24): self.client = client self.symbol = symbol self.base = base_usdt self.history = [] def _get_rsi(self): klines = self.client.get_klines(self.symbol, '1d', limit=20) closes = pd.Series([float(k[4]) for k in klines]) return rsi(pd.DataFrame({'close': closes})).iloc[-1] def execute(self): rsi_val = self._get_rsi() # Adjust buy size based on RSI if rsi_val < 25: multiplier = 3.0 # extreme dip → 3x elif rsi_val < 35: multiplier = 2.0 # dip → 2x elif rsi_val > 75: multiplier = 0.0 # overbought → skip elif rsi_val > 65: multiplier = 0.5 # hot → half size else: multiplier = 1.0 # normal → base size usdt_amount = self.base * multiplier if usdt_amount > 0: price = float(self.client.get_ticker(self.symbol)['lastPrice']) qty = round(usdt_amount / price, 5) order = self.client.place_order(self.symbol, "BUY", "MARKET", qty) self.history.append({'price': price, 'qty': qty, 'usdt': usdt_amount, 'rsi': rsi_val}) print(f"DCA: bought {qty} @ {price} (RSI={rsi_val:.1f}, {usdt_amount} USDT)")
📖 Deep Dive
Strategy Mechanics

Dollar-cost averaging removes the hardest problem in trading: timing. Purchasing a fixed dollar amount at regular intervals automatically buys more units when price is low and fewer when high — lowering average cost over time without prediction. The intelligent DCA layer adds RSI conditioning: when an asset is statistically oversold (RSI < 30), the bot increases purchase size by 2–3×, concentrating capital at the best entry points. Particularly powerful during bear markets when emotional traders are selling.

✅ Ideal Conditions

Bear markets and sideways accumulation phases. Long-term (months to years) horizon. High-conviction assets with fundamental value. Never use for speculative altcoins. Works best when capital is genuinely not needed for 12+ months.

📐 Position Sizing

Base purchase = fixed % of monthly discretionary budget. Never exceed 10% of total portfolio in a single DCA campaign. RSI multipliers: 3× at RSI < 25, 2× at RSI < 35, 1× at RSI 35–65, 0.5× at RSI > 65, 0× at RSI > 75. Maximum total exposure cap: never hold more than 20% of portfolio in any single asset regardless of RSI. Track cost basis carefully.

⚠️ Risk Parameters

DCA does not protect against a fundamentally broken asset. Apply only to high-conviction, liquid assets (BTC, ETH on LMEX). The strategy can create significant unrealised losses before recovering — ensure months-to-years runway to withstand drawdowns. Never use leverage in a DCA strategy. Maximum total loss threshold: if the position is down 60% from inception, pause and reassess the thesis.

⚖️ Statistical Arbitrage
Exploit statistical relationships between assets. Trade deviations from historical correlations.
Pairs Trading (Spread Arb)
Advanced
python
from statsmodels.tsa.stattools import coint import numpy as np class PairsTrader: def __init__(self, client, asset_a, asset_b, entry_z=2.0, exit_z=0.5, window=60): self.client = client self.A = asset_a # e.g. "BTCUSDT" self.B = asset_b # e.g. "ETHUSDT" self.entry_z = entry_z self.exit_z = exit_z self.window = window def test_cointegration(self, prices_a, prices_b): """Returns True if pair is cointegrated (p < 0.05).""" _, p_val, _ = coint(prices_a, prices_b) print(f"Cointegration p-value: {p_val:.4f}") return p_val < 0.05 def calc_hedge_ratio(self, prices_a, prices_b): """OLS regression to find hedge ratio.""" return np.polyfit(prices_b, prices_a, 1)[0] def get_signal(self, prices_a, prices_b): hedge = self.calc_hedge_ratio(prices_a, prices_b) spread = prices_a - hedge * prices_b z = (spread - spread.mean()) / spread.std() current_z = z.iloc[-1] if current_z > self.entry_z: return "SHORT_A_LONG_B" elif current_z < -self.entry_z: return "LONG_A_SHORT_B" elif abs(current_z) < self.exit_z: return "EXIT" else: return "HOLD"
💡 Good pairs on LMEX: BTC/ETH, ETH/BNB, BTC/SOL. Re-test cointegration weekly as relationships drift over time.
📖 Deep Dive
Strategy Mechanics

Statistical arbitrage exploits the mean-reverting relationship between two historically correlated assets. When their spread deviates beyond a statistically significant threshold (Z-score), the trader goes long the underperformer and short the outperformer — betting the spread compresses back to its historical mean. The foundation is cointegration, stronger than correlation: two cointegrated series may diverge temporarily but are mathematically bound to reconverge. ADF test confirms this. Natural LMEX pairs: BTC-PERP/ETH-PERP, ETH-PERP/SOL-PERP, OIL-PERP/BRENT-PERP.

✅ Ideal Conditions

Pairs with genuine economic linkage. ADF p-value < 0.05. Sufficient liquidity on both legs. No upcoming catalysts that could permanently change the relationship. Re-test cointegration weekly.

📐 Position Sizing

Position legs in equal dollar value, not equal quantity. Use the hedge ratio (β from OLS regression) to adjust relative sizes. Never risk more than 2% per spread trade. Entry at Z = ±2.0, exit target at Z = 0, hard stop at Z = ±3.5. Keep both legs on ISOLATED margin on LMEX perps. Recalibrate the hedge ratio monthly.

⚠️ Risk Parameters

Correlation breakdown is the catastrophic failure mode — cointegrated assets can decouple permanently due to protocol changes or regulatory events. Re-run ADF test weekly; if p-value > 0.10 close immediately. Both legs moving against simultaneously is possible. Hard time stop: if the trade hasn't converged within 14 days, close regardless of Z-score.

🤖 Bot Frameworks
Production-grade bot architecture. Event-driven, resilient, and ready to deploy.
Production Bot Template
Intermediate
python — bot.py
import asyncio, logging, time from lmex_client import LMEXClient from lmex_ws import LMEXWebSocket from risk import RiskManager logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") log = logging.getLogger(__name__) class LMEXBot: def __init__(self, strategy, symbol: str, paper=True): self.strategy = strategy self.symbol = symbol self.paper = paper self.client = LMEXClient() self.risk = RiskManager(max_drawdown=0.10, max_position=0.02) self.candles = [] self.running = False self.pnl = 0 self.trades = [] async def on_candle(self, candle: dict): """Called every time a new candle closes.""" self.candles.append(candle) if len(self.candles) < 50: return # warm-up signal = self.strategy.get_signal(self.candles) balance = self.client.get_balance() if not self.risk.approve(signal, balance): log.warning("Risk check failed — trade blocked") return if signal == 1 and not self.risk.in_position: await self.execute("BUY") elif signal == -1 and self.risk.in_position: await self.execute("SELL") async def execute(self, side: str): if self.paper: log.info(f"[PAPER] {side} {self.symbol}") return try: price = float(self.client.get_ticker(self.symbol)['lastPrice']) size = self.risk.position_size(price) order = self.client.place_order(self.symbol, side, "MARKET", size) self.trades.append(order) log.info(f"✅ {side} {size} {self.symbol} @ {price}") except Exception as e: log.error(f"Order failed: {e}") async def run(self): self.running = True log.info(f"Bot started | symbol={self.symbol} paper={self.paper}") async def ws_handler(msg): if "k" in msg.get("data", {}): k = msg["data"]["k"] if k["x"]: # candle closed await self.on_candle(k) ws = LMEXWebSocket(on_message=ws_handler) await ws.subscribe([f"{self.symbol.lower()}@kline_1h"]) await ws.connect() # Launch from strategies.ema_cross import EMACross strategy = EMACross(fast=9, slow=21) bot = LMEXBot(strategy, "BTCUSDT", paper=True) asyncio.run(bot.run())
🧠 AI Agent Layer
LLM-powered agents that reason, plan, and adapt. Goes beyond fixed rules — the agent chooses strategy based on market context.
LMEX Trading Agent
Advanced
An LLM-backed agent that reads market conditions, picks a strategy, sizes positions, and manages exits — all autonomously.
python — agents/lmex_agent.py
import anthropic, json from lmex_client import LMEXClient class LMEXTradingAgent: def __init__(self): self.llm = anthropic.Anthropic() self.client = LMEXClient() self.tools = self._define_tools() def _define_tools(self): return [ {"name": "get_market_data", "description": "Fetch OHLCV + ticker + orderbook for a symbol", "input_schema": {"type": "object", "properties": {"symbol": {"type": "string"}, "interval": {"type": "string"}}, "required": ["symbol"]}}, {"name": "place_order", "description": "Execute a trade on LMEX", "input_schema": {"type": "object", "properties": { "symbol": {"type": "string"}, "side": {"type": "string", "enum": ["BUY","SELL"]}, "type": {"type": "string", "enum": ["MARKET","LIMIT"]}, "quantity": {"type": "number"}, "price": {"type": "number"}}, "required": ["symbol","side","type","quantity"]}}, {"name": "get_account", "description": "Get current account balances and positions", "input_schema": {"type": "object", "properties": {}}} ] def _handle_tool(self, name, inp): if name == "get_market_data": ticker = self.client.get_ticker(inp['symbol']) klines = self.client.get_klines(inp['symbol'], inp.get('interval','1h'), 50) return json.dumps({"ticker": ticker, "klines": klines[-5:]}) elif name == "place_order": return json.dumps( self.client.place_order(**inp)) elif name == "get_account": return json.dumps(self.client.get_balance()) def run(self, instruction: str): messages = [{"role": "user", "content": instruction}] system = """You are a professional crypto trading agent on LMEX. Always check market data and account before placing orders. Apply risk management: never risk more than 2% per trade. Use LIMIT orders unless urgency requires MARKET. Explain your reasoning before each action.""" while True: resp = self.llm.messages.create( model="claude-opus-4-5", max_tokens=2000, system=system, tools=self.tools, messages=messages) if resp.stop_reason == "end_turn": break messages.append({"role": "assistant", "content": resp.content}) tool_results = [] for block in resp.content: if block.type == "tool_use": result = self._handle_tool(block.name, block.input) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": result}) messages.append({"role": "user", "content": tool_results}) # Run agent agent = LMEXTradingAgent() agent.run("Analyse BTC/USDT on the 4H chart and if there's a clear" " trend signal, open a position with appropriate sizing.")
🧪 Backtesting
Validate your strategy on historical data before risking real capital. A strategy that doesn't backtest well won't trade well.
Vectorized Backtester
Intermediate
python — backtest.py
import pandas as pd import numpy as np class Backtester: def __init__(self, initial_capital=10000, fee_rate=0.001): self.capital = initial_capital self.fee = fee_rate def run(self, df: pd.DataFrame, signal_col='signal') -> dict: df = df.copy() df['position'] = df[signal_col].shift(1).fillna(0) df['returns'] = df['close'].pct_change() df['strat_ret'] = df['position'] * df['returns'] df['trades'] = df['position'].diff().abs() df['fees'] = df['trades'] * self.fee df['net_ret'] = df['strat_ret'] - df['fees'] df['equity'] = self.capital * (1 + df['net_ret']).cumprod() return { 'total_return': self._total_return(df), 'sharpe': self._sharpe(df), 'max_drawdown': self._max_dd(df), 'win_rate': self._win_rate(df), 'total_trades': int(df['trades'].sum()), 'profit_factor': self._profit_factor(df), 'equity_curve': df['equity'], } def _total_return(self, df): return (df['equity'].iloc[-1] / self.capital - 1) * 100 def _sharpe(self, df, annual=365): r = df['net_ret'].dropna() return np.sqrt(annual) * r.mean() / (r.std() + 1e-10) def _max_dd(self, df): peak = df['equity'].cummax() dd = (df['equity'] - peak) / peak return dd.min() * 100 def _win_rate(self, df): wins = (df['net_ret'] > 0).sum() total = df['net_ret'].count() return wins / total * 100 def _profit_factor(self, df): gross_profit = df[df['net_ret'] > 0]['net_ret'].sum() gross_loss = abs(df[df['net_ret'] < 0]['net_ret'].sum()) return gross_profit / (gross_loss + 1e-10) # Usage client = LMEXClient() klines = client.get_klines("BTCUSDT", "1h", 500) df = pd.DataFrame(klines, columns=['time','open','high','low','close','vol',...]) df = ema_cross_signal(df.astype(float)) bt = Backtester(initial_capital=10000) res = bt.run(df) print(f"Return: {res['total_return']:.1f}% | Sharpe: {res['sharpe']:.2f}") print(f"MaxDD: {res['max_drawdown']:.1f}% | WinRate: {res['win_rate']:.1f}%") print(f"PF: {res['profit_factor']:.2f} | Trades: {res['total_trades']}")
Reading Backtest Results
MetricGoodAcceptableWarning
Sharpe Ratio> 2.01.0 – 2.0< 1.0
Max Drawdown< 10%10% – 25%> 25%
Win Rate> 55%45% – 55%< 40%
Profit Factor> 1.51.1 – 1.5< 1.1
Total Trades> 10030 – 100< 30
⚠️ Overfitting alert: if backtest looks perfect, you've likely overfit. Always validate on out-of-sample data (last 20% of history, unseen).
🛡️ Risk Management
The difference between profitable traders and blown accounts. No strategy works without disciplined risk controls.
Risk Manager
Intermediate
python — risk.py
class RiskManager: def __init__(self, account_equity=10000, risk_per_trade=0.02, # 2% per trade max_drawdown=0.10, # 10% max drawdown max_position=0.20, # 20% max position size max_open_trades=3): self.equity = account_equity self.peak_equity = account_equity self.risk_per_trade = risk_per_trade self.max_dd = max_drawdown self.max_pos = max_position self.max_trades = max_open_trades self.open_trades = 0 self.in_position = False self.halted = False def position_size(self, entry_price: float, stop_price: float) -> float: """Kelly-inspired fixed-fraction sizing with stop loss.""" risk_amount = self.equity * self.risk_per_trade stop_dist = abs(entry_price - stop_price) stop_pct = stop_dist / entry_price qty = risk_amount / (entry_price * stop_pct) max_qty = (self.equity * self.max_pos) / entry_price return round(min(qty, max_qty), 5) def approve(self, signal, current_equity: float) -> bool: """Gate every trade through risk checks.""" self.equity = current_equity # Drawdown halt dd = (self.peak_equity - self.equity) / self.peak_equity if dd >= self.max_dd: print(f"🛑 TRADING HALTED — Drawdown {dd:.1%} exceeded limit") self.halted = True return False # Max concurrent trades if self.open_trades >= self.max_trades: return False if signal != 0 and not self.halted: self.peak_equity = max(self.peak_equity, self.equity) return True return False def calc_stop_loss(self, entry, side, atr, atr_mult=2.0): """ATR-based dynamic stop loss.""" if side == "BUY": return entry - atr * atr_mult else: return entry + atr * atr_mult def trailing_stop(self, entry, current, peak, side, trail_pct=0.02): """Trailing stop that locks in profits.""" if side == "BUY": stop = peak * (1 - trail_pct) return stop > entry, stop else: stop = peak * (1 + trail_pct) return stop < entry, stop
Risk Rules — Cheat Sheet
✅ Always Do
  • Risk ≤2% of account per trade
  • Set stop loss BEFORE entering
  • Use ISOLATED margin on futures
  • Halt bot at 10% drawdown
  • Log every trade with full context
  • Test on paper first — always
  • Keep API keys IP-whitelisted
❌ Never Do
  • No stop loss on leveraged positions
  • Add to losing positions (averaging down)
  • Use CROSSED margin with multiple bots
  • Deploy untested code with real funds
  • Give bots withdraw permissions
  • Trade max leverage (>10x for bots)
  • Ignore drawdown — cut the bot
Leverage vs Liquidation Distance
LeverageLiquidation atMove to Liq.Recommendation
2x-50%50%Conservative — safe
5x-20%20%Moderate — add stops
10x-10%10%Experienced only
20x-5%5%High risk — tight stops
50x+-2%2%Avoid for bots
📋 Paper Trading
Simulate live trading without real money. Match live market prices exactly — prove the bot works before going live.
Paper Trading Engine
Beginner
python — paper_trader.py
from dataclasses import dataclass, field from typing import List import time @dataclass class PaperTrade: side: str symbol: str qty: float entry: float exit_price: float = 0 pnl: float = 0 ts: int = field(default_factory=lambda: int(time.time())) class PaperTrader: def __init__(self, client, starting_usdt=10000, fee=0.001): self.client = client # real client for price feeds self.cash = starting_usdt self.holdings = {} # {symbol: qty} self.fee = fee self.trades = [] self.orders = [] def _price(self, symbol): return float(self.client.get_ticker(symbol)['lastPrice']) def buy(self, symbol, usdt_amount): price = self._price(symbol) cost = usdt_amount * (1 + self.fee) if cost > self.cash: print("❌ Insufficient paper balance") return qty = usdt_amount / price self.cash -= cost self.holdings[symbol] = self.holdings.get(symbol, 0) + qty trade = PaperTrade("BUY", symbol, qty, price) self.trades.append(trade) print(f"📋 [PAPER] BUY {qty:.5f} {symbol} @ {price} | cash={self.cash:.2f}") return trade def sell(self, symbol, qty=None): held = self.holdings.get(symbol, 0) qty = qty or held if qty > held: print("❌ Insufficient paper holdings") return price = self._price(symbol) proceeds = qty * price * (1 - self.fee) self.holdings[symbol] -= qty self.cash += proceeds print(f"📋 [PAPER] SELL {qty:.5f} {symbol} @ {price} | cash={self.cash:.2f}") def portfolio_value(self): total = self.cash for sym, qty in self.holdings.items(): if qty > 0: total += qty * self._price(sym) return total def summary(self, starting=10000): val = self.portfolio_value() pnl_pct = (val / starting - 1) * 100 print(f"Portfolio: ${val:.2f} | P&L: {pnl_pct:+.2f}%") print(f"Cash: ${self.cash:.2f} | Trades: {len(self.trades)}") for sym, qty in self.holdings.items(): print(f" {sym}: {qty:.5f} @ ${self._price(sym):.2f}") # Plug into any bot — just swap PaperTrader for real client paper = PaperTrader(LMEXClient(), starting_usdt=10000) paper.buy("BTCUSDT", 500) # buy $500 worth of BTC paper.summary()
Paper → Live Checklist
Only go live when ALL boxes are checked.
CheckTargetStatus
Paper traded for minimum 2 weeks14 daysTrack manually
Sharpe ratio ≥ 1.5 on live paper data≥ 1.5Calculate from log
Max drawdown ≤ 15% in paper period≤ 15%Check daily
All error paths tested (network, API fail)100%Simulate failures
Stop loss confirmed firing correctly100%Force test
Start with ≤ 5% of total intended capital≤ 5%Scale slowly
Monitoring + alerting set upActiveTelegram/email
🚀 Scale capital in stages: 5% → 25% → 50% → 100%, with 2+ weeks between each step. Slippage and liquidity issues only appear at real scale.
⚡ Scalping
High-frequency, low-timeframe trading on 1m/5m charts. Small targets, tight stops, high volume of trades. Requires fast execution and discipline.
Order Flow Scalper
Advanced
Read the live order book and tape (time & sales) to detect aggressive buyers/sellers. Enter in the direction of the dominant flow before price confirms the move.
python — strategies/scalping.py
from collections import deque import time class OrderFlowScalper: def __init__(self, client, symbol, window_sec=10, imbalance_thresh=0.65): self.client = client self.symbol = symbol self.window = window_sec self.threshold = imbalance_thresh # 65% buy flow → long bias self.tape = deque() # (timestamp, side, qty, price) def on_trade(self, trade: dict): """Feed live aggTrade WebSocket events here.""" now = time.time() side = "buy" if trade["m"] is False else "sell" # m=True means maker=sell self.tape.append((now, side, float(trade["q"]), float(trade["p"]))) # Prune old entries outside window while self.tape and self.tape[0][0] < now - self.window: self.tape.popleft() def get_flow_signal(self): if not self.tape: return 0 buy_vol = sum(t[2] for t in self.tape if t[1] == "buy") sell_vol = sum(t[2] for t in self.tape if t[1] == "sell") total = buy_vol + sell_vol if total == 0: return 0 buy_ratio = buy_vol / total if buy_ratio > self.threshold: return 1 # LONG elif buy_ratio < 1 - self.threshold: return -1 # SHORT return 0 def get_book_pressure(self): """Compare top-5 bid vs ask depth for order book pressure.""" book = self.client.get_orderbook(self.symbol, limit=5) bid_vol = sum(float(b[1]) for b in book["bids"]) ask_vol = sum(float(a[1]) for a in book["asks"]) ratio = bid_vol / (bid_vol + ask_vol) return ratio # > 0.6 = buy pressure, < 0.4 = sell pressure class EMARibbonScalper: """5m scalper using 3-EMA ribbon for entry, 1:1.5 R:R.""" def __init__(self, emas=(8, 13, 21), atr_mult=1.5): self.emas = emas self.atr_mult = atr_mult def get_signal(self, df): import pandas as pd for e in self.emas: df[f'ema{e}'] = df['close'].ewm(span=e).mean() last = df.iloc[-1] aligned_bull = last.ema8 > last.ema13 > last.ema21 aligned_bear = last.ema8 < last.ema13 < last.ema21 atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1] stop = atr * self.atr_mult if aligned_bull: return 1, stop elif aligned_bear: return -1, stop return 0, stop
⚠️ Scalping fees erode edge fast. At 0.1% taker fee, a 0.3% target means 33% goes to fees. Use maker (limit) orders wherever possible to cut costs.
💡 Best scalping pairs on LMEX: BTC/USDT, ETH/USDT on 1m–5m. Look for high volume sessions (London open 08:00–10:00 UTC, NY open 13:00–15:00 UTC).
Tape Reading — Key Signals
Advanced
PatternWhat It MeansAction
Large buy prints hitting ask repeatedlyAggressive accumulationLong bias
Thin ask wall absorbed quicklySupply exhaustedLong breakout likely
Large sell prints lifting bidAggressive distributionShort bias
Iceberg order (refreshing size)Institutional presenceFade or follow direction
Volume spike with no price moveAbsorption — reversal riskWatch for direction change
Thin both sides (low liquidity)Spread widens, slippage highAvoid — widen targets
📖 Deep Dive
Strategy Mechanics

Scalping extracts tiny profits from microstructure inefficiencies at high frequency. Order flow scalping reads the live tape to detect aggressive buyers or sellers — when market buy orders dominate for a sustained window, price is about to be pushed up. This predicts short-term direction before candlestick patterns confirm it. The EMA ribbon (8/13/21 on 5m) filters for alignment: when all three EMAs are stacked bullish AND flow is bullish, the probability of a 3–5 tick gain is significantly elevated. Tape reading patterns (icebergs, absorption, thin walls) add conviction.

✅ Ideal Conditions

High-volume sessions: London open (08:00–10:00 UTC), NY open (13:00–15:00 UTC). BTC-USDT and ETH-USDT spot for tightest spreads. Avoid 30 minutes before/after major economic data releases.

📐 Position Sizing

Extremely small per trade — 0.25% risk maximum. At 0.1% taker fee per side, a 0.5% target means 40% of gross profit goes to fees. Use post-only limit orders wherever possible to earn maker rebate. Size = (Account × 0.0025) ÷ (Entry − Stop). Stop: 1–1.5 × ATR(14) on 1m chart. Target: 1.5–2 × stop. If 5 consecutive stops hit, halt for 30 minutes.

⚠️ Risk Parameters

Fee erosion is slow death for scalpers — calculate fee-adjusted expectancy before scaling up. Emotional degradation after losses leads to over-trading and revenge trading. Use a strict daily loss limit (2% of account). Slippage on fast markets can turn a 3-tick target into a scratch trade — account for 0.5–1 tick slippage in backtests. Sub-100ms execution and stable internet required.

💸 Funding Rate Arbitrage
Collect the funding rate on perpetual futures by holding a delta-neutral position. One of the most consistent yield strategies in crypto.
Cash-and-Carry Funding Arb
Intermediate
When funding is positive (longs pay shorts), go SHORT futures + LONG spot. Collect funding every 8 hours while remaining market-neutral.
python — strategies/funding_arb.py
class FundingRateArb: """Delta-neutral funding rate harvest strategy.""" FUNDING_INTERVAL = 8 # hours between funding settlements def __init__(self, spot_client, futures_client, symbol="BTCUSDT", min_rate=0.0003, # 0.03% = ~32% APR minimum position_usdt=1000): self.spot = spot_client self.fut = futures_client self.symbol = symbol self.min_rate = min_rate self.size_usd = position_usdt self.active = False def get_funding_rate(self): data = self.fut.get_funding_rate(self.symbol) rate = float(data['lastFundingRate']) next_ts = int(data['nextFundingTime']) return rate, next_ts def annual_rate(self, funding_rate: float) -> float: return funding_rate * (24 / self.FUNDING_INTERVAL) * 365 * 100 def open_position(self, funding_rate: float): price = float(self.spot.get_ticker(self.symbol)['lastPrice']) qty = round(self.size_usd / price, 5) if funding_rate > self.min_rate: # Positive funding: shorts collect → short futures, long spot self.spot.place_order(self.symbol, "BUY", "MARKET", qty) self.fut.place_futures_order(self.symbol, "SELL", "MARKET", qty) self.active = True self.direction = "short_futures" print(f"✅ Opened: LONG spot + SHORT futures | rate={funding_rate:.4%}") print(f" Est. APR: {self.annual_rate(funding_rate):.1f}%") elif funding_rate < -self.min_rate: # Negative funding: longs collect → long futures, short spot self.spot.place_order(self.symbol, "SELL", "MARKET", qty) self.fut.place_futures_order(self.symbol, "BUY", "MARKET", qty) self.active = True self.direction = "long_futures" print(f"✅ Opened: SHORT spot + LONG futures | rate={funding_rate:.4%}") def close_position(self): if not self.active: return price = float(self.spot.get_ticker(self.symbol)['lastPrice']) qty = round(self.size_usd / price, 5) if self.direction == "short_futures": self.spot.place_order(self.symbol, "SELL", "MARKET", qty) self.fut.close_position(self.symbol) else: self.spot.place_order(self.symbol, "BUY", "MARKET", qty) self.fut.close_position(self.symbol) self.active = False print("🔒 Position closed") def monitor(self, exit_rate_threshold=0.0001): """Run every hour. Exit when rate drops below threshold.""" rate, next_ts = self.get_funding_rate() print(f"Funding: {rate:.4%} | APR: {self.annual_rate(rate):.1f}%") if self.active and abs(rate) < exit_rate_threshold: print("Rate too low — closing position") self.close_position() elif not self.active: self.open_position(rate)
💡 BTC funding rates on LMEX settle every 8h. Monitor rates across all pairs — SOL, ETH, BNB often have higher rates than BTC. Target APR above 20% after fees before opening.
⚠️ This is NOT truly risk-free. Risks include: spot/futures leg slippage on open/close, basis divergence during extreme vol, and liquidation risk if margin on futures leg is insufficient.
Funding Rate APR Calculator
8H RateDaily RateAnnual APRSignal
0.01%0.03%10.95%Too low — skip
0.03%0.09%32.85%Borderline
0.05%0.15%54.75%Good — enter
0.10%0.30%109.5%Excellent
0.30%+0.90%328%+Market extreme — high risk
📖 Deep Dive
Strategy Mechanics

Funding rate arbitrage captures the periodic payment that perpetual futures make to balance the market. When funding is positive (longs pay shorts every 8h), holding short futures + long spot in equal dollar value creates a delta-neutral position — price moves cancel across legs — while funding payments arrive as pure income. On LMEX, OIL-PERP and BRENT-PERP often carry elevated funding during commodity moves. The yield is calculable in advance (8h rate × 3 × 365 = APR), making it one of the most transparent strategies available.

✅ Ideal Conditions

Funding rate > 0.03% per 8h (~33% APR). Stable basis between spot and futures (< 0.1% divergence). Available liquidity on both legs. Best entered just after a funding settlement to maximise collection window.

📐 Position Sizing

Equal dollar value on both legs — non-negotiable for delta neutrality. Size based on targeted yield: calculate the funding APR at entry, compare to capital deployed. Scale up only when basis (futures − spot price) is confirmed stable. Max exposure: 20% of total capital per funding arb position. Use ISOLATED margin on the futures leg — never cross margin.

⚠️ Risk Parameters

Not truly risk-free. Key risks: (1) Leg slippage on open/close — enter with limit orders, accept slightly worse entry rather than chasing. (2) Funding flip — if sentiment shifts, funding goes negative and you're paying. Monitor every 8h. (3) Basis divergence during extreme volatility can trigger margin calls on the futures leg even though overall position is neutral. Keep 30% of futures leg as maintenance buffer.

📐 VWAP & TWAP
Institutional execution algorithms. Minimize market impact when entering or exiting large positions. Used by every professional desk.
VWAP — Signal & Execution
Intermediate
Volume Weighted Average Price anchors intraday fair value. Price above VWAP = bullish; below = bearish. Also used as a dynamic support/resistance level.
python — strategies/vwap.py
import numpy as np import pandas as pd def vwap(df: pd.DataFrame) -> pd.Series: """Anchored VWAP from session open. Reset daily.""" tp = (df['high'] + df['low'] + df['close']) / 3 # typical price cum_vol = df['volume'].cumsum() cum_tpvol = (tp * df['volume']).cumsum() return cum_tpvol / cum_vol def vwap_bands(df, std_dev_mult=(1, 2)): """VWAP with standard deviation bands (like Bollinger around VWAP).""" tp = (df['high'] + df['low'] + df['close']) / 3 vwap_val = vwap(df) variance = ((tp - vwap_val) ** 2 * df['volume']).cumsum() / df['volume'].cumsum() std = np.sqrt(variance) bands = {'vwap': vwap_val} for m in std_dev_mult: bands[f'upper{m}'] = vwap_val + m * std bands[f'lower{m}'] = vwap_val - m * std return pd.DataFrame(bands, index=df.index) def vwap_signals(df): """Long: pullback to VWAP in uptrend. Short: rally to VWAP in downtrend.""" bands = vwap_bands(df) df['vwap'] = bands['vwap'] df['above_vwap'] = df['close'] > df['vwap'] df['at_vwap'] = (df['low'] <= df['vwap']) & (df['high'] >= df['vwap']) # Long: price above VWAP, pulls back to touch, bounces df['long_entry'] = df['above_vwap'].shift(1) & df['at_vwap'] & (df['close'] > df['vwap']) df['short_entry'] = (~df['above_vwap']).shift(1) & df['at_vwap'] & (df['close'] < df['vwap']) return df class TWAPExecutor: """Split large orders into equal-sized slices over a time window.""" def __init__(self, client, symbol, total_qty, total_minutes, slices): self.client = client self.symbol = symbol self.total = total_qty self.slices = slices self.interval = (total_minutes * 60) / slices # seconds between slices self.qty_each = round(total_qty / slices, 5) self.filled = 0 async def run(self, side: str): import asyncio print(f"TWAP: {side} {self.total} over {self.slices} slices") for i in range(self.slices): order = self.client.place_order(self.symbol, side, "MARKET", self.qty_each) self.filled += self.qty_each print(f" Slice {i+1}/{self.slices}: {self.qty_each} filled | total={self.filled:.5f}") if i < self.slices - 1: await asyncio.sleep(self.interval) print(f"✅ TWAP complete: {self.filled} {self.symbol} {side}") # Buy 0.1 BTC over 30 minutes in 10 equal slices # executor = TWAPExecutor(client, "BTCUSDT", 0.1, 30, 10) # asyncio.run(executor.run("BUY"))
VWAP vs TWAP — When to Use Each
ScenarioUseReason
Entering a large spot positionTWAPSpreads impact evenly over time
Scalping with intraday biasVWAP signalTrade pullbacks to VWAP in trending sessions
Closing a leveraged futures positionTWAPAvoids spiking price against yourself
Identifying intraday fair valueVWAP levelDynamic S/R respected by algorithms
DCA accumulationTWAP + RSI filterCombine time slicing with dip detection
📖 Deep Dive
Strategy Mechanics

VWAP represents the true intraday fair value of an asset — where the most volume has traded. Institutional algorithms anchor to VWAP for performance benchmarking, meaning large players consistently buy pullbacks to VWAP in uptrends and sell rallies in downtrends. This creates self-fulfilling support and resistance. TWAP is an execution algorithm, not a signal — it solves the large order problem by slicing into equal-sized chunks across a defined time window, minimising market impact and achieving a price close to the arithmetic mean of the period.

✅ Ideal Conditions

Intraday strategies only. Active trading hours with high volume. Trending sessions with clear directional bias. Avoid first 15 minutes of session open. TWAP appropriate when order size exceeds 0.5% of average 1-hour volume.

📐 Position Sizing

VWAP signal: risk 1–1.5% per trade. Stop below VWAP for longs — price breaking VWAP invalidates the thesis. Size = (Account × 0.01) ÷ (Entry − VWAP). TWAP execution: divide total intended position by number of slices. Rule of thumb: 1 slice per 10% of target time window. Monitor fill quality — if market is moving strongly, adjust slice frequency.

⚠️ Risk Parameters

VWAP resets at midnight — loses relevance for multi-day positions. Institutional players know where VWAP is and can manipulate price around it to flush stops. Use a 0.2–0.3% buffer below VWAP for stops rather than exactly at VWAP. TWAP risk: if market moves strongly during execution, average fill price degrades. Set a price limit on TWAP slices.

🌊 Order Flow & Imbalance
Read the order book and trade execution data to detect institutional footprints before price moves. The edge that HFT firms don't want you to know.
Order Book Imbalance (OBI)
Advanced
Measure the ratio of bid liquidity vs ask liquidity in the top N levels. When bids dominate, price tends to rise — and vice versa.
python — strategies/order_flow.py
import numpy as np from collections import deque class OrderFlowAnalyzer: def __init__(self, client, symbol, depth=10, history=30): self.client = client self.symbol = symbol self.depth = depth self.obi_history = deque(maxlen=history) def order_book_imbalance(self) -> float: """OBI = (bid_qty - ask_qty) / (bid_qty + ask_qty). Range -1 to +1.""" book = self.client.get_orderbook(self.symbol, self.depth) bid_vol = sum(float(b[1]) for b in book["bids"]) ask_vol = sum(float(a[1]) for a in book["asks"]) obi = (bid_vol - ask_vol) / (bid_vol + ask_vol + 1e-10) self.obi_history.append(obi) return obi def detect_large_wall(self, wall_mult=5.0) -> dict: """Flag price levels with abnormally large resting orders.""" book = self.client.get_orderbook(self.symbol, 20) bids = [(float(b[0]), float(b[1])) for b in book["bids"]] asks = [(float(a[0]), float(a[1])) for a in book["asks"]] avg_bid = np.mean([b[1] for b in bids]) avg_ask = np.mean([a[1] for a in asks]) walls = {} for price, qty in bids: if qty > avg_bid * wall_mult: walls[price] = {'side': 'bid', 'qty': qty, 'strength': qty/avg_bid} for price, qty in asks: if qty > avg_ask * wall_mult: walls[price] = {'side': 'ask', 'qty': qty, 'strength': qty/avg_ask} return walls def cumulative_delta(self, trades: list) -> float: """Cumulative delta = sum of buy volume - sell volume from tape.""" delta = 0 for t in trades: if t['side'] == 'buy': delta += t['qty'] else: delta -= t['qty'] return delta def get_signal(self, obi_threshold=0.3) -> int: obi = self.order_book_imbalance() # Confirm with recent OBI trend if len(self.obi_history) >= 3: recent_avg = np.mean(list(self.obi_history)[-3:]) if recent_avg > obi_threshold: return 1 # long elif recent_avg < -obi_threshold: return -1 # short return 0
💡 OBI works best on 1m–5m timeframes combined with VWAP context. A strong OBI signal against the VWAP trend is likely a trap — wait for alignment.
📖 Deep Dive
Strategy Mechanics

Order flow analysis reads the live microstructure of the market — actual buy/sell transactions as they happen — rather than waiting for candlestick patterns to form. Order Book Imbalance (OBI) measures whether resting bids or asks dominate the visible book. Cumulative Delta tracks the running total of aggressor volume (market buys minus market sells) — persistent positive delta in an uptrend confirms institutional buying; divergence warns of hidden selling. Large wall detection identifies unusual concentrations of resting limit orders signalling institutional interest levels.

✅ Ideal Conditions

Liquid markets with visible, meaningful order book (BTC-USDT, ETH-USDT on LMEX). Active trading sessions. Avoid during news events when book is typically pulled. Works best at key S/R levels where institutional players position.

📐 Position Sizing

Very tight stops and small sizes — OBI signals are short-lived (seconds to minutes). Risk 0.5% per trade maximum. Stop placed just beyond the imbalance level. Target = 1.5–2× stop distance. Signal confidence scales size: OBI > 0.5 = full 0.5% risk; OBI 0.3–0.5 = half size. Never hold through a funding reset or major scheduled event. Use limit orders to enter.

⚠️ Risk Parameters

Spoofing is the primary risk — large resting orders placed with intent to cancel as price approaches. Watch whether walls hold as price approaches or disappear. HFT noise on sub-1m timeframes makes OBI signals extremely noisy — stick to 1m minimum. Cumulative delta divergences can persist for extended periods in trending markets. Always require price action confirmation before acting on OBI signals alone.

🏛️ Wyckoff Method
A 100-year-old market structure framework that still works. Detect accumulation and distribution phases before the big move happens.
Wyckoff Phase Detection
Advanced
The Wyckoff cycle has four phases: Accumulation → Markup → Distribution → Markdown. Identify the phase from price/volume behaviour and position accordingly.
python — strategies/wyckoff.py
import pandas as pd import numpy as np class WyckoffDetector: def __init__(self, lookback=50): self.lookback = lookback def detect_spring(self, df) -> bool: """Spring: price breaks support briefly then recovers — bullish reversal.""" recent = df.tail(5) support = df['low'].tail(self.lookback).quantile(0.1) broke_support = recent['low'].min() < support recovered = recent['close'].iloc[-1] > support vol_spike = recent['volume'].iloc[-1] > recent['volume'].mean() * 1.5 return broke_support and recovered and vol_spike def detect_upthrust(self, df) -> bool: """Upthrust: price breaks resistance briefly then fails — bearish reversal.""" recent = df.tail(5) resistance = df['high'].tail(self.lookback).quantile(0.9) broke_res = recent['high'].max() > resistance failed = recent['close'].iloc[-1] < resistance vol_spike = recent['volume'].iloc[-1] > recent['volume'].mean() * 1.5 return broke_res and failed and vol_spike def phase_score(self, df) -> dict: """Score each Wyckoff phase based on price/volume characteristics.""" window = df.tail(self.lookback) price_range = window['high'].max() - window['low'].min() price_pos = (window['close'].iloc[-1] - window['low'].min()) / price_range vol_trend = window['volume'].tail(10).mean() / window['volume'].head(10).mean() price_trend = np.polyfit(range(len(window)), window['close'], 1)[0] scores = { 'accumulation': 1 if (price_pos < 0.3 and vol_trend > 1.2 and price_trend > 0) else 0, 'markup': 1 if (price_pos > 0.5 and price_trend > 0 and vol_trend > 1.0) else 0, 'distribution': 1 if (price_pos > 0.7 and vol_trend > 1.3 and price_trend < 0) else 0, 'markdown': 1 if (price_pos < 0.5 and price_trend < 0 and vol_trend > 1.0) else 0, } return scores def get_signal(self, df) -> int: if self.detect_spring(df): return 1 # Spring → Long if self.detect_upthrust(df): return -1 # Upthrust → Short phases = self.phase_score(df) if phases['accumulation']: return 1 elif phases['distribution']: return -1 return 0
Wyckoff Events Reference
EventAbbr.PhaseSignal
Preliminary Support / Selling ClimaxPS / SCAccumulation AWatch for long
Automatic RallyARAccumulation BDefine trading range
Secondary TestSTAccumulation BConfirm support
Spring (shakeout)SPAccumulation CStrong long entry
Sign of StrengthSOSAccumulation DConfirm long
Buying ClimaxBCDistribution AWatch for short
Upthrust After DistributionUTADDistribution CStrong short entry
Sign of WeaknessSOWDistribution DConfirm short
📖 Deep Dive
Strategy Mechanics

The Wyckoff Method describes how institutional smart money systematically accumulates or distributes large positions without moving the market against themselves. During accumulation, price is pushed into a range, retail traders are shaken out at the Spring (a brief dip below support), then markup begins. The Spring is the highest-conviction entry — a false breakdown that immediately recovers, trapping short sellers. Distribution mirrors this: an Upthrust After Distribution (UTAD) breaks above resistance briefly, trapping breakout buyers, then reverses. Volume confirms — accumulation Springs are accompanied by high volume (institutional buying) and immediate recovery.

✅ Ideal Conditions

After an extended downtrend (accumulation) or uptrend (distribution). On 4H and Daily charts. Volume data must be reliable. Most effective on liquid pairs (BTC-PERP, ETH-PERP). Combine with VWAP and volume profile for institutional confluence.

📐 Position Sizing

Spring entries: full risk 2–3%. Other Wyckoff events (AR, ST, SOS): 1–1.5% risk. Stop for Spring longs: below the Spring low by 0.5 × ATR. Target for markup: measure the depth of the accumulation range and project that distance above range resistance. Scale out 50% at 1R, move stop to breakeven, let remainder run.

⚠️ Risk Parameters

Wyckoff counting is subjective — practitioners often disagree in real-time. Hard rules (Wave 2 cannot exceed Wave 1 start) help but alternation guideline creates ambiguity. A Spring that fails to recover means the accumulation was actually continuation — exit immediately at stop, no averaging down. Crypto markets can compress or skip phases due to 24/7 trading. Phase identification improves significantly with practice.

🌀 Elliott Wave & Fibonacci
Market moves in predictable wave patterns. Use Fibonacci retracements and extensions to identify high-probability entry zones and price targets.
Fibonacci Retracement & Extension
Intermediate
python — strategies/fibonacci.py
class FibonacciLevels: RETRACEMENT = [0.236, 0.382, 0.5, 0.618, 0.786] EXTENSION = [1.272, 1.618, 2.0, 2.618] def retracements(self, swing_high: float, swing_low: float) -> dict: """Key retracement levels from a swing high to low.""" diff = swing_high - swing_low return {round(r * 100): swing_high - r * diff for r in self.RETRACEMENT} def extensions(self, swing_high: float, swing_low: float, retracement_level: float) -> dict: """Price targets for Wave 3/5 using extensions.""" diff = swing_high - swing_low return {round(e * 100): retracement_level + e * diff for e in self.EXTENSION} def find_confluence(self, levels_a: dict, levels_b: dict, tolerance_pct=0.003) -> list: """Find price levels where multiple Fibonacci levels cluster. Confluence = higher probability support/resistance.""" confluences = [] for _, pa in levels_a.items(): for _, pb in levels_b.items(): if abs(pa - pb) / pa < tolerance_pct: confluences.append(round((pa + pb) / 2, 2)) return confluences def wave3_target(self, w1_start, w1_end, w2_low) -> dict: """Wave 3 is typically 1.618x Wave 1 projected from Wave 2 low.""" w1_size = w1_end - w1_start return { 'conservative': w2_low + w1_size * 1.272, 'standard': w2_low + w1_size * 1.618, 'extended': w2_low + w1_size * 2.618, } # Example: BTC rallied $60k→$70k, pulled back to $64k fib = FibonacciLevels() retrace = fib.retracements(70000, 60000) print("Retracement levels:") for pct, price in retrace.items(): print(f" {pct}%: ${price:,.0f}") # 23.6%: $67,640 → shallow pullback # 38.2%: $66,180 → moderate pullback # 61.8%: $63,820 → golden pocket (prime long entry)
Elliott Wave Cheat Sheet
WaveCharacterFibonacci RuleTrade
Wave 1Weak, few believe itRisky early entry
Wave 2Sharp retracement, looks like reversalRetraces 50–61.8% of W1Buy the 61.8% dip
Wave 3Strongest, broadest participation1.618–2.618× W1 lengthBest long, large size
Wave 4Sideways correction, overlappingRetraces 23.6–38.2% of W3Hold longs, no new entries
Wave 5Weak momentum, divergences formEqual to W1 or 61.8% of W1+W3Reduce exposure, watch for top
Wave A–CCorrective — sharp/complexC = 100% of AShort Wave B rally
⚠️ Elliott Wave is subjective — counts can be redrawn. Always use it alongside objective indicators (RSI divergence, volume, order flow) rather than in isolation.
📖 Deep Dive
Strategy Mechanics

Elliott Wave Theory describes market movement as alternating 5-wave impulse + 3-wave correction cycles. Rules are strict: Wave 2 never retraces beyond Wave 1 start; Wave 3 is never the shortest impulse; Wave 4 never enters Wave 1's price territory. Fibonacci ratios define expected relationships: Wave 2 typically retraces 50–61.8% of Wave 1 (the golden pocket), Wave 3 extends to 1.618–2.618 × Wave 1, Wave 5 often equals Wave 1. These create high-probability entry zones that combine wave structure with mathematical precision.

✅ Ideal Conditions

Clear wave structure visible on 4H or Daily chart. Trending markets with identifiable prior impulse. Fibonacci levels must cluster (confluence) for valid entries. Combine with RSI divergence at Wave 5 top as distribution warning.

📐 Position Sizing

Wave 3 entries (61.8% Wave 2 retracement confirmed): full 2% risk. Wave 5 entries: 1% only — Wave 5 often truncates and momentum diverges. Corrective wave trades (shorting Wave B): 0.5% risk — corrections are complex. Fibonacci extension targets: take 33% at 1.272× extension, 33% at 1.618×, trail remainder. Stop for Wave 3 entry: below the start of Wave 1 (invalidation rule).

⚠️ Risk Parameters

Elliott Wave counting is notoriously subjective. The hard rules help but extended waves create ambiguity. Risk of misidentification is highest at market turning points — exactly where you most want to be positioned. Strict rule: if the next wave violates the count's rules, you have the wrong count — exit and re-analyse. Never argue with the market about the wave count. Extensions are the most dangerous: a third-of-third can look like a Wave 3 top prematurely.

📦 Volume Profile & POC
See where the most volume traded over a period. Point of Control (POC), Value Area High (VAH) and Value Area Low (VAL) are the levels that matter to big players.
Volume Profile Builder
Intermediate
python — strategies/volume_profile.py
import numpy as np import pandas as pd class VolumeProfile: def __init__(self, num_bins=50, value_area_pct=0.70): self.bins = num_bins self.va_pct = value_area_pct # typically 70% of total volume def build(self, df: pd.DataFrame) -> dict: """Build volume profile from OHLCV data.""" lo, hi = df['low'].min(), df['high'].max() edges = np.linspace(lo, hi, self.bins + 1) profile = np.zeros(self.bins) for _, row in df.iterrows(): # Distribute candle's volume across its price range mask = (edges[:-1] >= row['low']) & (edges[1:] <= row['high']) filled = mask.sum() or 1 profile[mask] += row['volume'] / filled prices = (edges[:-1] + edges[1:]) / 2 # bin midpoints poc_idx = profile.argmax() # Value Area: 70% of total volume around POC total_vol = profile.sum() va_vol = total_vol * self.va_pct va_profile = 0 va_indices = [poc_idx] lo_i, hi_i = poc_idx, poc_idx while va_profile < va_vol: add_lo = profile[lo_i - 1] if lo_i > 0 else 0 add_hi = profile[hi_i + 1] if hi_i < self.bins - 1 else 0 if add_lo >= add_hi and lo_i > 0: lo_i -= 1 elif hi_i < self.bins - 1: hi_i += 1 else: break va_profile = profile[lo_i:hi_i + 1].sum() return { 'poc': prices[poc_idx], # Point of Control 'vah': prices[hi_i], # Value Area High 'val': prices[lo_i], # Value Area Low 'profile': list(zip(prices, profile)), 'total_volume': total_vol, } def get_signal(self, df, current_price: float) -> int: vp = self.build(df) poc, vah, val = vp['poc'], vp['vah'], vp['val'] # Price above VAH → momentum long; below VAL → momentum short # Approaching POC from above → support; below → resistance if current_price > vah: return 1 elif current_price < val: return -1 elif abs(current_price - poc) / poc < 0.002: return 0 # at POC — wait return 0
💡 POC acts as a magnet. Price often revisits the POC before continuing. Use VAH/VAL as breakout triggers and POC as a mean-reversion target when inside the value area.
📖 Deep Dive
Strategy Mechanics

Volume Profile maps where the most volume has traded over a given period, revealing price levels that matter most to market participants. The Point of Control (POC) — the price level with the highest traded volume — acts as a powerful magnet. Markets frequently gravitate back toward the POC because it represents where buyers and sellers reached the most agreement. The Value Area (70% of volume) defines fair value for the period. Breaking above VAH with expanding volume signals strong institutional demand. Falling below VAL signals aggressive selling.

✅ Ideal Conditions

Liquid markets with meaningful volume data. Session or weekly volume profiles for liquid pairs. Most powerful at the start of a new session when yesterday's value area provides initial reference.

📐 Position Sizing

POC-based trades (fading back to POC from outside Value Area): 0.75% risk — stop beyond the opposite VA boundary. VAH/VAL breakout trades: 1–1.5% risk — stop back inside the Value Area. Size up when multiple volume profile lookback periods agree on the same level. Scale out: take 50% at POC when approaching from outside, let 50% run to the opposite VA boundary.

⚠️ Risk Parameters

Volume profile shows the past, not the future — levels that were significant can be taken out with minimal resistance if the context changes. Always use the most recent relevant timeframe. In thin markets (altcoins on LMEX), volume profiles are noisy and unreliable — stick to BTC-USDT and ETH-USDT. POC drift: as new volume prints, the POC slowly migrates. Your analysis can be valid at entry but invalidated an hour later as the profile updates.

☁️ Ichimoku Cloud
A complete trading system in one indicator. Trend, momentum, support/resistance, and signal lines — all from price alone. No volume required.
Ichimoku System
Intermediate
python — strategies/ichimoku.py
import pandas as pd def ichimoku(df, tenkan=9, kijun=26, senkou_b=52, displacement=26): """Compute all five Ichimoku components.""" high, low = df['high'], df['low'] def mid(n): return (high.rolling(n).max() + low.rolling(n).min()) / 2 tenkan_sen = mid(tenkan) # Conversion Line (9) kijun_sen = mid(kijun) # Base Line (26) senkou_a = ((tenkan_sen + kijun_sen) / 2).shift(displacement) # Cloud A senkou_b = mid(senkou_b).shift(displacement) # Cloud B chikou_span = df['close'].shift(-displacement) # Lagging Span df['tenkan'] = tenkan_sen df['kijun'] = kijun_sen df['senkou_a'] = senkou_a df['senkou_b'] = senkou_b df['chikou'] = chikou_span df['cloud_top'] = df[['senkou_a','senkou_b']].max(axis=1) df['cloud_bot'] = df[['senkou_a','senkou_b']].min(axis=1) return df def ichimoku_signal(df) -> int: last = df.iloc[-1] prev = df.iloc[-2] # Strong bull: price above cloud, tenkan above kijun, bullish cloud above_cloud = last['close'] > last['cloud_top'] tk_cross_up = (last['tenkan'] > last['kijun']) and \ (prev['tenkan'] <= prev['kijun']) bull_cloud = last['senkou_a'] > last['senkou_b'] # Strong bear: price below cloud, tenkan below kijun, bearish cloud below_cloud = last['close'] < last['cloud_bot'] tk_cross_dn = (last['tenkan'] < last['kijun']) and \ (prev['tenkan'] >= prev['kijun']) bear_cloud = last['senkou_a'] < last['senkou_b'] if above_cloud and bull_cloud and tk_cross_up: return 1 # strong long if below_cloud and bear_cloud and tk_cross_dn: return -1 # strong short if above_cloud and bull_cloud: return 1 # weak long if below_cloud and bear_cloud: return -1 # weak short return 0 # inside cloud = no trade
Ichimoku Signal Hierarchy
Signal StrengthConditionsAction
Strong Bull (A+)Price above cloud + TK cross up above cloud + green cloudFull size long
Moderate Bull (B)Price above cloud + TK cross up below cloudHalf size long
NeutralPrice inside cloudNo trade — wait
Moderate Bear (B)Price below cloud + TK cross down above cloudHalf size short
Strong Bear (A+)Price below cloud + TK cross down below cloud + red cloudFull size short
💡 Ichimoku was designed for daily charts. Works best on 4H and Daily timeframes on LMEX. On lower timeframes (1H), use it for trend context only — not entries.
📖 Deep Dive
Strategy Mechanics

Ichimoku Kinko Hyo ("equilibrium at a glance") is a complete trading system providing trend, momentum, and support/resistance analysis from a single indicator. The Cloud (Kumo) is the most important component — its colour and thickness define trend quality. The Tenkan/Kijun (TK) cross is the trigger. The Chikou (lagging span) confirms the signal by comparing current price to historical price 26 periods back. The most powerful entries occur when all elements align: price above a bullish Cloud, TK crossing above Kijun, and Chikou above historical price.

✅ Ideal Conditions

Trending markets on 4H and Daily timeframes. Original parameters (9/26/52) work well for crypto daily charts. Avoid intraday (sub-1H). Best when cloud is thick (strong trend). Combine with volume profile to confirm cloud levels as institutional support.

📐 Position Sizing

Scale size to signal quality. A+ signal (all 5 components aligned): 2% risk. B signal (price above cloud, TK cross but weak cloud or Chikou neutral): 1% risk. No trade inside the cloud — this is the noise zone. Stop placement for longs: below the bottom of the Cloud (Senkou Span B for thin clouds, Span A for thick). Trail stop with the bottom of the cloud. Target: measure the prior swing, project same distance from entry.

⚠️ Risk Parameters

In fast-moving markets, Ichimoku lags significantly — signals confirm moves already substantially complete. Use lower timeframe TK crosses (1H) to fine-tune entries within 4H/Daily signal direction. The Cloud creates wide stops which forces smaller position sizes — intentional but reduces return on capital. Flat/ranging markets produce meaningless TK crosses near the cloud — enforce the no-trade-inside-cloud rule strictly.

🔭 Multi-Timeframe Confluence
The highest-probability trades occur when multiple timeframes agree. Use higher timeframes for direction, lower timeframes for precise entry timing.
MTF Confluence Engine
Intermediate
python — strategies/mtf.py
import pandas as pd class MTFConfluence: """Multi-timeframe signal aggregator. Rule: Only trade when 2+ timeframes agree on direction. Use HTF for direction, LTF for entry trigger.""" TIMEFRAMES = ['1d', '4h', '1h', '15m'] WEIGHTS = {'1d': 4, '4h': 3, '1h': 2, '15m': 1} def __init__(self, client, symbol, strategy_fn): self.client = client self.symbol = symbol self.strat = strategy_fn # any function: df -> signal (-1,0,1) def get_tf_signal(self, tf: str) -> int: klines = self.client.get_klines(self.symbol, tf, 200) df = pd.DataFrame(klines, columns=['t','o','h','l','c','v', 'ct','qa','nt','tbv','tqv','i']) df = df.rename(columns={'o':'open','h':'high','l':'low', 'c':'close','v':'volume'}) df = df['open high low close volume'.split()].astype(float) return self.strat(df) def confluence_score(self) -> dict: """Returns weighted score and individual TF signals.""" signals = {} for tf in self.TIMEFRAMES: try: signals[tf] = self.get_tf_signal(tf) except: signals[tf] = 0 weighted = sum(signals[tf] * self.WEIGHTS[tf] for tf in self.TIMEFRAMES) max_weight = sum(self.WEIGHTS.values()) norm = weighted / max_weight # -1 to +1 return {'signals': signals, 'score': norm, 'weighted': weighted} def get_signal(self, strong_thresh=0.6, weak_thresh=0.3) -> int: result = self.confluence_score() score = result['score'] print(f"MTF signals: {result['signals']} | score: {score:.2f}") if score > strong_thresh: return 1 elif score < -strong_thresh: return -1 return 0
MTF Framework — Rules of Thumb
Timeframe PairDirection TFEntry TFBest For
Swing TradingWeekly / Daily4H / 1H3–10 day holds
Day Trading4H / 1H15m / 5mIntraday sessions
Scalping1H / 15m5m / 1mMinutes to 1 hour
Position TradingMonthly / WeeklyDailyWeeks to months
💡 Never trade against the daily trend on a 5m signal. The 5m provides the trigger; the daily provides the permission.
📖 Deep Dive
Strategy Mechanics

Multi-timeframe confluence is a framework that dramatically improves the quality of any strategy. A signal on a 5m chart has very different predictive power depending on whether the 4H and daily charts agree. The principle is trend permission: the higher timeframe grants or denies permission for lower timeframe signals. A perfect RSI divergence on 15m is tradeable if the 4H is bullish; the same signal is ignored if the 4H is bearish. The weighted scoring system (Daily = 4×, 4H = 3×, 1H = 2×, 15m = 1×) quantifies confluence objectively.

✅ Ideal Conditions

Any trending market. Most powerful when the dominant cycle (Daily or Weekly) is in a clear trend. Useless in choppy flat markets where all timeframes are simultaneously ranging. Apply to any core strategy (EMA cross, RSI, VWAP) to filter out low-quality signals.

📐 Position Sizing

Scale risk directly with the weighted confluence score. Score > 0.7 (strong alignment): full 2% risk. Score 0.5–0.7: 1% risk. Score 0.3–0.5: 0.5% risk or skip. Score < 0.3: no trade. For exits: close 50% when LTF signals reversal; close fully when MTF (1H) signals reversal; only let run to full target if HTF (4H+) remains aligned.

⚠️ Risk Parameters

Conflicting timeframes are normal and expected — accept that a score of 0.6–0.7 is sufficient for a trade. Don't over-filter. Timeframe lag is a real risk — the daily signal may have triggered 2 days ago, making the LTF entry very late in the daily move. Check that the HTF signal is not exhausted (e.g., daily RSI at 85 when wanting to go long on 5m). Conflicting signals in adjacent timeframes often indicate a turning point — reduce size significantly.

🛢️ WTI / Brent Oil Spread Arbitrage
Trade the price differential between WTI and Brent crude directly on LMEX perpetual futures — OIL-PERP (WTI) and BRENT-PERP. The spread is historically mean-reverting. Both legs are USDT-margined perps — no spot, no expiry, no physical delivery.
LMEX Perp Contracts
ContractLMEX SymbolUnderlyingSettlementMargin
WTI Crude OilOIL-PERPWTI Crude FuturesUSDT PerpetualIsolated / Cross
Brent Crude OilBRENT-PERPBrent Crude FuturesUSDT PerpetualIsolated / Cross
💡 Both contracts settle in USDT. You never hold physical oil — pure price exposure. Use ISOLATED margin on each leg so a spike on one side doesn't liquidate the other.
Oil Spread Arb — Full Perp Implementation
Advanced
Both legs are perpetual futures via LMEXFutures. The strategy monitors the BRENT-PERP minus OIL-PERP spread, enters on Z-score extremes, and accounts for funding rate drag on both legs.
python — strategies/oil_spread.py
import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller from lmex_client import LMEXFutures class OilSpreadArb: """WTI/Brent spread arb on LMEX perpetual futures. OIL-PERP = WTI crude perpetual BRENT-PERP = Brent crude perpetual Strategy: trade the BRENT-PERP minus OIL-PERP spread. Enter when Z-score exceeds threshold, exit at mean reversion.""" OIL_PERP = "OIL-PERP" BRENT_PERP = "BRENT-PERP" def __init__(self, futures_client: LMEXFutures, window=30, entry_z=2.0, exit_z=0.5, position_usd=1000, leverage=3, max_hold_hours=72): self.fut = futures_client self.window = window self.entry_z = entry_z self.exit_z = exit_z self.pos_usd = position_usd self.leverage = leverage self.max_hold = max_hold_hours self.active = False self.direction = None def _setup_legs(self): """Configure both perp legs: isolated margin + leverage.""" for sym in [self.OIL_PERP, self.BRENT_PERP]: self.fut.set_margin_type(sym, "ISOLATED") self.fut.set_leverage(sym, self.leverage) print(f"✅ Both legs: ISOLATED margin | {self.leverage}x leverage") def fetch_prices(self, limit=100) -> tuple: """Pull daily OHLCV from both perp contracts.""" oil_k = self.fut.get_klines(self.OIL_PERP, "1d", limit) brent_k = self.fut.get_klines(self.BRENT_PERP, "1d", limit) oil_c = pd.Series([float(k[4]) for k in oil_k]) brent_c = pd.Series([float(k[4]) for k in brent_k]) return oil_c, brent_c def fetch_funding(self) -> dict: """Check 8h funding rates on both perps — affects net carry cost.""" oil_f = self.fut.get_funding_rate(self.OIL_PERP) brent_f = self.fut.get_funding_rate(self.BRENT_PERP) return { "oil": float(oil_f["lastFundingRate"]), "brent": float(brent_f["lastFundingRate"]) } def compute_spread(self, oil: pd.Series, brent: pd.Series) -> pd.Series: """Spread = BRENT-PERP - OIL-PERP. Positive = Brent premium (normal).""" return brent - oil def test_stationarity(self, spread: pd.Series) -> bool: result = adfuller(spread.dropna()) p_val = result[1] print(f"ADF p={p_val:.4f} {'✅ Mean-reverting' if p_val < 0.05 else '❌ Trending — skip'}") return p_val < 0.05 def zscore(self, spread: pd.Series): mu = spread.rolling(self.window).mean() std = spread.rolling(self.window).std() z = (spread - mu) / (std + 1e-10) return z.iloc[-1], mu.iloc[-1], std.iloc[-1] def net_funding_cost(self, direction: str, funding: dict) -> float: """Net 8h funding drag per $1000 notional on each leg.""" if direction == "long_oil_short_brent": return -funding["oil"] + funding["brent"] # pay oil, receive brent return funding["oil"] - funding["brent"] # receive oil, pay brent def get_signal(self) -> dict: oil_p, brent_p = self.fetch_prices() spread = self.compute_spread(oil_p, brent_p) z, mean, std = self.zscore(spread) cur_spread = spread.iloc[-1] funding = self.fetch_funding() print(f"OIL-PERP: ${oil_p.iloc[-1]:.2f}") print(f"BRENT-PERP: ${brent_p.iloc[-1]:.2f}") print(f"Spread: ${cur_spread:.2f} | Mean: ${mean:.2f} | Z: {z:.2f}") print(f"Funding: OIL={funding['oil']:.4%} | BRENT={funding['brent']:.4%}") if not self.test_stationarity(spread): return {"action": None, "reason": "Spread not mean-reverting"} action = None if not self.active: if z > self.entry_z: action = "LONG_OIL_SHORT_BRENT" elif z < -self.entry_z: action = "SHORT_OIL_LONG_BRENT" elif self.active and abs(z) < self.exit_z: action = "EXIT" return { "action": action, "zscore": z, "spread": cur_spread, "mean": mean, "oil_price": oil_p.iloc[-1], "brent_price": brent_p.iloc[-1], "funding": funding } def execute(self, signal: dict): if not signal["action"]: return oil_p = signal["oil_price"] brent_p = signal["brent_price"] qty_oil = round(self.pos_usd / oil_p, 2) qty_brent = round(self.pos_usd / brent_p, 2) if signal["action"] == "LONG_OIL_SHORT_BRENT": self._setup_legs() self.fut.place_futures_order(self.OIL_PERP, "BUY", "MARKET", qty_oil) self.fut.place_futures_order(self.BRENT_PERP, "SELL", "MARKET", qty_brent) self.active = True; self.direction = "long_oil_short_brent" print(f"✅ LONG OIL-PERP @ ${oil_p:.2f}") print(f"✅ SHORT BRENT-PERP @ ${brent_p:.2f} | Z={signal['zscore']:.2f}") elif signal["action"] == "SHORT_OIL_LONG_BRENT": self._setup_legs() self.fut.place_futures_order(self.OIL_PERP, "SELL", "MARKET", qty_oil) self.fut.place_futures_order(self.BRENT_PERP, "BUY", "MARKET", qty_brent) self.active = True; self.direction = "short_oil_long_brent" print(f"✅ SHORT OIL-PERP @ ${oil_p:.2f}") print(f"✅ LONG BRENT-PERP @ ${brent_p:.2f} | Z={signal['zscore']:.2f}") elif signal["action"] == "EXIT": self.fut.close_position(self.OIL_PERP) self.fut.close_position(self.BRENT_PERP) self.active = False; self.direction = None print("🔒 Both legs closed — spread normalised") # Deploy arb = OilSpreadArb(LMEXFutures(), position_usd=1000, leverage=3, entry_z=2.0) sig = arb.get_signal() arb.execute(sig)
💡 Both legs are perpetual futures — no expiry, no roll. The main carry cost is funding, which can work for or against you. Always run fetch_funding() before entry to confirm net funding is favourable.
⚠️ Set ISOLATED margin on both OIL-PERP and BRENT-PERP independently. A geopolitical oil spike on one leg won't liquidate your other LMEX positions.
Spread Levels & Trade Logic
BRENT-PERP minus OIL-PERPZ-ScoreInterpretationTrade
$1 – $5~0Normal Brent premium — historical averageNo edge — stand aside
$6 – $10>1.5Elevated — US supply glut or bottleneckLONG OIL-PERP / SHORT BRENT-PERP
$10+>2.0Extreme dislocation — geopolitical shockFull size — high conviction entry
$0 or negative<-2.0WTI premium (inversion) — rareSHORT OIL-PERP / LONG BRENT-PERP
Risk Factors — OIL-PERP & BRENT-PERP
RiskImpactMitigation
OPEC production announcementSudden BRENT-PERP spike — spread widens sharplyMax hold period stop: 72h
EIA inventory data (Wednesday)OIL-PERP specific volatility weeklyAvoid entry Tue–Wed
Funding divergence between legsOne leg pays, other collects — net dragCheck both funding rates before entry
Thin liquidity on perp order bookWide spread + slippage on both legsUse limit orders to enter each leg
Geopolitical shockSpread stays dislocated for weeksHard stop at 3× entry Z-score
📖 Deep Dive
Strategy Mechanics

The WTI/Brent spread exists because they are chemically similar crude oils traded at different benchmark hubs — Cushing, Oklahoma for WTI; North Sea for Brent. Historically, Brent trades at a $1–5 premium to WTI due to transportation economics and sulphur content. When the spread widens significantly beyond this norm (geopolitical supply shock, pipeline disruption, US export surge), it is expected to mean-revert. On LMEX, both trade as USDT-margined perpetuals (OIL-PERP and BRENT-PERP) — equal dollar positions on both legs, no physical delivery, no expiry roll. The ADF test confirms stationarity before any trade is placed.

✅ Ideal Conditions

ADF p-value < 0.05 (stationarity confirmed). Z-score ≥ ±2.0 from rolling 30-day mean. No OPEC meetings within 48 hours. Not during Wednesday EIA inventory release. Both LMEX perps showing normal order book liquidity.

📐 Position Sizing

Equal dollar value on both legs — non-negotiable for delta neutrality. Start at $500/leg, scale to $2,000/leg with track record. Leverage: 2–3× maximum — excessive leverage can be liquidated during transient volatility even when the thesis is correct. Set ISOLATED margin on each leg independently on LMEX. Target: Z-score returning from ±2.0 to ±0.5. Hard stop at Z = ±3.5.

⚠️ Risk Parameters

Non-stationarity periods: OPEC+ production cuts or geopolitical embargoes can permanently shift the spread. Re-run ADF weekly — if p > 0.05, close immediately. EIA inventory data every Wednesday at 15:30 UTC causes sharp WTI-specific moves — avoid new positions Tuesday/Wednesday. Max hold time: 72 hours hard limit prevents riding a structural dislocation indefinitely. Funding on both perp legs accumulates as a carry cost.

🤖
LMEX Support AI
Online · API & Trading Expert
🤖
Hi! I'm the LMEX Support AI — your expert on the LMEX.IO API, trading platform, and all strategies in this academy.

Ask me anything about API authentication, order placement, WebSocket streams, strategy setup, or troubleshooting.
🔑 Auth setup 📉 Place order ⚡ WebSocket 🔲 Grid bot 📐 Position size 🛠️ Troubleshoot