12. MA Cross Auto Trade

Automated trading bot based on MA5/MA10 crossover signal via real-time WebSocket

Objective

Automated trading when MA5 crosses MA10: combine WebSocket for real-time tick data, aggregate into candlesticks, compute MA, place orders on signal, and monitor via stream.

Flow

Strategy → Market Data API → Account API → Trading API → Monitor/Logger
  1. Seed historical 5m data to initialize MA
  2. Open WebSocket, subscribe trade stream (market data) and order stream (trading)
  3. Each TradeMessage → update current bar (OHLCV)
  4. When bar closes → compute MA5/MA10 → check crossover
  5. Signal detected + no pending order → risk check → place MARKET order
  6. OrderStatusMessage provides real-time order feedback, no polling needed
  7. SDK ping keepalive to maintain WebSocket connection

Architecture

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│ WebSocket    │────→│ BarBuilder   │────→│ MA Signal    │
│ Trade Tick   │     │ (OHLCV 5m)  │     │ Detector     │
└─────────────┘     └──────────────┘     └──────┬───────┘
                                                 │ BUY/SELL
                                                 ▼
┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│ WebSocket    │←────│ Order State  │←────│ Risk Check   │
│ Order Status │     │ Manager      │     │ + Place Order│
└─────────────┘     └──────────────┘     └──────────────┘

Core Components

BarBuilder — Aggregate ticks into bars

import time
from collections import deque
from dataclasses import dataclass

@dataclass
class Bar:
    ts: int; open: float; high: float; low: float; close: float; volume: int

class BarBuilder:
    def __init__(self, interval_seconds: int):
        self.interval = interval_seconds
        self._current: Bar | None = None
        self.closed: deque[Bar] = deque(maxlen=200)

    def seed(self, historical_bars) -> None:
        for b in historical_bars:
            self.closed.append(Bar(0, b.open_price, b.high_price, b.low_price, b.close_price, b.volume))

    def on_trade(self, price: float, quantity: int) -> Bar | None:
        bucket = (int(time.time()) // self.interval) * self.interval
        if self._current is None or self._current.ts != bucket:
            closed = self._current
            self._current = Bar(bucket, price, price, price, price, quantity)
            if closed:
                self.closed.append(closed)
                return closed
            return None
        self._current.high = max(self._current.high, price)
        self._current.low = min(self._current.low, price)
        self._current.close = price
        self._current.volume += quantity
        return None

    def snapshot(self) -> list[Bar]:
        """All bars (closed + current) as a list."""
        result = list(self.closed)
        if self._current is not None:
            result.append(self._current)
        return result

The sample in the repo wraps BarBuilder in a threading.Lock because WebSocket callbacks run on the SDK's own thread. The snippet above omits the lock for brevity.

MA Signal Detector

def calculate_ma(bars, period):
    if len(bars) < period:
        return None
    return sum(b.close for b in bars[-period:]) / period

def detect_cross(bars, fast, slow):
    if len(bars) < slow + 1:
        return None
    mf_now, ms_now = calculate_ma(bars, fast), calculate_ma(bars, slow)
    mf_prev, ms_prev = calculate_ma(bars[:-1], fast), calculate_ma(bars[:-1], slow)
    if mf_prev <= ms_prev and mf_now > ms_now:
        return "BUY"
    if mf_prev >= ms_prev and mf_now < ms_now:
        return "SELL"
    return None

Sample Code — Sync

python/sample_12_ma_cross_auto_trade.py
from ssi_sdk import Auth, Data, Stream, Trading, Config
from ssi_sdk.enums import OrderSide, OrderStatus
from ssi_sdk.models.streaming import TradeMessage, OrderStatusMessage
from auth_helper import ensure_auth

config = Config(...)
ACCOUNT_NO = "<your_account_no>"
SYMBOL = "SSI"
MA_FAST, MA_SLOW = 5, 10

# Terminal statuses: release the pending order
TERMINAL_STATUSES = {
    OrderStatus.FILLED,
    OrderStatus.CANCELLED,
    OrderStatus.REJECTED,
    OrderStatus.EXPIRED,
    OrderStatus.PARTIAL_CANCELLED,
}

with Auth(config) as auth:
    ensure_auth(auth, otp="<your_otp>")

    builder = BarBuilder(300)  # 5-min bars
    with Data(auth) as data:
        hist = data.market_data.get_ohlc_5minute_historical(
            symbol=SYMBOL, from_date="2026/01/01 00:00:00",
            to_date="2026/05/12 23:59:59", page=1, size=MA_SLOW + 5,
        )
        builder.seed(hist)

    state = {"active_order_id": None, "last_signal": None}

    with Trading(auth) as trading:
        def on_market_data(msg):
            if not isinstance(msg, TradeMessage):
                return
            closed_bar = builder.on_trade(msg.price, msg.quantity)
            if closed_bar:
                signal = detect_cross(builder.snapshot(), MA_FAST, MA_SLOW)
                if signal and not state["active_order_id"] and state["last_signal"] != signal:
                    state["last_signal"] = signal
                    side = OrderSide.BUY if signal == "BUY" else OrderSide.SELL
                    result = trading.trading.place_market_order(
                        account_no=ACCOUNT_NO, symbol=SYMBOL, side=side, quantity=100,
                    )
                    state["active_order_id"] = getattr(result, "order_id", "pending")

        def on_trading_event(msg):
            # Free the pending order once it reaches a terminal status
            if isinstance(msg, OrderStatusMessage) and msg.status in TERMINAL_STATUSES:
                state["active_order_id"] = None

        with Stream(auth) as stream:
            stream.streaming.on_data = on_market_data
            stream.streaming.on_trading = on_trading_event
            stream.streaming.connect()
            stream.streaming.subscribe_symbol([SYMBOL])
            stream.streaming.subscribe_order_status(ACCOUNT_NO)
            stream.streaming.ping(interval_ms=30_000)  # 30 seconds (milliseconds)
            stream.streaming.wait(timeout_ms=3_600_000)  # 1 hour

Important Notes

  • This is sample code for illustration, not a real trading strategy.
  • Always check risk (buying/selling power) before placing orders.
  • Use ping(interval_ms=30_000) to keep the WebSocket connection alive. Since SDK 3.3.0 duration parameters are in milliseconds (interval_ms, timeout_ms); the old interval= / timeout= (seconds) were removed and raise TypeError.
  • OrderStatusMessage.status can be an OrderStatus enum or a str (a code the SDK does not know); the sample compares against TERMINAL_STATUSES and uses helpers.enum_value when printing.
  • WebSocket callbacks run on the SDK's own thread: the repo sample uses threading.Lock (sync) / asyncio.Lock (async) to protect state.
  • OrderStatusMessage enables real-time order tracking without polling.
  • Prevent duplicate same-direction signals by tracking last_signal.

On this page