Skip to content

Sandbox Adapter

Paper trading adapter for testing without real money.

sandbox

Sandbox adapter for paper trading.

Simulates order execution without a real broker. Every order fills immediately at the given price with zero commission. Intraday prices are simulated via a random walk from the EOD close.

Example
from adapters import get_adapter

adapter = get_adapter("sandbox", {})
adapter.connect()

quote = adapter.fetch_quote("AAPL", 186.50)
# {"close": 186.32, "bid": 186.31, "ask": 186.33}

result = adapter.execute_buy("AAPL", 10, 186.33)
# {"success": True, "fill_price": 186.33, "fill_shares": 10, "commission": 0}

SandboxAdapter

Bases: BaseAdapter

Paper trading adapter.

Every order fills immediately at the requested price. Intraday prices are simulated as a random walk from the EOD close.

Source code in adapters/sandbox.py
class SandboxAdapter(BaseAdapter):
    """
    Paper trading adapter.

    Every order fills immediately at the requested price.
    Intraday prices are simulated as a random walk from the EOD close.
    """

    def __init__(self, **kwargs):
        self._last_prices: dict[str, float] = {}
        self._session: dict[str, dict] = {}  # {symbol: {open, high, low}}

    @classmethod
    def get_config_fields(cls) -> list[ConfigField]:
        """
        Return configuration fields for web UI.

        Returns:
            Empty list — sandbox needs no configuration.
        """
        return []

    def connect(self) -> bool:
        """
        Connect to sandbox.

        Returns:
            Always True.
        """
        return True

    def disconnect(self) -> None:
        """Disconnect from sandbox."""
        self._last_prices.clear()
        self._session.clear()

    def execute_buy(self, symbol: str, shares: int, price: float) -> FillResult:
        """
        Execute a buy order.

        Fills immediately at the given price with zero commission.

        Args:
            symbol: Stock symbol.
            shares: Number of shares to buy.
            price: Price per share.

        Returns:
            Fill result with the requested price and shares.
        """
        return {
            "success": True,
            "fill_price": price,
            "fill_shares": shares,
            "commission": 0.0,
        }

    def execute_sell(self, symbol: str, shares: int, price: float) -> FillResult:
        """
        Execute a sell order.

        Fills immediately at the given price with zero commission.

        Args:
            symbol: Stock symbol.
            shares: Number of shares to sell.
            price: Price per share.

        Returns:
            Fill result with the requested price and shares.
        """
        return {
            "success": True,
            "fill_price": price,
            "fill_shares": shares,
            "commission": 0.0,
        }

    def fetch_quote(self, symbol: str, eod_last_price: float | None) -> Quote | None:
        """
        Simulate an intraday quote via random walk.

        On the first call for a symbol, starts from the EOD close price.
        On subsequent calls, walks from the previous simulated price.
        Uses a random walk to simulate realistic intraday movement.

        Args:
            symbol: Stock symbol.
            eod_last_price: Last EOD close from TradeTracer.

        Returns:
            Simulated quote, or None if no EOD price available.
        """
        if not eod_last_price:
            return None

        base = self._last_prices.get(symbol, eod_last_price)
        step = random.uniform(-0.005, 0.005)
        price = round(base * (1 + step), 2)
        self._last_prices[symbol] = price

        # Track intraday session (open/high/low)
        if symbol not in self._session:
            self._session[symbol] = {"open": price, "high": price, "low": price}
        sess = self._session[symbol]
        sess["high"] = max(sess["high"], price)
        sess["low"] = min(sess["low"], price)

        # Spread: 0.01-0.05% of price, varies per tick
        spread_pct = random.uniform(0.0001, 0.0005)
        half_spread = round(price * spread_pct, 2) or 0.01

        # Bid/ask: last trade (close) is between bid and ask
        bid = round(price - half_spread, 2)
        ask = round(price + half_spread, 2)

        return {
            "open": sess["open"],
            "high": sess["high"],
            "low": sess["low"],
            "close": price,
            "volume": random.randint(100, 5000),
            "bid": bid,
            "ask": ask,
            "time": int(time.time()),
        }

    def fetch_bars(self, symbol: str, count: int) -> list[Bar]:
        """
        Generate simulated historical bars via random walk.

        Walks backwards from the last known price (or a neutral base)
        to produce ``count`` 1-minute bars ending at the current time.
        """
        base = self._last_prices.get(symbol)
        if not base:
            return []

        now = int(time.time())
        bars: list[Bar] = []
        price = base

        # Walk backwards to generate prices, then reverse
        prices = [price]
        for _ in range(count - 1):
            step = random.uniform(-0.003, 0.003)
            price = round(price / (1 + step), 2)
            prices.append(price)
        prices.reverse()

        for i, p in enumerate(prices):
            t = now - (len(prices) - 1 - i) * 60
            high = round(p * (1 + random.uniform(0, 0.002)), 2)
            low = round(p * (1 - random.uniform(0, 0.002)), 2)
            bars.append({
                "t": t,
                "o": round(p + random.uniform(-0.1, 0.1), 2),
                "h": high,
                "l": low,
                "c": p,
                "v": random.randint(100, 5000),
            })

        return bars

get_config_fields() classmethod

Return configuration fields for web UI.

Returns:

Type Description
list[ConfigField]

Empty list — sandbox needs no configuration.

Source code in adapters/sandbox.py
@classmethod
def get_config_fields(cls) -> list[ConfigField]:
    """
    Return configuration fields for web UI.

    Returns:
        Empty list — sandbox needs no configuration.
    """
    return []

