← Cloud Compute
Multi-account IBKR · ECS + serverless

IBKR Trading Infrastructure on AWS

A live multi-account futures execution platform: TradingView strategy alerts become real broker orders across many isolated Interactive Brokers accounts, with end-to-end reconciliation proving every signal became the right fill. Each account runs as its own ECS service, discovered over DNS, with credentials injected from Secrets Manager and zero long-lived IAM keys anywhere. ~19k lines of Python, IaC and tooling across 7 ordered SAM/CloudFormation stacks — a full re-architecture of an earlier single-VM design.

Python 3.12 IBKR Gateway + IBC AWS SAM / CloudFormation ECS on EC2 Cloud Map (DNS SRV) Lambda API Gateway SQS FIFO DynamoDB Secrets Manager CloudWatch CloudFront + S3 Docker Chrome extension (MV3)
Built with agentic AI. Development on this project runs through multi-agent Claude Code workflows — developer → independent-reviewer cycles with evidence gates, batched into unattended runs that execute overnight and resume from committed ledgers. How the agent system works →

The signal path

TradingView webhook → API Gateway (secret path token, since TradingView cannot set headers) → ingest Lambda persists the signal to DynamoDB and fans out only to accounts flagged provisioned and tradingSQS FIFO with per-account message groups, so each account's entries and exits stay strictly ordered → an in-VPC forward Lambda resolves which ECS task hosts this account and POSTs the signal to it → the executor qualifies the contract, applies latency and risk gates, and places bracket orders — fills, P&L and per-hop latency land back in DynamoDB.

Problem solved

Running automated strategies across many IBKR accounts normally means hand-managed gateways, ports and credentials, no ordering guarantees when signals arrive close together, and no way to prove that what the strategy printed is what the broker actually filled.

  • Route one TradingView alert to many accounts safely, in order, per account.
  • Provision and retire whole accounts without touching a server.
  • Keep credentials out of code, disks and shell history entirely.
  • Answer "did every alert become the right fill?" with data, daily.

Account = one ECS service

  • Each account is an ECS task: an IB Gateway container plus an executor container, credentials injected at start from Secrets Manager — never on disk.
  • A reconciler Lambda is the control plane: it polls Secrets Manager, maintains the accounts table, and creates/updates/deletes one ECS service per account. Onboarding an account = creating a secret.
  • ECS-on-EC2 with an auto-scaling group, capacity provider and memory binpacking; operator access via SSM Session Manager — no SSH, no open ports.
  • Failure domains stay small: a broken gateway affects exactly one account.

Service discovery over DNS — a latency and cost win

forward Lambda – "which task hosts this account" via Cloud Map SRV
def _resolve_via_dns(account):
    fqdn = f"acc-{account}.{NAMESPACE_NAME}"
    srv = _dns.resolve(fqdn, "SRV")
    # MULTIVALUE routing: lowest priority wins, then highest weight.
    rec = min(srv, key=lambda r: (r.priority, -r.weight))
    port, target = str(rec.port), str(rec.target).rstrip(".")
    if not port or not target:
        raise RuntimeError(f"{fqdn} SRV missing port/target")
    return _dns.resolve(target, "A")[0].address, port

Cloud Map's control-plane API calls (list_services / list_instances) needed a paid VPC interface endpoint and two TLS handshakes per signal. Replacing them with plain DNS SRV lookups through the VPC resolver — same source of truth, one UDP round trip — removed the endpoint and ~150–190 ms of per-signal latency. Every signal also carries per-hop timestamp breadcrumbs (API Gateway request time, SQS sent/received attributes, forward, executor receive, fill) with zero added network calls on the trading path — aggregatable in CloudWatch Logs Insights.

End-to-end trade reconciliation

The platform doesn't just execute — it proves it executed correctly.

The 3-way join

  • A Chrome extension (Manifest V3) captures TradingView's List-of-Trades into S3; a reconciliation Lambda joins TradingView trade ↔ ingested signal ↔ broker fill.
  • Matching uses normalized symbols — a futures continuous-contract normalizer maps front-month aliases to real contract codes; without it the join silently fails.
  • An asymmetric time window: a fill can only land at-or-after its signal — a symmetric window was false-flagging real trades.
  • Every row classified along a ladder: signal_missing → fill_missing → price_mismatch → pnl_gap → high_latency → matched.

Execution gates

  • The executor rejects stale signals (maximum allowed latency) and refuses entries without explicit take-profit and stop-loss.
  • Bracket orders (entry + TP + SL) placed only after unambiguous contract qualification.
  • Pre-close snapshotting and post-open restoration handle overnight risk windows per timezone.
  • Structured ALARM log markers become CloudWatch metrics and alerts automatically — a new alarm condition is a log line, not an infrastructure change.

Bracket execution with latency and risk checks (executor)

executor – simplified signal execution
def execute_signal_for_account(IB_INSTANCE, sig: Signal, settings: Settings) -> dict:
    contract = build_contract(sig)
    qualified = qualify_contract(contract)
    if not qualified or len(qualified) != 1:
        log_step("[ALARM] Ambiguous contract; skipping")
        return {"ok": True, "action": "skipped_ambiguous_contract"}

    qty = current_position_qty(IB_INSTANCE, qualified[0])
    sig_age = time.time() - sig.signal_timestamp if sig.signal_timestamp else None
    allow_entry = sig_age is None or sig_age <= int(settings.execution_delay)

    if qty == 0 and sig.desired_direction != 0:
        if not sig.risk_valid:
            log_step("[ALARM][EXEC] TP or SL missing, skipping execution")
            return {"ok": True, "action": "entry_ignored_no_risk_params"}
        if not allow_entry:
            return {"ok": True, "action": "entry_skipped_due_to_latency"}

        open_position_with_brackets(IB_INSTANCE, qualified[0],
            sig.desired_direction, sig.desired_qty,
            sig.take_profit, sig.stop_loss, sig.target_percentage)
        return {"ok": True, "action": "opened"}

Security & operations

Security posture

  • Everything in-VPC with security-group-to-security-group rules only; the sole public surface is the tokenized webhook.
  • Zero long-lived IAM keys: SSO locally, IAM roles everywhere else — no key material exists to leak.
  • The ops dashboard is a CloudFront + S3 static app behind an auth function, backed by a small HTTP API Lambda with per-column filtering.

Cost & ops discipline

  • Per-task smoke tests and written review checklists gate every milestone.
  • A real monthly bill variance was traced to a specific oversized status object and a full-table scan — then fixed at the source.
  • standby / wake scripts park the entire platform without deleting infrastructure.
  • DynamoDB on-demand with TTLs; the cluster binpacks accounts onto the fewest instances.

Deployment — 7 ordered stacks

  1. Network — VPC, subnets, security groups, Cloud Map namespace.
  2. Data — DynamoDB tables (signals, accounts, fills), on-demand, with TTL.
  3. Cluster — ECS on EC2: auto-scaling group, capacity provider, SSM access.
  4. Signals — API Gateway, ingest Lambda, SQS FIFO + DLQ, in-VPC forward Lambda.
  5. Ops — metric filters on structured log markers, alarms, alarm-formatter Lambda.
  6. Dashboard — CloudFront + S3 static app, auth function, HTTP API.
  7. Reconciliation — trade-capture ingestion and the 3-way recon Lambda.

All deployed in order by one PowerShell script; onboarding a new account afterwards is a single secret in Secrets Manager — the reconciler does the rest.