Skip to content

Latest commit

 

History

History
235 lines (185 loc) · 7.65 KB

File metadata and controls

235 lines (185 loc) · 7.65 KB

QuantDinger Python Strategy Development Guide

This guide describes how to develop trading strategies using Python in the QuantDinger platform. QuantDinger provides a flexible execution environment that supports data access, indicator calculation, and signal generation.

1. Overview

Strategies in QuantDinger operate based on the Signal Provider mode. The system executes your Python script, which is expected to process market data (a DataFrame) and output trading signals.

The execution flow is as follows:

  1. Input: The system injects a df (Pandas DataFrame) containing OHLCV data into your script environment.
  2. Processing: You use Python (pandas, numpy) to calculate indicators and define buy/sell logic.
  3. Output: You construct a specific output dictionary containing plot data and signals.

2. Environment & Data

Your script runs in a sandboxed Python environment.

2.1 Pre-imported Libraries

The following libraries are available by default (do not import them):

  • pd (pandas)
  • np (numpy)

2.2 Input Data (df)

A Pandas DataFrame variable named df is automatically available in the global scope. It contains the historical market data for the selected symbol and timeframe.

Columns:

  • time: Timestamp (datetime or int, depending on context, usually localized)
  • open: Open price (float)
  • high: High price (float)
  • low: Low price (float)
  • close: Close price (float)
  • volume: Trading volume (float)

Example:

# Access closing prices
closes = df['close']

# Calculate a Simple Moving Average (SMA)
sma_20 = df['close'].rolling(20).mean()

3. Developing a Strategy

A standard strategy script consists of three parts:

  1. Indicator Calculation: Compute technical indicators.
  2. Signal Generation: Define logic for Buy and Sell signals.
  3. Output Construction: Format the results for the chart and execution engine.

3.1 Indicator Calculation

You can use standard Pandas operations to calculate indicators.

# Example: MACD Calculation
short_window = 12
long_window = 26
signal_window = 9

ema12 = df['close'].ewm(span=short_window, adjust=False).mean()
ema26 = df['close'].ewm(span=long_window, adjust=False).mean()
macd = ema12 - ema26
signal_line = macd.ewm(span=signal_window, adjust=False).mean()

3.2 Signal Generation (Crucial)

You MUST create two boolean Series in the df or as standalone variables, named buy and sell.

  • True indicates a signal trigger.
  • False indicates no signal.

Important: Edge Triggering To avoid repeated signals on consecutive candles (which might lead to multiple orders depending on backend config), it is best practice to use edge-triggered signals (signal only on the moment the condition becomes true).

# Condition: Close price crosses above SMA 20
condition_buy = (df['close'] > sma_20) & (df['close'].shift(1) <= sma_20.shift(1))

# Condition: Close price crosses below SMA 20
condition_sell = (df['close'] < sma_20) & (df['close'].shift(1) >= sma_20.shift(1))

# Assign to df (Required for backtesting)
df['buy'] = condition_buy.fillna(False)
df['sell'] = condition_sell.fillna(False)

Note on Signal Types:

  • QuantDinger normalizes signals based on your strategy configuration (Long-only, Short-only, or Bi-directional).
  • Your script simply outputs "buy" (bullish intent) or "sell" (bearish intent). The backend handles opening/closing positions.

3.3 Visual Markers

For charting, you often want to place the signal icon slightly above or below the candle.

# Place Buy marker 0.5% below the Low
buy_marks = [
    df['low'].iloc[i] * 0.995 if df['buy'].iloc[i] else None 
    for i in range(len(df))
]

# Place Sell marker 0.5% above the High
sell_marks = [
    df['high'].iloc[i] * 1.005 if df['sell'].iloc[i] else None 
    for i in range(len(df))
]

3.4 The output Variable (Mandatory)

The final step is to assign a dictionary to the variable output. This tells the frontend what to draw and the backend where the signals are.

Structure:

output = {
    "name": "My Strategy Name",
    "plots": [ ... ],   # List of lines/indicators to draw
    "signals": [ ... ]  # List of signal markers
}

Plots Schema:

  • name: Legend name (e.g., "SMA 20")
  • data: List of values (must match df length). Use .tolist().
  • color: Hex color string (e.g., "#ff0000").
  • overlay: True to draw on main chart (price), False to draw on separate pane (like RSI/MACD).

Signals Schema:

  • type: Must be "buy" or "sell".
  • text: Text to display on icon (e.g., "B", "S").
  • data: List of values (prices) where the icon appears. None where no signal.
  • color: Icon color.

4. Complete Example: Dual SMA Crossover

Here is a full, copy-pasteable example of a strategy that buys when SMA(10) crosses above SMA(30) and sells when it crosses below.

# 1. Indicator Calculation
# -----------------------
# Calculate Short and Long SMAs
sma_short = df['close'].rolling(10).mean()
sma_long = df['close'].rolling(30).mean()

# 2. Signal Logic
# -----------------------
# Buy: Short SMA crosses above Long SMA
raw_buy = (sma_short > sma_long) & (sma_short.shift(1) <= sma_long.shift(1))

# Sell: Short SMA crosses below Long SMA
raw_sell = (sma_short < sma_long) & (sma_short.shift(1) >= sma_long.shift(1))

# Clean up NaNs and ensure boolean type
buy = raw_buy.fillna(False)
sell = raw_sell.fillna(False)

# Assign to df columns (CRITICAL for backend execution)
df['buy'] = buy
df['sell'] = sell

# 3. Visual Formatting
# -----------------------
# Calculate marker positions
buy_marks = [
    df['low'].iloc[i] * 0.995 if buy.iloc[i] else None 
    for i in range(len(df))
]

sell_marks = [
    df['high'].iloc[i] * 1.005 if sell.iloc[i] else None 
    for i in range(len(df))
]

# 4. Final Output
# -----------------------
output = {
  'name': 'Dual SMA Strategy',
  'plots': [
    {
        'name': 'SMA 10',
        'data': sma_short.fillna(0).tolist(),
        'color': '#1890ff',
        'overlay': True
    },
    {
        'name': 'SMA 30',
        'data': sma_long.fillna(0).tolist(),
        'color': '#faad14',
        'overlay': True
    }
  ],
  'signals': [
    {
        'type': 'buy',
        'text': 'B',
        'data': buy_marks,
        'color': '#00E676'
    },
    {
        'type': 'sell',
        'text': 'S',
        'data': sell_marks,
        'color': '#FF5252'
    }
  ]
}

5. Best Practices & Troubleshooting

5.1 Handling NaNs

Rolling calculations (like rolling(14)) produce NaN values at the beginning of the data.

  • Rule: Always handle NaNs before generating signals.
  • Fix: Use .fillna(0) or .fillna(False) depending on context.

5.2 Look-ahead Bias

The system executes trades based on the signal generated at the close of a bar.

  • The backtester typically executes the order at the Open of the Next Bar.
  • Your signal logic should rely on close (current completed bar) or shift(1) (previous bar). Do not use shift(-1).

5.3 Performance

Avoid iterating over the DataFrame rows (for i in range(len(df)): ...) for calculation logic. It is slow.

  • Bad: Loop to calculate SMA.
  • Good: df['close'].rolling(...).
  • Exception: Constructing the buy_marks/sell_marks list usually requires a list comprehension, which is acceptable for visual output.

5.4 Debugging

Since you cannot see print() output easily in some execution modes, check the backend logs (backend_api_python/logs/app.log) if your strategy fails to load.

  • Common error: KeyError (wrong column name).
  • Common error: ValueError (arrays must be same length). Ensure plots data matches df length.