IG Markets SDK
Complete reference for ctx.ig — programmatic CFD, forex, indices, commodities, and equities trading on IG Markets from worker code.
IG REST + Lightstreamer
This SDK wraps the IG REST Trading API for orders and account data, plus the Lightstreamer streaming API (NOT WebSocket) for real-time prices and charts. Server-only.
Setup
- Add an IG Markets trading block to your workspace canvas.
- Open the inspector → API Keys → pick a key (Settings → API Keys → IG Markets to add new). IG keys consist of API Key + Username (identifier) + Password + a
Demotoggle. - Connect the block to your Worker block via an edge.
ctx.igis now available.
Demo vs Live
IG separates demo and live accounts at the API key level. The block reads is_demo from your stored credentials and routes to demo-api.ig.com or api.ig.com automatically. No per-call override.
def tick(ctx):
accounts = ctx.ig.get_accounts()
ctx.log.info(f"Accounts: {len(accounts.get('accounts', []))}")If no IG block is connected:
RuntimeError: No trading block connected. Connect an IG Markets block to this worker.Instrument Naming (Epics)
IG calls instruments epics — opaque codes like CS.D.EURUSD.TODAY.IP. There are tens of thousands. Two ways to discover them:
| Method | Use when |
|---|---|
search_markets | You know a name fragment (e.g. "Gold", "FTSE") |
get_market_navigation | You want to browse the hierarchy (Indices > UK > FTSE 100) |
Once you have an epic, get_market_info gives full instrument details (tick size, dealing rules, margin requirement).
Account & History
get_accounts
All accounts on this login (live + demo per session, multi-account users get one row per account).
ctx.ig.get_accounts() -> dictget_account_activity
Recent trade / order activity (last 6 months, paginated).
ctx.ig.get_account_activity(
from_date: str, # ISO 8601: "2026-01-01T00:00:00"
to_date: str = None,
detailed: bool = False,
deal_id: str = None,
filter: str = None, # FIQL filter expression
) -> dictimport time
since = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(time.time() - 86400 * 7))
acts = ctx.ig.get_account_activity(from_date=since, detailed=True)get_account_history
Account transaction history: deposits, withdrawals, dividends, fees, interest.
ctx.ig.get_account_history(
from_date: str,
to_date: str = None,
type: str = None, # "ALL" | "DEPOSIT" | "WITHDRAWAL"
) -> dictMarket Discovery
search_markets
Search for an epic by name fragment.
ctx.ig.search_markets(search_term: str) -> dictresults = ctx.ig.search_markets("Gold")
# → {"markets": [{"epic": "CS.D.CFDGOLD.CFD.IP", "instrumentName": "Spot Gold", ...}]}get_market_info
Detailed info for one epic: tick size, lot size, margin requirements, dealing rules, current prices.
ctx.ig.get_market_info(epic: str) -> dictget_market_navigation
Browse the market hierarchy.
ctx.ig.get_market_navigation(node_id: str = None) -> dict# Root: list of categories (Indices, Forex, Commodities, ...)
root = ctx.ig.get_market_navigation()
# Drill: list contents of one category by id
indices = ctx.ig.get_market_navigation(node_id="184733")get_historical_prices
OHLC bars + volume for an epic.
ctx.ig.get_historical_prices(
epic: str,
resolution: str = "HOUR",
max_points: int = 100,
from_date: str = None,
to_date: str = None,
) -> dictresolution | Values |
|---|---|
| Sub-minute | "SECOND" |
| Minutes | "MINUTE", "MINUTE_2", "MINUTE_3", "MINUTE_5", "MINUTE_10", "MINUTE_15", "MINUTE_30" |
| Hours | "HOUR", "HOUR_2", "HOUR_3", "HOUR_4" |
| Days+ | "DAY", "WEEK", "MONTH" |
bars = ctx.ig.get_historical_prices("CS.D.EURUSD.TODAY.IP",
resolution="HOUR", max_points=24)
closes = [p["closePrice"]["bid"] for p in bars["prices"]]Quota
IG enforces a strict quarterly historical-data quota — see bars["allowance"]. Don't burn through it polling every tick.
get_client_sentiment
IG's "% of clients are long / short" indicator — a contrarian-friendly signal.
ctx.ig.get_client_sentiment(market_id: str = None) -> dictTIP
market_id is not the same as epic. It's an IG-internal id you find under get_market_info(epic)["snapshot"]["marketId"] or by browsing.
s = ctx.ig.get_client_sentiment(market_id="EURUSD")
ctx.log.info(f"Long: {s['longPositionPercentage']}%")Streaming (Lightstreamer)
These methods read from the long-lived Lightstreamer stream that the IG worker maintains in the background. They return cached snapshots; no REST calls.
get_streaming_market
Latest snapshot for one epic (bid, offer, change, MID).
ctx.ig.get_streaming_market(epic: str) -> dict | Noneget_streaming_candles
Live-updating candle history (5-minute or 1-minute resolution).
ctx.ig.get_streaming_candles(epic: str, scale: str = "1MINUTE") -> list[dict]
# scale: "1SECOND" | "1MINUTE" | "5MINUTE" | "1HOUR"get_streaming_ticks
Recent tick stream (per-trade granularity).
ctx.ig.get_streaming_ticks(epic: str) -> list[dict]get_trade_confirms
Recent trade confirmations from the streaming connection.
ctx.ig.get_trade_confirms() -> list[dict]add_streaming_epics
Subscribe to additional epics on the running stream.
ctx.ig.add_streaming_epics(epics: list[str])Lightstreamer not WebSocket
IG uses Lightstreamer (a streaming protocol layered over HTTP) — not WebSocket. The adapter handles the protocol details for you; just subscribe and read the cache.
PRO+ workers maintain the stream automatically. Free-plan workers don't have streaming; methods return None / [].
Positions
get_positions
All open positions.
ctx.ig.get_positions() -> dictopen_position
Open a market or limit position.
ctx.ig.open_position(
epic: str,
direction: str, # "BUY" | "SELL"
size: float,
order_type: str = "MARKET",
currency_code: str = "USD",
expiry: str = "DFB",
force_open: bool = True,
guaranteed_stop: bool = False,
stop_distance: float = None,
limit_distance: float = None,
stop_level: float = None,
limit_level: float = None,
) -> dict# Market BUY 1 unit of EUR/USD with 50-point SL / 100-point TP
r = ctx.ig.open_position(
epic="CS.D.EURUSD.TODAY.IP",
direction="BUY", size=1.0,
stop_distance=50.0,
limit_distance=100.0,
)
deal_ref = r.get("dealReference")
# Get the actual dealId via confirmation
conf = ctx.ig.confirm_deal(deal_reference=deal_ref)
deal_id = conf.get("dealId")Two-step deals
IG's API responds with a dealReference immediately, but the actual dealId only comes back from confirm_deal(dealReference). Always confirm before using a dealId.
close_position
Close (fully or partially) by dealId.
ctx.ig.close_position(
deal_id: str,
direction: str, # opposite of open direction
size: float,
order_type: str = "MARKET",
) -> dictupdate_position
Update SL / TP on an open position.
ctx.ig.update_position(
deal_id: str,
stop_level: float = None,
limit_level: float = None,
) -> dictconfirm_deal
Get full details of a deal by dealReference.
ctx.ig.confirm_deal(deal_reference: str) -> dictWorking (Pending) Orders
get_orders
All working (unfilled) orders.
ctx.ig.get_orders() -> dictcreate_working_order
Place a pending limit or stop order.
ctx.ig.create_working_order(
epic: str,
direction: str, # "BUY" | "SELL"
size: float,
level: float, # the trigger price
order_type: str = "LIMIT", # "LIMIT" | "STOP"
time_in_force: str = "GOOD_TILL_CANCELLED", # or "GOOD_TILL_DATE"
good_till_date: str = None,
currency_code: str = "USD",
expiry: str = "DFB",
force_open: bool = True,
guaranteed_stop: bool = False,
stop_distance: float = None,
limit_distance: float = None,
stop_level: float = None,
limit_level: float = None,
) -> dictupdate_working_order
Modify a pending order's level, SL, TP, or expiry.
ctx.ig.update_working_order(
deal_id: str,
level: float = None,
stop_distance: float = None,
limit_distance: float = None,
stop_level: float = None,
limit_level: float = None,
time_in_force: str = None,
good_till_date: str = None,
type: str = None,
) -> dictdelete_working_order
Cancel a pending order.
ctx.ig.delete_working_order(deal_id: str) -> dictCommon Patterns
Demo during development
Set the API-key is_demo: true in Settings → API Keys → IG Markets. All worker calls auto-route to demo-api.ig.com. IG's demo accounts come pre-funded with virtual money.
Error handling
def tick(ctx):
try:
positions = ctx.ig.get_positions()
except Exception as e:
ctx.log.error(f"IG unreachable: {e}")
returnIGAPIError carries status and msg attributes. Network failures raise httpx.HTTPStatusError.
Cloud-Run proxy
PRO+ workers route IG REST traffic through the Cloud Run proxy with rotating exit IPs. Lightstreamer connects directly (it's a long-lived TCP connection, not pooled).
Rate limits
IG enforces 60 req/min on the trading API. Repeated 403 responses with exceeded-api-key-allowance invalidate your session for 1-2 minutes. The adapter caches the session for 55 minutes — re-using the same session avoids the per-login rate limit.
Recipes
Mean-reversion on EUR/USD with bracket exits
def tick(ctx):
bars = ctx.ig.get_historical_prices(
"CS.D.EURUSD.TODAY.IP",
resolution="MINUTE_15", max_points=20,
)
closes = [(p["closePrice"]["bid"] + p["closePrice"]["ask"]) / 2
for p in bars["prices"]]
sma = sum(closes) / len(closes)
last = closes[-1]
z = (last - sma) / (sma * 0.001) # rough z-score in pips
pos = ctx.ig.get_positions()
has_pos = any(p["market"]["epic"] == "CS.D.EURUSD.TODAY.IP"
for p in pos.get("positions", []))
if z < -2 and not has_pos:
# Cheap — buy with a 30-pt stop, 60-pt limit (1:2 R:R)
ctx.ig.open_position(
epic="CS.D.EURUSD.TODAY.IP", direction="BUY", size=1.0,
stop_distance=30.0, limit_distance=60.0,
)
ctx.log.info(f"Long entry @ {last:.5f}, z={z:.2f}")Contrarian on extreme client sentiment
def tick(ctx):
s = ctx.ig.get_client_sentiment(market_id="EURUSD")
long_pct = float(s.get("longPositionPercentage", 50))
pos = ctx.ig.get_positions()
has_pos = any(p["market"]["epic"].startswith("CS.D.EURUSD")
for p in pos.get("positions", []))
# When >75% of retail are long, fade them
if long_pct > 75 and not has_pos:
ctx.ig.open_position(
epic="CS.D.EURUSD.TODAY.IP", direction="SELL", size=1.0,
stop_distance=40.0, limit_distance=80.0,
)
ctx.log.info(f"Fading {long_pct}% long crowd")Resting order ladder around current price
def setup(ctx):
info = ctx.ig.get_market_info("CS.D.EURUSD.TODAY.IP")
px = float(info["snapshot"]["bid"])
# Cancel any leftover working orders first
open_orders = ctx.ig.get_orders().get("workingOrders", [])
for o in open_orders:
if o["marketData"]["epic"] == "CS.D.EURUSD.TODAY.IP":
ctx.ig.delete_working_order(deal_id=o["workingOrderData"]["dealId"])
# Lay down a 5-rung buy ladder, 5 pips apart
for i in range(1, 6):
ctx.ig.create_working_order(
epic="CS.D.EURUSD.TODAY.IP",
direction="BUY", size=1.0,
level=round(px - i * 0.0005, 5),
order_type="LIMIT",
stop_distance=20.0, limit_distance=40.0,
)
ctx.log.info(f"Buy ladder placed below {px:.5f}")Reference
| IG endpoint | SDK method |
|---|---|
GET /accounts | get_accounts |
GET /history/activity | get_account_activity |
GET /history/transactions | get_account_history |
GET /markets?searchTerm= | search_markets |
GET /markets/{epic} | get_market_info |
GET /marketnavigation | get_market_navigation |
GET /prices/{epic} | get_historical_prices |
GET /clientsentiment | get_client_sentiment |
GET /positions | get_positions |
POST /positions/otc | open_position |
DELETE /positions/otc (body) | close_position |
PUT /positions/otc/{dealId} | update_position |
GET /confirms/{ref} | confirm_deal |
GET /workingorders | get_orders |
POST /workingorders/otc | create_working_order |
PUT /workingorders/otc/{dealId} | update_working_order |
DELETE /workingorders/otc/{dealId} | delete_working_order |
Lightstreamer: MARKET:{epic} | get_streaming_market |
Lightstreamer: CHART:{epic}:{scale} | get_streaming_candles |
Lightstreamer: CHART:{epic}:TICK | get_streaming_ticks |
Lightstreamer: TRADE:{accountId} | get_trade_confirms |