Appearance

Personalize your experience

Mode
Accent Color
Direction
Python Scripting with Pyodide

Python Scripting

IDE

Quatick strategies run in Python 3.11 via Pyodide — a full CPython compiled to WebAssembly. This means your strategies execute entirely in the browser, with no server roundtrip, while having access to the scientific Python ecosystem (NumPy, pandas, SciPy, etc.).

1. How Pyodide Works

  • Pyodide is a WebAssembly build of CPython 3.11 that runs in the browser
  • The first load downloads ~12 MB of — cached after first use
  • Python packages are installed from the Pyodide package index (not PyPI directly)
  • C- packages must be pre-compiled for WebAssembly
  • Performance is ~3-5x slower than CPython, but vectorized NumPy operations are near-native speed
  • No file system access, no network access, no threading — pure computation only

2. Data Flow

Chart is passed from to Python as a dictionary, which you convert to a pandas DataFrame:

python
import pandas as pdimport numpy as npdef initialize(bars_data):    """    Called once when the strategy starts.    bars_data is a list of dicts with keys: time, open, high, low, close, volume    """    df = pd.DataFrame(bars_data)    df['time'] = pd.to_datetime(df['time'], unit='s')    df.set_index('time', inplace=True)        # Add technical indicators    df['sma_20'] = df['close'].rolling(20).mean()    df['sma_50'] = df['close'].rolling(50).mean()    df['rsi'] = compute_rsi(df['close'], 14)    df['atr'] = compute_atr(df, 14)        return df

3.1 Simple Moving Average (SMA)

python
def sma(series: pd.Series, period: int) -> pd.Series:
    """Simple Moving Average"""
    return series.rolling(window=period).mean()

3.2 Exponential Moving Average (EMA)

python
def ema(series: pd.Series, period: int) -> pd.Series:
    """Exponential Moving Average"""
    return series.ewm(span=period, adjust=False).mean()

3.3 RSI (Relative Strength Index)

python
def compute_rsi(close: pd.Series, period: int = 14) -> pd.Series:    """RSI using Wilder's smoothing method"""    delta = close.diff()    gain = delta.where(delta > 0, 0.0)    loss = -delta.where(delta < 0, 0.0)        # Wilder's smoothing (equivalent to EMA with alpha=1/period)    avg_gain = gain.ewm(alpha=1/period, min_periods=period).mean()    avg_loss = loss.ewm(alpha=1/period, min_periods=period).mean()        rs = avg_gain / avg_loss    rsi = 100 - (100 / (1 + rs))    return rsi

3.4 ATR (Average True Range)

python
def compute_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:    """Average True Range for volatility measurement"""    high = df['high']    low = df['low']    close = df['close']        tr1 = high - low                       # Current range    tr2 = (high - close.shift(1)).abs()    # Gap up    tr3 = (low - close.shift(1)).abs()     # Gap down        true_range = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)    return true_range.ewm(alpha=1/period, min_periods=period).mean()

3.5 MACD

python
def compute_macd(close: pd.Series, fast=12, slow=26, signal=9):    """MACD: Moving Average Convergence Divergence"""    ema_fast = close.ewm(span=fast, adjust=False).mean()    ema_slow = close.ewm(span=slow, adjust=False).mean()        macd_line = ema_fast - ema_slow    signal_line = macd_line.ewm(span=signal, adjust=False).mean()    histogram = macd_line - signal_line        return macd_line, signal_line, histogram

3.6 Bollinger Bands

python
def compute_bollinger(close: pd.Series, period=20, std_dev=2.0):    """Bollinger Bands"""    middle = close.rolling(period).mean()    std = close.rolling(period).std()        upper = middle + std * std_dev    lower = middle - std * std_dev        # %B: Where price is relative to bands (0=lower, 1=upper)    percent_b = (close - lower) / (upper - lower)        # Bandwidth: How wide the bands are (volatility measure)    bandwidth = (upper - lower) / middle        return upper, middle, lower, percent_b, bandwidth

4.1 Crossover Detection

python
def crossover(fast: pd.Series, slow: pd.Series) -> pd.Series:    """Returns True where fast crosses above slow"""    return (fast > slow) & (fast.shift(1) <= slow.shift(1))def crossunder(fast: pd.Series, slow: pd.Series) -> pd.Series:    """Returns True where fast crosses below slow"""    return (fast < slow) & (fast.shift(1) >= slow.shift(1))# Usage:buy_signals = crossover(df['sma_20'], df['sma_50'])sell_signals = crossunder(df['sma_20'], df['sma_50'])

