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- Seed historical 5m data to initialize MA
- Open WebSocket, subscribe trade stream (market data) and order stream (trading)
- Each
TradeMessage→ update current bar (OHLCV) - When bar closes → compute MA5/MA10 → check crossover
- Signal detected + no pending order → risk check → place MARKET order
OrderStatusMessageprovides real-time order feedback, no polling needed- 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 resultThe sample in the repo wraps
BarBuilderin athreading.Lockbecause 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 NoneSample Code — Sync
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 hourImportant 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 oldinterval=/timeout=(seconds) were removed and raiseTypeError. OrderStatusMessage.statuscan be anOrderStatusenum or astr(a code the SDK does not know); the sample compares againstTERMINAL_STATUSESand useshelpers.enum_valuewhen printing.- WebSocket callbacks run on the SDK's own thread: the repo sample uses
threading.Lock(sync) /asyncio.Lock(async) to protectstate. OrderStatusMessageenables real-time order tracking without polling.- Prevent duplicate same-direction signals by tracking
last_signal.