Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 150 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ Each library addresses a distinct stage: data infrastructure, feature engineerin

Deploying a backtested strategy to live markets requires careful handling of async broker connections, risk limits, and testing infrastructure. ml4t-live provides:

- The same Strategy class used in ml4t-backtest works unchanged
- Shadow mode for testing without placing real orders
- Position and order limits with rate limiting
- Interactive Brokers integration via TWS/Gateway
- Thread-safe bridging between sync strategies and async brokers
- The same Strategy class used in ml4t-backtest works unchanged in production
- Two broker integrations: Interactive Brokers (TWS/Gateway) and Alpaca (stocks + crypto)
- Six data feeds: Alpaca, IB, Databento, CCXT (100+ crypto exchanges), OKX

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intro bullet claims "Six data feeds" but only lists five (Alpaca, IB, Databento, CCXT, OKX). Either add the 6th item (e.g., BarAggregator) to the list or change the count to match the items listed.

Suggested change
- Six data feeds: Alpaca, IB, Databento, CCXT (100+ crypto exchanges), OKX
- Five data feeds: Alpaca, IB, Databento, CCXT (100+ crypto exchanges), OKX

Copilot uses AI. Check for mistakes.
- Shadow mode for testing without placing real orders (VirtualPortfolio tracking)
- 16-parameter risk configuration: position limits, order limits, loss limits, price protection

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README states this is a "16-parameter" risk configuration, but LiveRiskConfig currently has 17 fields (including blocked_assets and state_file). Please update the count (or clarify what is/isn't counted) to avoid drifting from the actual API.

Suggested change
- 16-parameter risk configuration: position limits, order limits, loss limits, price protection
- Multi-parameter risk configuration: position limits, order limits, loss limits, price protection

Copilot uses AI. Check for mistakes.
- Kill switch with crash-safe state persistence (atomic JSON writes)
- Async architecture with thread-safe sync bridge for strategy callbacks

The goal is gradual deployment: shadow mode first, then paper trading, then live with small positions.

Expand All @@ -38,8 +40,9 @@ pip install ml4t-live

```python
from ml4t.backtest import Strategy, OrderSide
from ml4t.live import LiveEngine, LiveRiskConfig
from ml4t.live.brokers.ib import IBBroker
from ml4t.live import LiveEngine, LiveRiskConfig, SafeBroker
from ml4t.live.brokers.alpaca import AlpacaBroker
from ml4t.live.feeds.alpaca_feed import AlpacaDataFeed
import asyncio

# Same strategy class from backtesting
Expand All @@ -49,49 +52,47 @@ class MyStrategy(Strategy):
broker.submit_order('SPY', 10, side=OrderSide.BUY)

async def main():
broker = IBBroker(port=7497) # Paper trading port
await broker.connect()
broker = AlpacaBroker(api_key="...", secret_key="...", paper=True)
feed = AlpacaDataFeed(api_key="...", secret_key="...", symbols=["SPY"])

config = LiveRiskConfig(
shadow_mode=True, # No real orders
max_position_value=50_000,
)
engine = LiveEngine(broker, MyStrategy(), config)
safe = SafeBroker(broker, config)

engine = LiveEngine(MyStrategy(), safe, feed)
await engine.connect()

try:
await engine.run()
finally:
await broker.disconnect()
await engine.stop()

asyncio.run(main())
```

Shadow mode output:
## Broker Integrations

```
Bar 1: SPY close = $450.02
-> Buying 10 shares of SPY (VIRTUAL - shadow mode)
Virtual position: +10 SPY @ $450.02
No real orders placed (shadow mode active)
```
### Alpaca

## Risk Configuration
Stocks and crypto with paper trading by default:

```python
config = LiveRiskConfig(
shadow_mode=True, # Virtual orders only
max_position_value=50_000, # Per-position limit
max_positions=10, # Total positions
max_order_value=10_000, # Per-order limit
max_orders_per_minute=10, # Rate limiting
max_daily_loss=5_000, # Stop trading limit
from ml4t.live.brokers.alpaca import AlpacaBroker

broker = AlpacaBroker(
api_key="...",
secret_key="...",
paper=True, # Paper trading (default)
)
await broker.connect()
```

## Broker Integration

### Interactive Brokers

Full market access via TWS or IB Gateway:

```python
from ml4t.live.brokers.ib import IBBroker

Expand All @@ -100,18 +101,119 @@ broker = IBBroker(port=7497) # TWS paper port

await broker.connect()
print(f"Connected: {broker.is_connected}")
print(f"Account: {broker.account_id}")
```

Requirements:
- IB TWS or Gateway running
- API connections enabled in TWS settings
- Paper trading account for initial testing

## Data Feeds

| Feed | Source | Coverage |
|------|--------|----------|
| `AlpacaDataFeed` | Alpaca | US stocks + crypto, real-time bars/quotes/trades |
| `IBDataFeed` | Interactive Brokers | Multi-asset tick-by-tick data |
| `DataBentoFeed` | Databento | Historical replay + real-time streaming |
| `CryptoFeed` | CCXT | 100+ crypto exchanges (Binance, Coinbase, Kraken, ...) |
| `OKXFundingFeed` | OKX | Perpetual swaps with funding rates |
| `BarAggregator` | Any feed | Multi-feed aggregation + bar assembly |

```python
from ml4t.live.feeds.alpaca_feed import AlpacaDataFeed
from ml4t.live.feeds.crypto_feed import CryptoFeed

# Stock + crypto via Alpaca
feed = AlpacaDataFeed(
api_key="...", secret_key="...",
symbols=["AAPL", "BTC/USD"],
feed="iex", # "iex" (free) or "sip" (premium)
)

# Crypto via CCXT (any of 100+ exchanges)
feed = CryptoFeed(
exchange="binance",
symbols=["BTC/USDT", "ETH/USDT"],
timeframe="1m",
)
```

## Risk Configuration

`LiveRiskConfig` controls all safety parameters. Wrap any broker with `SafeBroker` to enforce them:

```python
from ml4t.live import LiveRiskConfig, SafeBroker

config = LiveRiskConfig(
# Shadow mode
shadow_mode=True, # Virtual orders only (no real execution)

# Position limits
max_position_value=50_000, # Max $ per position
max_position_shares=1000, # Max shares per position
max_total_exposure=200_000, # Max total $ across all positions
max_positions=20, # Max number of positions

# Order limits
max_order_value=10_000, # Max $ per order
max_order_shares=500, # Max shares per order
max_orders_per_minute=10, # Rate limiting

# Loss limits
max_daily_loss=5_000, # Stop trading if exceeded
max_drawdown_pct=0.05, # Stop if 5% drawdown

# Price protection
max_price_deviation_pct=0.05, # Fat finger: reject if >5% from market
max_data_staleness_seconds=60, # Reject if data older than 60s
dedup_window_seconds=1.0, # Block duplicate orders within 1s

# Asset restrictions
allowed_assets={"SPY", "QQQ"}, # Whitelist (empty = allow all)
)

safe_broker = SafeBroker(broker, config)
```

## Safety System

### Kill Switch

When drawdown exceeds `max_drawdown_pct`, the kill switch activates and blocks all new orders. The state persists across process restarts:

```python
config = LiveRiskConfig(
kill_switch_enabled=True,
max_drawdown_pct=0.05,
state_file=".ml4t_risk_state.json", # Atomic JSON writes
)
Comment on lines +186 to +190

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example sets kill_switch_enabled=True, but SafeBroker.submit_order_async() treats kill_switch_enabled as an active kill switch and will immediately reject all orders. To document crash-safe persistence for drawdown-triggered kills, the example should not pre-enable the kill switch (instead configure max_drawdown_pct/state_file, and mention SafeBroker.enable_kill_switch() for manual activation).

Suggested change
config = LiveRiskConfig(
kill_switch_enabled=True,
max_drawdown_pct=0.05,
state_file=".ml4t_risk_state.json", # Atomic JSON writes
)
# Configure crash-safe kill switch persistence
config = LiveRiskConfig(
max_drawdown_pct=0.05,
state_file=".ml4t_risk_state.json", # Atomic JSON writes
)
# Create a SafeBroker with this risk config
safe_broker = SafeBroker(broker, config)
# Optional: manually activate the kill switch (e.g., from an ops tool)
safe_broker.enable_kill_switch()

Copilot uses AI. Check for mistakes.
```

### Virtual Portfolio

Shadow mode tracks positions internally without broker interaction:

```python
from ml4t.live import VirtualPortfolio

portfolio = VirtualPortfolio(initial_cash=100_000)
# SafeBroker uses this automatically when shadow_mode=True
```

### State Persistence

Risk state survives process crashes via atomic file writes:

- `daily_loss` - Cumulative daily loss
- `orders_placed` - Orders placed today
- `high_water_mark` - Session high equity
- `kill_switch_activated` - Persists until manually reset

## Deployment Progression

1. **Shadow Mode** (1-2 weeks): Verify logic without real orders
2. **Paper Trading** (2-4 weeks): Test with IB paper account
2. **Paper Trading** (2-4 weeks): Test with paper account
3. **Live Micro** (1-2 weeks): Small positions ($100-500)
4. **Live Small** (ongoing): Gradual size increase

Expand All @@ -133,15 +235,26 @@ result = Engine(feed, MyStrategy(), config).run()

# Live
from ml4t.live import LiveEngine
await LiveEngine(broker, MyStrategy(), risk_config).run()
await LiveEngine(MyStrategy(), safe_broker, live_feed).run()

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the live snippet, calling LiveEngine(...).run() without await engine.connect() will raise RuntimeError("Call connect() before run()"). Update this example to show the required connect/run/stop lifecycle (similar to the Quick Start section).

Suggested change
await LiveEngine(MyStrategy(), safe_broker, live_feed).run()
engine = LiveEngine(MyStrategy(), safe_broker, live_feed)
await engine.connect()
try:
await engine.run()
finally:
await engine.stop()

Copilot uses AI. Check for mistakes.
```

## Documentation

- [Installation](docs/getting-started/installation.md) — setup instructions
- [Quick Start](docs/getting-started/quickstart.md) — first live strategy
- [Brokers](docs/user-guide/brokers.md) — IB and Alpaca setup
- [Data Feeds](docs/user-guide/feeds.md) — 6 feed types
- [Risk Management](docs/user-guide/risk.md) — LiveRiskConfig and SafeBroker

## Technical Characteristics

- **Async/sync bridge**: Sync strategy callbacks work with async broker connections
- **Thread-safe**: Safe to use across multiple event loops
- **Async/sync bridge**: Sync strategy callbacks work with async broker connections via `ThreadSafeBrokerWrapper`
- **Thread-safe**: Strategy runs in worker thread, broker I/O on async event loop
- **Protocol-based**: `BrokerProtocol`, `AsyncBrokerProtocol`, `DataFeedProtocol` for extensibility
- **Virtual portfolio**: Shadow mode tracks positions without broker interaction
- **Atomic state**: Risk state persisted via POSIX-atomic file writes (crash-safe)
- **Rate limiting**: Built-in protection against order flooding
- **Type-safe**: Full type annotations throughout

## Related Libraries

Expand All @@ -153,7 +266,7 @@ await LiveEngine(broker, MyStrategy(), risk_config).run()
## Development

```bash
git clone https://github.com/applied-ai/ml4t-live.git
git clone https://github.com/ml4t/ml4t-live.git
cd ml4t-live
uv sync
uv run pytest tests/ -q
Expand All @@ -164,10 +277,11 @@ uv run ty check

This library is designed for paper trading and educational purposes. When transitioning to live trading:

- Always start with shadow_mode=True
- Always start with `shadow_mode=True`
- Set conservative position and order limits
- Enable `kill_switch_enabled=True` with a reasonable `max_drawdown_pct`

Copilot AI Mar 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Safety Notice suggests setting kill_switch_enabled=True, but that flag makes the kill switch immediately active and will block all new orders. Consider recommending leaving it False initially (and relying on max_drawdown_pct activation), or calling SafeBroker.enable_kill_switch() when you actually want to halt trading.

Suggested change
- Enable `kill_switch_enabled=True` with a reasonable `max_drawdown_pct`
- Configure a reasonable `max_drawdown_pct` for the kill switch, and only enable it (for example via `SafeBroker.enable_kill_switch()`) when you explicitly intend to halt trading

Copilot uses AI. Check for mistakes.
- Monitor virtual vs real positions carefully
- Use stop-losses and position limits
- Use the deployment progression above

## License

Expand Down