Skip to main content

SPL — Shadow · Paper · Live

A modular DIY trading execution platform that lets you run the same strategy logic across Shadow, Paper, and Live modes — without touching real liquidity until you choose to.


🌐 Overview

SPL (Shadow · Paper · Live) is a pluggable execution engine for running your trade strategies in three tiers:

ModeDescriptionMarket Impact
PaperSynthetic fills via quotes + slippage modelNone
ShadowFills triggered by live trade tape (no orders sent)None
LiveReal order placement with exchange SDKReal

Use the same indicator and signal code across all modes. Switch environments by config — not by rewriting logic.


🧱 Architecture

spl/
├─ core/
│ ├─ types.py
│ ├─ interfaces.py
│ └─ utils.py
├─ engine/
│ ├─ engine.py
│ ├─ fill_paper.py
│ └─ fill_shadow.py
├─ adapters/
│ ├─ drift.py
│ └─ hyperliquid.py
├─ exec/
│ ├─ backend_paper.py
│ ├─ backend_shadow.py
│ └─ backend_live.py
├─ storage/
│ ├─ sqlite_store.py
│ └─ parquet_store.py
├─ risk/
│ └─ basic.py
└─ cli/
└─ main.py

⚙️ Core Interfaces

(abbreviated here for brevity)

from typing import Protocol, Iterable, Dict, Any
from .types import Quote, Trade, OrderReq, Fill, AccountSnapshot

class IMarketData(Protocol):
def subscribe_quotes(self, symbol: str) -> Iterable[Quote]: ...
def subscribe_trades(self, symbol: str) -> Iterable[Trade]: ...
def get_funding(self, symbol: str) -> float: ...
def get_mark_price(self, symbol: str) -> float: ...

…and similarly for IExecutionBackend, IStorage, and IRisk.


📊 Fill Models

(unchanged — same examples for Paper and Shadow fills)


🔌 Adapters

  • Drift: uses driftpy for live quotes/trades/funding.
  • Hyperliquid: uses hyperliquid-python-sdk.
  • Adapters only translate data → SPL’s core types.

🧮 Engine

(unchanged; core event loop same as before)


🧾 Config (Now in TOML)

# SPL configuration
# mode can be "shadow", "paper", or "live"
mode = "shadow"
exchange = "drift"
symbol = "SOL-PERP"

[fees]
bps = 1.0 # trading fee in basis points

[latency]
ms = 120

[slippage]
bps = 1.5

[risk]
max_attempts = 3
max_notional = 150000

[risk.circuit_breaker]
stops = 3
minutes = 10

[storage]
kind = "sqlite"
dsn = "sqlite:///spl.db"

🖥️ CLI

spl run --config config/drift.shadow.toml

Example Python Entry

import click, tomllib
from pathlib import Path
from spl.adapters.drift import DriftMarket
from spl.exec.backend_shadow import ShadowBackend
from spl.storage.sqlite_store import SQLiteStore
from spl.risk.basic import BasicRisk
from spl.engine.engine import Engine
from spl.strategies.demo import RangeBounce

@click.command()
@click.option("--config", required=True)
def run(config):
cfg = tomllib.load(Path(config).open("rb"))
market = DriftMarket(cfg)
exec_ = ShadowBackend(SQLiteStore(cfg), fee_bps=cfg["fees"]["bps"])
risk = BasicRisk(cfg["risk"])
eng = Engine(market, exec_, SQLiteStore(cfg), risk)
strat = RangeBounce(cfg)
eng.run(cfg["symbol"], strat)

if __name__ == "__main__":
run()

🧠 Design Philosophy

  • Pluggable: Market data, execution, storage, risk all swappable.
  • Reproducible: Config + run ID → reproducible behavior.
  • Safe: Live mode opt-in with confirmation guard.
  • Unified: Same logic runs identically in paper, shadow, or live.

🧩 Future Work

  • Async merge by timestamp
  • Funding & fees models per venue
  • Native OCO emulation
  • Grafana/Prometheus metrics
  • Historical replay ingestion
  • Backtest vs. shadow performance comparison

⚡ Installation

pip install -e .
spl run --config config/drift.shadow.toml

📜 License

MIT — freely usable and forkable.