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:
- Sizing — Kelly fraction, vol-targeting, or fixed-weight
- Session matrix — different sizes for RTH / pre / post / 24h sessions
- Capacity — per-symbol position limits based on ADV
- 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
| Version | Items |
|---|---|
| 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
mts1b-quantkit— HRP, BL, Ledoit-Wolf (these are the math; portfolio is the wrapper)mts1b-research— primary consumer- Concept: Risk envelopes — how riskengine uses capacity