Skip to main content

mts1b-portfolio

Portfolio construction: position sizing (canonical Kelly + VolTarget), session matrix, capacity buckets.

Repo: github.com/MTS1B/mts1b-portfolio Layer: 3 Depends on: foundation, platform, quantkit, numpy, scipy Audience: mts1b-research, mts1b-oms, mts1b-treasury

What it is

Converts a signal (target weights from a factor + universe) into orders (target positions per fund), applying:

  1. Sizing — Kelly fraction, vol-targeting, or fixed-weight
  2. Session matrix — different sizes for RTH / pre / post / 24h sessions
  3. Capacity — per-symbol position limits based on ADV
  4. Holding buckets — bucket by holding period (intraday / daily / weekly / monthly)

Sizing methods

equal_weight_ls

from mts1b_portfolio.sizers import equal_weight_ls

weights = equal_weight_ls(
signal=z_scores, # (A,) cross-sectional ranking
n_long=5,
n_short=5,
gross=1.0, # total gross exposure
)
# {AAPL: 0.10, MSFT: 0.10, ..., XYZ: -0.10}

Pick top-N long, bottom-N short, equal-weight within each side.

kelly_fraction

from mts1b_portfolio.sizers import kelly_fraction

w = kelly_fraction(
expected_returns=mu, # (A,)
cov=sigma, # (A, A)
risk_aversion=2.0,
cap=0.20, # cap any single position at 20% NAV
)

Full Kelly is w = (cov^-1 @ mu) / risk_aversion. The cap prevents the single-asset concentration that pure Kelly can recommend.

API source: services/research/src/gpuBT1060/portfolio/long_short.py:_apply_kelly.

vol_target_weights

from mts1b_portfolio.sizers import vol_target_weights

w = vol_target_weights(
base_weights=w_signal, # (A,) from signal
asset_log_returns=lr, # (T, A) historical log returns
target_ann_vol=0.12, # target 12% annualized portfolio vol
window=63, # 3-month vol estimate
max_scale=3.0, # don't scale up more than 3x
)

Scales portfolio gross exposure so realized annualized vol matches the target. Conservative — never scales below 1.0 if vol is below target (you'd just leave dry powder).

API source: services/research/src/gpuBT1060/portfolio/long_short.py:_apply_vol_target.

kelly_voltarget_12 (composite, default for v1 demo)

from mts1b_portfolio.sizers import kelly_voltarget_12

w = kelly_voltarget_12(
signal=z,
returns=hist_returns,
nav_usd=fund_nav,
)

= kelly fraction (capped at 20%) + vol target (12% annualized).

Session matrix

Different sizing per session:

from mts1b_portfolio.session import SessionMatrix

matrix = SessionMatrix({
"RTH": {"sizer": "kelly_voltarget_12", "gross_cap": 1.0},
"pre": {"sizer": "kelly_voltarget_8", "gross_cap": 0.3},
"post": {"sizer": "kelly_voltarget_8", "gross_cap": 0.3},
"IBEOS": {"sizer": "equal_weight_ls", "gross_cap": 0.1},
"24h": {"sizer": "kelly_voltarget_12", "gross_cap": 1.0},
})

# At any point, given a fund + symbol + current time:
sizer = matrix.resolve(asset_class="equities", asof=datetime.utcnow())

For equities, MTS1B distinguishes 5 sessions (pre 4-9:30 ET, RTH 9:30-16, post 16-20, IBEOS 20-3:50, closed). For crypto/fx, 24h. For futures, ~24×5 minus daily maintenance.

Capacity

from mts1b_portfolio.capacity import position_capacity

cap = position_capacity(
symbol="AAPL",
nav_usd=100_000,
fraction_of_adv=0.01, # take at most 1% of 21-day ADV
adv_lookback_days=21,
)
# Decimal('5234.50') ← USD cap per position

If your target position exceeds the cap, it's truncated. Used by mts1b-riskengine as a hard gate.

Holding buckets

Holding-period bucketing for performance attribution:

from mts1b_portfolio.holding import bucket

bucket("intraday") # exit within session
bucket("daily") # exit < 5 trading days
bucket("weekly") # 5-21 days
bucket("monthly") # 21-63 days
bucket("quarterly") # 63-252 days

mts1b-reportslibrary aggregates P/L by bucket so you can see which holding period earns its keep.

Multi-sleeve

mts1b-treasury may run multiple strategies (sleeves) per fund. mts1b-portfolio composes sleeve-level signals into fund-level targets:

from mts1b_portfolio.multi_sleeve import compose

target_weights = compose(
sleeves={
"momentum": {"weights": {...}, "allocation": 0.4},
"carry": {"weights": {...}, "allocation": 0.3},
"value": {"weights": {...}, "allocation": 0.3},
},
overlay="hrp", # HRP weighting on sleeve returns
)

The composition is itself a portfolio problem (HRP, BL, or equal-weight on sleeve allocations).

Build + test

pip install -e ".[dev]"
pytest -v

Property-based tests verify:

  • Weights sum to gross exposure (modulo rounding)
  • Position caps are never exceeded
  • Sizing is deterministic given the same inputs

Roadmap

VersionItems
0.1 (Wave 1)equal_weight_ls, kelly_fraction, vol_target_weights, session matrix, capacity
0.2 (Wave 2)More sizers: max-diversification, min-variance
0.3 (Wave 3)Multi-sleeve composition with HRP overlay
1.0 (LTS)Stable sizer interface

See also