PORTFOLIO files¶
A PORTFOLIO file composes N strategy files into one deployable unit. Each child runs independently, gated by a RUN <child> rule at the portfolio level. The daemon fans out into one observability port + log file per child.
Use a portfolio when:
- You want regime-gated strategy switching — different strategies for different market conditions
- You want multi-asset / multi-timeframe strategies running concurrently as one unit
- You need per-strategy operator control while keeping deployment as one artifact
Shape¶
PORTFOLIO <name> VERSION <int>
[ SYMBOLS
<alias> = <BROKER>:<symbol> EVERY <timeframe>
[ ... ]
]
IMPORT '<path>' AS <alias> [ HOLD ]
[ ... more imports ... ]
[ REGIMES
NAME <regime-set-name>
STATE <name> WHEN <condition>
[ ... more states ... ]
STATE <name> DEFAULT
]
[ ALLOCATE
METHOD regime_weighted [ REBALANCE EVERY <duration> ]
<state-name> -> <alias> <weight>[, <alias> <weight> ...]
[ ... more states ... ]
]
RULES
[ WHEN <condition> ] RUN <alias> [ WEIGHT <fraction> ] [ OVERRIDE { <key> = <literal>, ... } ]
[ ... more RUN rules ... ]
The shape is similar to STRATEGY, but with IMPORT declaring child strategies and RUN <alias> actions, and without DEFAULTS, PARAM or LET blocks. REGIMES + ALLOCATE add adaptive capital allocation on top of the on/off gating; CAPITAL on the header is what WEIGHT and ALLOCATE divide.
Basic — regime-gated switching¶
PORTFOLIO btc_regimes VERSION 1
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h
IMPORT 'trend.qkt' AS trend
IMPORT 'meanrev.qkt' AS meanrev
IMPORT 'breakout.qkt' AS breakout
RULES
WHEN adx(btc, 14) > 30 RUN trend -- strong trend
WHEN adx(btc, 14) < 20 RUN meanrev -- ranging
WHEN adx(btc, 14) BETWEEN 20 AND 30 RUN breakout -- transitional
Only one child runs at a time. When ADX moves from ≥30 to <20, trend deactivates (positions closed unless HOLD) and meanrev activates.
Regime-weighted allocation¶
Instead of fully switching children on or off, you can assign each active child a regime-dependent weight. The weights scale the child's new risk-increasing orders, so the portfolio tilts capital toward the child that fits the current regime while keeping others alive at a smaller size.
PORTFOLIO btc_regime_weighted VERSION 1 CAPITAL 10000
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h
IMPORT 'trend.qkt' AS trend
IMPORT 'meanrev.qkt' AS meanrev
REGIMES
NAME market_regime
STATE trend WHEN adx(btc, 14) > 25
STATE range DEFAULT
ALLOCATE
METHOD regime_weighted
trend -> trend 0.8, meanrev 0.2
range -> trend 0.2, meanrev 0.8
RULES
RUN trend
RUN meanrev
REGIMESdefines named states. The firstWHENstate that matches wins;DEFAULTcatches everything else.ALLOCATE METHOD regime_weightedmaps each state to per-alias weights. Weights are fractions of normal risk (1.0 = full size, 0.0 = suppressed).cashis reserved and ignored as an alias; use it to park risk-off capital.- All imported aliases receive a weight in every state. Missing aliases default to
0.0. REBALANCE EVERY <duration>(optional) re-applies the current state's weights on that clock interval as well as on a state change.
The weights are evaluated on every closed candle and applied to new orders through the same book-risk seam used by drawdown de-risking, so backtest and live use identical scaling.
PORTFOLIO btc_rebalanced VERSION 1 CAPITAL 10000
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h
IMPORT 'trend.qkt' AS trend
IMPORT 'meanrev.qkt' AS meanrev
REGIMES
NAME market_regime
STATE trend WHEN adx(btc, 14) > 25
STATE range DEFAULT
ALLOCATE
METHOD regime_weighted REBALANCE EVERY 1d
trend -> trend 0.8, meanrev 0.2
range -> trend 0.2, cash 0.8
RULES
RUN trend
RUN meanrev
Fixed weights: RUN <alias> WEIGHT <fraction>¶
Without regimes, each RUN can carry a fixed share of CAPITAL instead. The rules are checked
when the file parses: CAPITAL must be on the header, every RUN carries a WEIGHT or none
does, each weight is in (0, 1], the weights sum to at most 1.0 (no implicit leverage), and
WEIGHT cannot be combined with an ALLOCATE block.
PORTFOLIO two_sleeves VERSION 1 CAPITAL 10000
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h
IMPORT 'trend.qkt' AS trend
IMPORT 'meanrev.qkt' AS meanrev
RULES
RUN trend WEIGHT 0.6
WHEN adx(btc, 14) < 20 RUN meanrev WEIGHT 0.4
PORTFOLIO no_capital VERSION 1
IMPORT 'trend.qkt' AS trend
RULES
RUN trend WEIGHT 0.6
-- error: CAPITAL is required on the header when any RUN carries WEIGHT
PORTFOLIO levered VERSION 1 CAPITAL 10000
IMPORT 'trend.qkt' AS trend
IMPORT 'meanrev.qkt' AS meanrev
RULES
RUN trend WEIGHT 0.6
RUN meanrev WEIGHT 0.6
-- error: total WEIGHT must sum to <= 1.0 (no implicit leverage), got 1.2
IMPORT syntax¶
- Path is relative to the portfolio file. Use forward slashes.
- Alias is how the portfolio's
RUNrules reference the child. HOLD(optional): keep the child's positions when it deactivates. Without HOLD, deactivation flattens.
IMPORT 'trend.qkt' AS trend -- closes positions on deactivate
IMPORT 'longterm.qkt' AS longterm HOLD -- keeps positions when deactivated
HOLD mode is useful for long-horizon children whose entries take days to play out. You don't want a regime flip to flush a 3-day position.
RUN <alias> OVERRIDE { ... } — per-child param tuning¶
Retune a child's PARAM values for this portfolio without editing the child file:
- Each
key = valuemaps to aPARAMdeclared in the child strategy. An unknown key or a type mismatch (number → string) is a compile-time error. - The result is identical to hand-editing the child's
PARAM riskPct = 0.008line — the engine sees no difference. - Two
RUNrules for the same alias with differentOVERRIDEvalues are a compile-time error (use the same value or consolidate into one rule). OVERRIDEis optional.RUN awith no override uses the child's declared defaults.
RUN <alias> action¶
The portfolio's only action verb. Activates the named child.
- The child becomes "active" — it processes ticks, evaluates its own rules, emits signals.
- An "active" child whose condition transitions to false becomes "inactive" — pauses processing, flattens (or holds) positions.
- Multiple children can be active simultaneously if multiple conditions hold.
Always-on (no gating)¶
Activates someChild always — used for the "always running" parts of a portfolio.
Mutually exclusive (regime gates)¶
Above example with ADX. Make sure your conditions are mutually exclusive (BETWEEN/<>) so exactly one child is active at any time.
Concurrent (multi-asset)¶
Three children running in parallel, each trading its own asset. The portfolio is just a deployment wrapper here — no regime logic.
Operator control¶
Children expose their own ports + logs. The daemon's qkt list shows them indented under the portfolio:
NAME KIND PORT TRADES STATE
btc-regimes portfolio 47291 - running
trend child 47292 8 active
meanrev child 47293 23 inactive
breakout child 47294 5 inactive
Operators can override gating:
qkt start btc-regimes/meanrev -- force meanrev active regardless of regime
qkt stop btc-regimes/trend -- force trend inactive
Manual overrides clear when you qkt resume <portfolio>.
Shared symbol declarations¶
Portfolio-level SYMBOLS is inherited by every child that doesn't redeclare. Useful when all children trade the same underlying:
PORTFOLIO multi_strat VERSION 1
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h -- all children see this
IMPORT 'a.qkt' AS a
IMPORT 'b.qkt' AS b
-- if a.qkt references 'btc', it picks up the portfolio's declaration
-- if a.qkt has its own SYMBOLS block, that wins
The portfolio's symbols are also the only ones whose data the portfolio rules can read (for regime detection).
Risk and the portfolio¶
Daemon-level risk rules apply to the whole portfolio, not per-child. If one child triggers max-daily-loss, the entire daemon halts — every other strategy too.
To express per-child risk limits, use qkt.config.yaml's risk.per_strategy block:
risk:
per_strategy:
btc_regimes:trend:
max_trades_per_day: 5
cooldown_after_loss: 1h
cooldown_after_loss_after_consecutive: 1
loss_streak_halt: 3
loss_streak_halt_scope: persistent
These apply independently to each strategy hosted in the daemon — including portfolio children.
Conditions in portfolios¶
PORTFOLIO files have no LET block (and no DEFAULTS or PARAM): the sections are SYMBOLS, IMPORT, REGIMES, ALLOCATE and RULES, in that order. Write the gating expression directly in the RUN condition, or name the market state with REGIMES:
PORTFOLIO mybook VERSION 1
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h
IMPORT 'trend.qkt' AS trend
IMPORT 'meanrev.qkt' AS meanrev
RULES
WHEN adx(btc, 14) > 30 RUN trend
WHEN adx(btc, 14) < 20 RUN meanrev
PORTFOLIO with_let VERSION 1
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1h
IMPORT 'trend.qkt' AS trend
LET trending = adx(btc, 14) > 30
RULES
WHEN trending RUN trend
-- parse error: unexpected 'LET' after the last recognized block
What children inherit¶
| Inherited | Notes |
|---|---|
SYMBOLS declarations |
Children can override |
| Daemon-level risk rules | Apply across all children |
Broker configurations from qkt.config.yaml |
Children resolve broker prefixes the same way |
--param overrides |
Apply to all children unless qualified |
| Not inherited | |
|---|---|
DEFAULTS, PARAM, LET |
A portfolio has none of these blocks; children declare their own, and OVERRIDE is how a portfolio reaches a child's PARAM |
Common gotchas¶
- Non-mutually-exclusive regime gates leave gaps. If your rules cover only
> 30and< 20, the range[20, 30]has no child active. UseBETWEEN ... AND ...orELSEpatterns to cover everything. - Children with their own
SYMBOLSblocks override. A child that redeclaresbtcwith a different timeframe than the portfolio works fine, but it costs an extra aggregator. HOLDdoesn't mean "keep entering." A held child has its positions preserved but stops generating new signals when inactive. It only manages existing positions.- Cascade stop.
qkt stop <portfolio>cascades to every child. Useqkt stop <portfolio>/<child>to stop a specific child. - Portfolio file is not a strategy. It shares
VERSION,SYMBOLSandRULESwith one, but its rules useRUNinstead ofBUY/SELL, and it has noDEFAULTS,PARAMorLETblock.
Backtesting portfolios¶
qkt backtest <portfolio.qkt> runs the children as N attributed strategies on one engine, sharing one account and the book-risk layer. The same backtest tiers that work for single strategies also work for portfolios:
--barsreplays built bars instead of ticks. Per symbol it picks the coarsest built timeframe that divides the declared one and covers the whole window; a partially built coarser folder (say4hfor two months) is skipped, with a note on stderr, in favour of a finer complete one. When no folder covers the window the coarsest one's gaps are reported.--bar-tf <tf>pins the bar feed to a specific built timeframe (must divide every child's declared timeframe).--tick-fillsdrives signals from bars but resolves fills on real ticks.--report-dir <dir>writes the standard report bundle (result.json,trades.csv, per-strategy equity curves, etc.) in the same format as a single-strategy backtest.
Regime-weighted allocation and WHEN..RUN gating use the same PortfolioGate + BookRiskController code in backtest and live, so results are directly comparable. A child imported without HOLD is flattened on deactivation in backtest exactly as the live supervisor would, while a HOLD child keeps its positions and continues to manage exits.
What this composes with¶
- STRATEGY block — child files are regular strategies
- Conditions —
RUNrules use the same condition grammar - Indicators — regime detectors are usually indicator-based
- Portfolio example — full deployment with three regimes
- Regime-adaptive portfolio —
REGIMES+ALLOCATE METHOD regime_weighted - Phase 13b, Phase 14 — design notes on portfolio fan-out