connect()

Connect to sandbox.

Returns:

Type Description
bool

Always True.

Source code in adapters/sandbox.py
def connect(self) -> bool:
    """
    Connect to sandbox.

    Returns:
        Always True.
    """
    return True

disconnect()

Disconnect from sandbox.

Source code in adapters/sandbox.py
def disconnect(self) -> None:
    """Disconnect from sandbox."""
    self._last_prices.clear()
    self._session.clear()

execute_buy(symbol, shares, price)

Execute a buy order.

Fills immediately at the given price with zero commission.

Parameters:

Name Type Description Default
symbol str

Stock symbol.

required
shares int

Number of shares to buy.

required
price float

Price per share.

required

Returns:

Type Description
FillResult

Fill result with the requested price and shares.

Source code in adapters/sandbox.py
def execute_buy(self, symbol: str, shares: int, price: float) -> FillResult:
    """
    Execute a buy order.

    Fills immediately at the given price with zero commission.

    Args:
        symbol: Stock symbol.
        shares: Number of shares to buy.
        price: Price per share.

    Returns:
        Fill result with the requested price and shares.
    """
    return {
        "success": True,
        "fill_price": price,
        "fill_shares": shares,
        "commission": 0.0,
    }

execute_sell(symbol, shares, price)

Execute a sell order.

Fills immediately at the given price with zero commission.

Parameters:

Name Type Description Default
symbol str

Stock symbol.

required
shares int

Number of shares to sell.

required
price float

Price per share.

required

Returns:

Type Description
FillResult

Fill result with the requested price and shares.

Source code in adapters/sandbox.py
def execute_sell(self, symbol: str, shares: int, price: float) -> FillResult:
    """
    Execute a sell order.

    Fills immediately at the given price with zero commission.

    Args:
        symbol: Stock symbol.
        shares: Number of shares to sell.
        price: Price per share.

    Returns:
        Fill result with the requested price and shares.
    """
    return {
        "success": True,
        "fill_price": price,
        "fill_shares": shares,
        "commission": 0.0,
    }

fetch_quote(symbol, eod_last_price)

Simulate an intraday quote via random walk.

On the first call for a symbol, starts from the EOD close price. On subsequent calls, walks from the previous simulated price. Uses a random walk to simulate realistic intraday movement.

Parameters:

Name Type Description Default
symbol str

Stock symbol.

required
eod_last_price float | None

Last EOD close from TradeTracer.

required

Returns:

Type Description
Quote | None

Simulated quote, or None if no EOD price available.

Source code in adapters/sandbox.py
def fetch_quote(self, symbol: str, eod_last_price: float | None) -> Quote | None:
    """
    Simulate an intraday quote via random walk.

    On the first call for a symbol, starts from the EOD close price.
    On subsequent calls, walks from the previous simulated price.
    Uses a random walk to simulate realistic intraday movement.

    Args:
        symbol: Stock symbol.
        eod_last_price: Last EOD close from TradeTracer.

    Returns:
        Simulated quote, or None if no EOD price available.
    """
    if not eod_last_price:
        return None

    base = self._last_prices.get(symbol, eod_last_price)
    step = random.uniform(-0.005, 0.005)
    price = round(base * (1 + step), 2)
    self._last_prices[symbol] = price

    # Track intraday session (open/high/low)
    if symbol not in self._session:
        self._session[symbol] = {"open": price, "high": price, "low": price}
    sess = self._session[symbol]
    sess["high"] = max(sess["high"], price)
    sess["low"] = min(sess["low"], price)

    # Spread: 0.01-0.05% of price, varies per tick
    spread_pct = random.uniform(0.0001, 0.0005)
    half_spread = round(price * spread_pct, 2) or 0.01

    # Bid/ask: last trade (close) is between bid and ask
    bid = round(price - half_spread, 2)
    ask = round(price + half_spread, 2)

    return {
        "open": sess["open"],
        "high": sess["high"],
        "low": sess["low"],
        "close": price,
        "volume": random.randint(100, 5000),
        "bid": bid,
        "ask": ask,
        "time": int(time.time()),
    }

fetch_bars(symbol, count)

Generate simulated historical bars via random walk.

Walks backwards from the last known price (or a neutral base) to produce count 1-minute bars ending at the current time.

Source code in adapters/sandbox.py
def fetch_bars(self, symbol: str, count: int) -> list[Bar]:
    """
    Generate simulated historical bars via random walk.

    Walks backwards from the last known price (or a neutral base)
    to produce ``count`` 1-minute bars ending at the current time.
    """
    base = self._last_prices.get(symbol)
    if not base:
        return []

    now = int(time.time())
    bars: list[Bar] = []
    price = base

    # Walk backwards to generate prices, then reverse
    prices = [price]
    for _ in range(count - 1):
        step = random.uniform(-0.003, 0.003)
        price = round(price / (1 + step), 2)
        prices.append(price)
    prices.reverse()

    for i, p in enumerate(prices):
        t = now - (len(prices) - 1 - i) * 60
        high = round(p * (1 + random.uniform(0, 0.002)), 2)
        low = round(p * (1 - random.uniform(0, 0.002)), 2)
        bars.append({
            "t": t,
            "o": round(p + random.uniform(-0.1, 0.1), 2),
            "h": high,
            "l": low,
            "c": p,
            "v": random.randint(100, 5000),
        })

    return bars