Local Development¶
Everything you need to know about developing Meridian plugins locally -- no infrastructure, no Docker, no accounts. The LocalClient gives you an in-memory trading simulator with the same API you will use in production.
Local Mode¶
| Local Mode | |
|---|---|
| Client | LocalClient |
| Infrastructure | None -- everything in-process |
| Use case | Development, backtesting, unit tests |
| Data source | CSV, Parquet, DataFrame, dicts |
| Order execution | Simulated -- fills at close price |
# Local mode -- no network, no sidecar
from meridian import LocalClient
client = LocalClient(starting_cash=100_000)
Connected mode (live data and execution) is planned for a future release.
LocalClient API Reference¶
Data Loading¶
load_bars(symbol, source)¶
Load price history for a symbol. The source argument is flexible:
# From a CSV file
client.load_bars("AAPL", "data/aapl_2023.csv")
# From a Parquet file (requires pandas)
client.load_bars("AAPL", "data/aapl_2023.parquet")
# From a list of dicts
client.load_bars("AAPL", [
{"date": "2024-01-02", "open": 150.0, "high": 152.5, "low": 149.5, "close": 151.0, "volume": 1000000},
{"date": "2024-01-03", "open": 151.0, "high": 153.0, "low": 150.0, "close": 152.5, "volume": 1100000},
])
# From a pandas DataFrame
import pandas as pd
df = pd.read_csv("data/aapl_2023.csv")
client.load_bars("AAPL", df)
Flexible Column Names¶
You do not need to rename your columns. The SDK recognizes common variants:
| Standard | Also accepted |
|---|---|
date |
timestamp, time, dt, Date, Timestamp |
open |
o, Open, OPEN |
high |
h, High, HIGH |
low |
l, Low, LOW |
close |
c, Close, CLOSE, adj_close, Adj Close |
volume |
vol, v, Volume, VOLUME |
Only date and close are required. Missing open, high, low, or volume columns default to zero.
CSV Format¶
date,open,high,low,close,volume
2024-01-02,150.00,152.50,149.50,151.00,1000000
2024-01-03,151.00,153.00,150.00,152.50,1100000
Dates are parsed automatically. ISO 8601 (2024-01-02), US format (01/02/2024), and Unix timestamps are all supported.
Order Submission¶
submit_order(symbol, direction, quantity)¶
How simulation works:
- The order fills immediately at the current bar's close price
- For a BUY: cash is debited by
price * quantity - For a SELL: cash is credited by
price * quantity - A
Fillobject is appended to the fill log - A balanced double-entry journal record is created
Rejection cases:
| Condition | Result |
|---|---|
| Insufficient cash for a BUY | Order rejected, on_reject callback fired |
| Selling more shares than held | Order rejected, on_reject callback fired |
| Symbol not loaded | Order rejected, on_reject callback fired |
| Order during warmup period | Order silently skipped (not rejected) |
Position Tracking¶
positions(symbol=None)¶
Returns current positions computed cumulatively from fills.
# All positions
client.positions()
# {'AAPL': {'quantity': Decimal('100'), 'cost_basis': Decimal('15100.00')}}
# Single symbol
client.positions("AAPL")
# {'AAPL': {'quantity': Decimal('100'), 'cost_basis': Decimal('15100.00')}}
quantity is the net share count. cost_basis is the total cost of the current position (average cost times quantity).
Cash Tracking¶
cash(currency=None)¶
Starting balance is configurable at construction:
client = LocalClient(starting_cash=500_000) # half a million
client = LocalClient(starting_cash=1_000_000) # one million
Cash is debited on buys and credited on sells. The balance is checked before every buy order -- insufficient cash causes rejection.
Query Methods¶
fills(start=None, end=None)¶
Every executed order produces a Fill:
for fill in client.fills():
print(f"{fill.timestamp} {fill.direction} {fill.quantity} {fill.symbol} @ ${fill.price}")
Each fill has: symbol, direction (BUY/SELL), quantity, price, timestamp, order_id, fill_id.
journal(start=None, end=None)¶
Every fill generates a balanced double-entry accounting record:
for entry in client.journal():
print(f"{entry.timestamp} DR {entry.debit_account} / CR {entry.credit_account} ${entry.amount}")
A BUY debits the asset account and credits cash. A SELL debits cash and credits the asset account. The journal always balances.
orders(status=None)¶
# All orders
client.orders()
# Filter by status
client.orders(status="FILLED")
client.orders(status="REJECTED")
history(symbol, start=None, end=None)¶
Query loaded bar data:
available()¶
List all symbols that have been loaded:
resolve(symbol)¶
Look up metadata for a loaded symbol:
client.resolve("AAPL")
# {'symbol': 'AAPL', 'bar_count': 252, 'start': '2024-01-02', 'end': '2024-12-31'}
SignalEngine¶
The SignalEngine base class drives your strategy through historical data one bar at a time.
from meridian import LocalClient, SignalEngine
from decimal import Decimal
client = LocalClient(starting_cash=100_000)
client.load_bars("AAPL", "data/aapl_2023.csv")
client.load_bars("MSFT", "data/msft_2023.csv")
class MyStrategy(SignalEngine):
def on_data(self, window):
"""Called once per bar date. This is where your logic lives."""
bars = window.bars("AAPL", lookback=20)
if len(bars) < 20:
return
latest = window.latest("AAPL")
sma = sum(b.close for b in bars) / len(bars)
pos = self.client.positions("AAPL")
holding = pos.get("AAPL", {}).get("quantity", Decimal(0))
if latest.close > sma and holding == 0:
self.submit_order("AAPL", "BUY", 100)
elif latest.close < sma and holding > 0:
self.submit_order("AAPL", "SELL", int(holding))
def on_fill(self, fill):
"""Called after every successful fill."""
print(f"Filled: {fill.direction} {fill.quantity} {fill.symbol} @ ${fill.price}")
def on_reject(self, order, reason):
"""Called when an order is rejected."""
print(f"Rejected: {order.symbol} {order.direction} -- {reason}")
MyStrategy(client).run(warmup=20)
Warmup¶
The warmup parameter tells the engine how many bars to replay before orders are allowed:
During warmup, window.is_warmup is True and any calls to submit_order are silently skipped. This gives indicators time to stabilize -- a 50-day moving average needs 50 bars of history before producing meaningful values.
DataWindow¶
The window argument passed to on_data provides:
| Method | Returns |
|---|---|
window.bars(symbol, lookback=N) |
List of the last N bars for the symbol |
window.latest(symbol) |
The current bar for the symbol |
window.is_warmup |
True during the warmup period |
window.date |
The current bar date |
Each bar is an object with date, open, high, low, close, and volume attributes. Prices are Decimal values for exact arithmetic.
Event Handlers¶
| Handler | When it fires |
|---|---|
on_data(window) |
Once per bar date (required) |
on_fill(fill) |
After every successful order fill |
on_reject(order, reason) |
When an order is rejected |
All handlers are optional except on_data.
Next Steps¶
- SDK Core Concepts -- sidecar protocol, plugin lifecycle, kernel API surfaces
- Quick Start -- zero-to-running in under 5 minutes