4.2 Threshold Detection

python
def crosses_above(series: pd.Series, threshold: float) -> pd.Series:    """Returns True where series crosses above a threshold"""    return (series > threshold) & (series.shift(1) <= threshold)def crosses_below(series: pd.Series, threshold: float) -> pd.Series:    """Returns True where series crosses below a threshold"""    return (series < threshold) & (series.shift(1) >= threshold)# Usage:rsi_oversold = crosses_above(df['rsi'], 30)   # RSI crosses above 30rsi_overbought = crosses_below(df['rsi'], 70)  # RSI crosses below 70

4.3 Pattern Detection

python
def detect_engulfing(df: pd.DataFrame) -> pd.Series:    """    Detect bullish and bearish engulfing candles.    Returns: 1 for bullish engulfing, -1 for bearish, 0 for none.    """    prev_body = df['close'].shift(1) - df['open'].shift(1)    curr_body = df['close'] - df['open']        bullish = (        (prev_body < 0) &           # Previous candle is bearish        (curr_body > 0) &            # Current candle is bullish        (df['open'] <= df['close'].shift(1)) &   # Opens at or below prev close        (df['close'] >= df['open'].shift(1))      # Closes at or above prev open    )        bearish = (        (prev_body > 0) &           # Previous candle is bullish        (curr_body < 0) &            # Current candle is bearish        (df['open'] >= df['close'].shift(1)) &   # Opens at or above prev close        (df['close'] <= df['open'].shift(1))      # Closes at or below prev open    )        result = pd.Series(0, index=df.index)    result[bullish] = 1    result[bearish] = -1    return result

5. Backtesting Framework

python
class Backtester:    """Simple backtesting engine for strategy validation."""        def __init__(self, df, config):        self.df = df        self.config = config        self.capital = config['initial_capital']        self.position = None     # {'side': 'long'/'short', 'entry': price, 'size': qty, 'stop': price}        self.trades = []        self.equity_curve = []        def run(self, signal_func):        """Execute backtest with the given signal function."""        for i in range(1, len(self.df)):            bar = self.df.iloc[i]            prev = self.df.iloc[i-1]                        # Check stop loss / take profit            if self.position:                self._check_exit(bar)                        # Generate signal            signal = signal_func(self.df.iloc[:i+1], self.position, self.config)                        if signal['signal'] == Signal.BUY and not self.position:                self._open_position('long', bar['close'], i,                    stop=signal.get('stop_loss'),                    tp=signal.get('take_profit'))                        elif signal['signal'] == Signal.SELL and not self.position:                self._open_position('short', bar['close'], i,                    stop=signal.get('stop_loss'),                    tp=signal.get('take_profit'))                        elif signal['signal'] == Signal.CLOSE and self.position:                self._close_position(bar['close'], i, 'Signal close')                        # Track equity            equity = self.capital            if self.position:                pnl = self._unrealized_pnl(bar['close'])                equity += pnl            self.equity_curve.append({                'time': bar.name if hasattr(bar.name, 'timestamp') else i,                'equity': equity,            })                return self._calculate_metrics()        def _open_position(self, side, price, bar_idx, stop=None, tp=None):        risk_pct = self.config['position_size_pct'] / 100        size = (self.capital * risk_pct) / price        commission = price * size * (self.config['commission_pct'] / 100)                self.position = {            'side': side,            'entry': price,            'size': size,            'stop': stop,            'take_profit': tp,            'entry_bar': bar_idx,        }        self.capital -= commission        def _close_position(self, price, bar_idx, reason):        pnl = self._unrealized_pnl(price)        commission = price * self.position['size'] * (self.config['commission_pct'] / 100)                self.capital += pnl - commission        self.trades.append({            'side': self.position['side'],            'entry': self.position['entry'],            'exit': price,            'size': self.position['size'],            'pnl': pnl - commission,            'bars_held': bar_idx - self.position['entry_bar'],            'reason': reason,        })        self.position = None        def _unrealized_pnl(self, current_price):        if self.position['side'] == 'long':            return (current_price - self.position['entry']) * self.position['size']        else:            return (self.position['entry'] - current_price) * self.position['size']        def _check_exit(self, bar):        if self.position['stop']:            if self.position['side'] == 'long' and bar['low'] <= self.position['stop']:                self._close_position(self.position['stop'], -1, 'Stop loss')            elif self.position['side'] == 'short' and bar['high'] >= self.position['stop']:                self._close_position(self.position['stop'], -1, 'Stop loss')                if self.position and self.position.get('take_profit'):            if self.position['side'] == 'long' and bar['high'] >= self.position['take_profit']:                self._close_position(self.position['take_profit'], -1, 'Take profit')            elif self.position['side'] == 'short' and bar['low'] <= self.position['take_profit']:                self._close_position(self.position['take_profit'], -1, 'Take profit')        def _calculate_metrics(self):        if not self.trades:            return {'total_trades': 0, 'message': 'No trades generated'}                pnls = [t['pnl'] for t in self.trades]        wins = [p for p in pnls if p > 0]        losses = [p for p in pnls if p <= 0]                return {            'total_trades': len(self.trades),            'winning_trades': len(wins),            'losing_trades': len(losses),            'win_rate': len(wins) / len(self.trades) * 100,            'total_pnl': sum(pnls),            'avg_win': np.mean(wins) if wins else 0,            'avg_loss': np.mean(losses) if losses else 0,            'profit_factor': abs(sum(wins) / sum(losses)) if losses else float('inf'),            'max_drawdown': self._max_drawdown(),            'sharpe_ratio': self._sharpe_ratio(),        }        def _max_drawdown(self):        equity = [e['equity'] for e in self.equity_curve]        peak = equity[0]        max_dd = 0        for e in equity:            peak = max(peak, e)            dd = (peak - e) / peak * 100            max_dd = max(max_dd, dd)        return max_dd        def _sharpe_ratio(self, risk_free=0.02):        if len(self.equity_curve) < 2:            return 0        equity = [e['equity'] for e in self.equity_curve]        returns = pd.Series(equity).pct_change().dropna()        if returns.std() == 0:            return 0        return (returns.mean() - risk_free/252) / returns.std() * np.sqrt(252)

6. Tips for Pyodide Performance

  • Use vectorized NumPy/pandas operations instead of Python loops — 10-100x faster
  • Avoid creating temporary DataFrames — use inplace=True where possible
  • Pre-compute all indicators in initialize(), not in generate_signal()
  • Use .values or .to_numpy() when you don't need the pandas index
  • Limit lookback size — don't pass the entire history to every signal call
  • Use numba-compatible patterns even though numba isn't available — they're cleaner
  • Profile slow strategies by timing individual sections with time.time()
ℹ
Note:Pyodide's first load may take 5-10 seconds. Subsequent runs are instant because the is cached in the browser.

Related articles

  • Quatick Script SDK Reference

    Coding / IDE / Reference

    The Quatick IDE injects the SDK directly into TypeScript indicator, overlay, and strategy scripts at Apply time. You do not import SDK functions. Use the…

  • Backtesting in Python and C++

    Backtesting / SDKs

    The quatick Python package and the header-only C++ library let you import indicators, backtest on your own machine and send the same file to Quatick.…

  • Built-in Indicators & Overlays

    Coding / IDE / Reference

    Quatick ships with a full library of pre-built indicators and overlays. You can use these out of the box, or study their implementations as reference for…

  • Engines — Python vs C++

    Automation / Predefined Strategies

    Two independent implementations of the same grid semantics, consuming the same config and the same command channel. The choice is a runtime choice, not a…

  • IDE Keyboard Shortcuts

    Coding / IDE / Keyboard

    Quatick IDE supports extensive keyboard shortcuts inspired by VS Code. Shortcuts are organized by area. On macOS, replace Ctrl with ⌘ and Alt with ⌥ where…

  • Python: Event-Driven Strategies

    Backtesting / SDKs / Python

    Subclass quatick.Strategy to trade bar by bar with real orders: brackets, trailing stops, limits, risk-based sizing and a drawdown kill switch. The same…

  • Python: Research Toolkit

    Backtesting / SDKs / Python

    Everything after the first backtest: tearsheet analytics, multi-asset portfolios, position sizing, optimization and walk-forward, overfitting tests, Monte…

  • Quatick IDE — Getting Started

    Coding / IDE / Getting Started

    Quatick IDE is a browser-based development environment for creating custom trading indicators, overlays, and automated strategies. Scripts are written in…

  • Script API Reference

    Coding / IDE / Reference

    This reference covers all APIs, interfaces, and utilities available to indicator, overlay, and strategy scripts in Quatick IDE.

Browse all knowledge base articles →