Quant Buffet API
Syntax cookbook
The pandas and numpy syntax you actually need: panel access, indicators, cross-sectional ranking, weights, and cadence.
Almost every Quant Buffet strategy is assembled from the same two dozen expressions. This page is the reference for them — copy a block, rename the variables, and you have a signal.
Reaching into the price panel
python
# Always intersect with the columns that actually loaded
cols = [c for c in ASSETS if c in prices.columns]
close = prices[cols]
close.index # DatetimeIndex, tz-naive, sorted ascending
close.columns # loaded tickers, in ASSETS order
close.shape # (n_days, n_symbols)
prices.attrs.get("load_errors") # list[str] when some symbols failed
close.loc[dt] # Series: one row, indexed by symbol
close["SPY"] # Series: one column, indexed by date
close.at[dt, "SPY"] # scalar — the fastest single-cell access
close.iloc[-1] # last row (avoid inside on_day: it hides the date)| Accessor | Returns | Use inside on_day? |
|---|---|---|
prices.at[dt, sym] | scalar float | Yes — fastest option |
prices.loc[dt] | Series indexed by symbol | Yes — one row per day |
prices[sym] | Series indexed by date | Build it in make_on_day |
prices.loc[:dt] | growing DataFrame slice | Avoid — this is what causes timeouts |
prices.iloc[-1] | last row of the whole panel | No — ignores dt, leaks the future |
Indicator vocabulary
python
close = prices[cols]
# --- Trend ---------------------------------------------------------------
sma_200 = close.rolling(200, min_periods=200).mean()
ema_50 = close.ewm(span=50, adjust=False).mean()
above = close > sma_200 # boolean DataFrame
# --- Returns and momentum ----------------------------------------------
daily = close.pct_change() # 1-day simple returns
mom_12m = close.pct_change(252) # trailing 12-month return
mom_6m = close.pct_change(126) # trailing 6-month return
log_ret = np.log(close).diff() # log returns
# --- Volatility and risk ------------------------------------------------
vol_20 = daily.rolling(20).std() * np.sqrt(252) # annualised
downside = daily.clip(upper=0).rolling(60).std()
# --- Mean reversion -----------------------------------------------------
mu = close.rolling(20, min_periods=20).mean()
sd = close.rolling(20, min_periods=20).std(ddof=0)
zscore = (close - mu) / sd.replace(0, np.nan) # guard divide-by-zero
# --- Range and drawdown -------------------------------------------------
high_252 = close.rolling(252).max()
dd_from_high = close / high_252 - 1
rsi_up = daily.clip(lower=0).rolling(14).mean()
rsi_dn = (-daily.clip(upper=0)).rolling(14).mean()
rsi = 100 - 100 / (1 + rsi_up / rsi_dn.replace(0, np.nan))| Expression | Meaning | Warmup rows |
|---|---|---|
close.pct_change() | 1-day simple return | 1 |
close.pct_change(252) | trailing 12-month return | 252 |
close.rolling(n, min_periods=n).mean() | simple moving average | n |
close.ewm(span=n, adjust=False).mean() | exponential moving average | ≈ n |
daily.rolling(n).std() * np.sqrt(252) | annualised volatility | n + 1 |
close.rolling(n).max() | n-day high (breakout logic) | n |
(close - mu) / sd | z-score, mean reversion | window of mu |
Cross-sectional work (axis=1)
Momentum rotation, relative strength, and top-N selection all compare symbols against each other on the same date, which in pandas means operating along axis=1.
python
# Rank across symbols on each date: axis=1 is the cross-section
mom = close.pct_change(126)
ranks = mom.rank(axis=1, ascending=False) # 1 = strongest that day
top3 = ranks <= 3 # boolean mask
# Cross-sectional z-score (relative strength, market-neutral in spirit)
row_mu = mom.mean(axis=1)
row_sd = mom.std(axis=1).replace(0, np.nan)
rel = mom.sub(row_mu, axis=0).div(row_sd, axis=0)
# Inside on_day: pick names from a single row
def pick_top(dt, n=3):
row = mom.loc[dt].dropna()
if row.empty:
return []
ranked = row.sort_values(ascending=False)
return [s for s in ranked.head(n).index if ranked[s] > 0]Turning signals into weights
python
# Equal weight over the selected names
def equal_weight(picks: list[str]) -> dict[str, float]:
if not picks:
return {} # {} means go to cash
w = 1.0 / len(picks)
return {s: w for s in picks}
# Inverse-volatility weights (risk parity flavour)
def inverse_vol(dt, picks, vol) -> dict[str, float]:
inv = {s: 1.0 / float(vol.at[dt, s]) for s in picks
if pd.notna(vol.at[dt, s]) and vol.at[dt, s] > 1e-8}
total = sum(inv.values())
return {s: v / total for s, v in inv.items()} if total > 0 else {}
# Volatility targeting: scale exposure, leave the remainder in cash
def vol_scaled(dt, symbol, vol, target=0.10) -> dict[str, float]:
v = float(vol.at[dt, symbol])
return {symbol: min(1.0, target / v)} if v > 1e-8 else {}
# Fixed sleeves with a deliberate cash buffer
CORE = {"SPY": 0.45, "TLT": 0.25, "GLD": 0.15} # 85% invested, 15% cash| Weights returned | Engine behaviour |
|---|---|
{"SPY": 1.0} | 100% SPY |
{"SPY": 0.6, "TLT": 0.4} | Fully invested 60/40 |
{"SPY": 0.5, "TLT": 0.2} | 70% invested, 30% cash |
{"SPY": 0.8, "TLT": 0.8} | Normalised to 50/50 — leverage is not possible |
{"SPY": -0.4} | Clamped to 0.0 — shorting is not supported |
{"NVDA": 1.0} | Ignored — symbol is not a panel column |
{} | Liquidate to 100% cash |
Boolean logic on frames
python
# Combine conditions with & | ~ — and always parenthesise
trend_up = close > sma_200
cheap = zscore < -1.0
signal = trend_up & cheap # elementwise AND
risk_off = ~trend_up["SPY"] # elementwise NOT
# Row-level reductions (these are pandas methods, not the missing builtins)
sma_200.loc[dt].isna().all() # nothing ready yet
trend_up.loc[dt].any() # at least one asset in an uptrend
int(trend_up.loc[dt].sum()) # how many are in an uptrend
# Shift to enforce "decide today, act on the next bar"
delayed = signal.shift(1)Handling NaN properly
| Expression | Purpose |
|---|---|
pd.notna(x) / pd.isna(x) | Scalar-safe NaN test — prefer over x == x |
series.dropna() | Drop missing symbols before ranking |
frame.dropna(how="all") | Drop dates where nothing is ready |
sd.replace(0, np.nan) | Avoid divide-by-zero in z-scores |
series.reindex(prices.index).ffill() | Align a resampled series back to trading days |
frame.clip(lower=0) | Zero out negatives, e.g. for RSI or long-only scores |
Dates and calendars
python
dt.year, dt.month, dt.day # ints
dt.dayofweek # Monday = 0 … Sunday = 6
dt.isocalendar()[:2] # (ISO year, ISO week) — weekly gating
dt.strftime("%Y-%m-%d") # string form used in Trade.date
dt in prices.index # membership test before .at lookups
# Month-end resampling, then aligned back to daily trading dates
monthly = close.resample("ME").last()
monthly_mom = monthly.pct_change(12).reindex(close.index).ffill()