Strategy Development Guide
Build deterministic trading strategies with the Podium Strategy SDK. Strategies extend the Strategy base class and implement universe() and signal() to return target portfolio weights.
Strategy Lifecycle
Every strategy extends Strategy and implements up to 4 methods. The engine calls these in order on each trading day.
initialize(ctx) — Optional
Called once on the first tick. Use for setting parameters, loading state, or pre-computing values that don't change daily.
universe(ctx) → list[str] — Required
Called before each signal(). Return the list of symbols your strategy considers. Dynamic — can change daily based on data availability or screening criteria.
signal(ctx) → dict[str, float] — Required
Return target weights as a dict mapping symbol to weight. Positive weights = long, negative weights = short. The engine derives orders from weight changes (delay-1: signal at T close, fill at T+1 open).
risk_limits() → dict — Optional
Override default risk limits. See Risk Limits docs for all available parameters and what happens on breach.
Complete Example — Momentum Ranking
from podium_sdk import Strategy, StrategyContext
class MomentumRanking(Strategy):
"""6-month momentum, top 20 by trailing return, equal weight."""
TOP_N = 20
LOOKBACK_DAYS = 126
def initialize(self, ctx: StrategyContext) -> None:
# Called once on first tick. Use for loading parameters.
pass
def universe(self, ctx: StrategyContext) -> list[str]:
# Return the list of symbols to consider.
returns = ctx.data.returns(lookback=self.LOOKBACK_DAYS + 5)
if returns.empty:
return []
return list(returns.columns)
def signal(self, ctx: StrategyContext) -> dict[str, float]:
# Return target weights {symbol: weight}.
returns = ctx.data.returns(lookback=self.LOOKBACK_DAYS)
if returns.empty:
return {}
cum_return = (1 + returns).prod() - 1
ranked = cum_return.sort_values(ascending=False)
top_n = ranked.head(self.TOP_N)
if len(top_n) == 0:
return {}
weight = 1.0 / len(top_n)
return {sym: round(weight, 6) for sym in top_n.index}
def risk_limits(self) -> dict:
return {
"max_position_pct": 0.10,
"max_sector_pct": 0.35,
"max_drawdown_pct": 0.20,
"min_positions": 5,
}StrategyContext API
The ctx object is passed to every lifecycle method. It provides access to the current date, portfolio state, market data, and configuration.
# StrategyContext — available in every method:
ctx.date # Current trading date (datetime)
ctx.portfolio # PortfolioState (positions, cash, equity, drawdown)
ctx.data # DataAccessor (returns, prices, volume, fundamentals)
ctx.config # Strategy configuration dict
ctx.security_master # Security master lookup (sector, market cap)
# DataAccessor methods:
ctx.data.returns(lookback=126) # pd.DataFrame of daily returns
ctx.data.prices(lookback=126) # pd.DataFrame of close prices
ctx.data.volume(lookback=126) # pd.DataFrame of daily volume
ctx.data.fundamentals() # Dict of PE, ROE, market cap per symbol
ctx.data.sector() # Dict of symbol -> sector
# PortfolioState fields:
ctx.portfolio.positions # Dict of symbol -> {weight, quantity, value}
ctx.portfolio.cash # Available cash
ctx.portfolio.equity # Total portfolio value
ctx.portfolio.drawdown # Current drawdown from peakLong-Short Strategy Example
Use negative weights in signal() to short stocks. The engine handles borrow costs (1.5% annualized) and separate book accounting automatically.
from podium_sdk import Strategy, StrategyContext
class LongShortMomentum(Strategy):
"""130/30 momentum: long top 20, short bottom 10."""
def universe(self, ctx: StrategyContext) -> list[str]:
returns = ctx.data.returns(lookback=130)
return list(returns.columns)
def signal(self, ctx: StrategyContext) -> dict[str, float]:
returns = ctx.data.returns(lookback=126)
if returns.empty:
return {}
cum_return = (1 + returns).prod() - 1
ranked = cum_return.sort_values(ascending=False)
# Long top 20 at ~6.5% each = 130% gross long
longs = ranked.head(20)
# Short bottom 10 at ~3% each = 30% gross short
shorts = ranked.tail(10)
weights = {}
for sym in longs.index:
weights[sym] = 0.065 # Positive = long
for sym in shorts.index:
weights[sym] = -0.03 # Negative = short
return weights
def risk_limits(self) -> dict:
return {
"max_position_pct": 0.10,
"max_gross_exposure": 1.7,
"max_net_exposure": 1.2,
"max_short_position_pct": 0.05,
"max_drawdown_pct": 0.25,
}See Long-Short Strategies for detailed documentation on exposure, borrow costs, and archetypes.
Execution Model
Backtest Execution
- Engine calls universe() → gets list of symbols
- Engine calls signal() → gets target weights
- Guardrails enforce risk limits (scale down or reject)
- Orders derived from weight changes
- Fill at T+1 open price (delay-1 model)
- Portfolio marked to market at T+1 close
- Metrics computed (Sharpe, Sortino, drawdown, etc.)
Paper Trading Execution
- Daily cron triggers after market close (9:33 PM ET)
- Strategy code uploaded to Azure sandbox
- StrategyContext serialized (portfolio, OHLCV, fundamentals)
- signal() executed in sandbox (~500ms)
- Guardrails applied to target weights
- Orders filled at estimated close price
- T+1 reconciliation adjusts to actual open price
Available Python Packages
Strategies execute in a sandboxed Python environment with these pre-installed packages:
Data Science
- numpy
- pandas
- scipy
- statsmodels
Machine Learning
- scikit-learn
- xgboost
- lightgbm
Technical Analysis
- ta
- pandas-ta
Next Steps
- Run your first backtest — step-by-step tutorial
- Configure risk limits — every guardrail explained
- Long-short strategies — exposure, borrow costs, archetypes
- Leaderboard scoring — how to climb the rankings