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)
AccessorReturnsUse inside on_day?
prices.at[dt, sym]scalar floatYes — fastest option
prices.loc[dt]Series indexed by symbolYes — one row per day
prices[sym]Series indexed by dateBuild it in make_on_day
prices.loc[:dt]growing DataFrame sliceAvoid — this is what causes timeouts
prices.iloc[-1]last row of the whole panelNo — 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))
ExpressionMeaningWarmup rows
close.pct_change()1-day simple return1
close.pct_change(252)trailing 12-month return252
close.rolling(n, min_periods=n).mean()simple moving averagen
close.ewm(span=n, adjust=False).mean()exponential moving average≈ n
daily.rolling(n).std() * np.sqrt(252)annualised volatilityn + 1
close.rolling(n).max()n-day high (breakout logic)n
(close - mu) / sdz-score, mean reversionwindow 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 returnedEngine 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

ExpressionPurpose
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()