Strategies¶
fundcloud.strategies exposes the abstract BaseStrategy together with two preset implementations (Hold, DCA), a per-bar Context object, and the Cadence / Scheduler primitives that govern when a strategy fires. The @register_strategy decorator participates in Catalog serialisation so a strategy name round-trips through YAML configs. For usage patterns and the custom-strategy worked example, see the DCA & Hold guide.
fundcloud.strategies
¶
Decision-logic presets and extension points.
Two built-in presets (:class:Hold and :class:DCA) cover the common
retail / allocation workflows. Custom strategies subclass
:class:BaseStrategy and may optionally register themselves via
:func:register_strategy so they become discoverable by name.
BaseStrategy
¶
Bases: ABC
ABC for every concrete strategy.
The simulator drives subclasses through a three-stage lifecycle:
:meth:init once before the first bar, :meth:decide once per
bar, :meth:close once after the last bar. Only :meth:decide is
required.
Examples:
Minimal momentum strategy that buys 1 share when the 10-day return is positive and sells when it's negative:
>>> import pandas as pd
>>> from fundcloud.strategies import BaseStrategy, Context
>>> from fundcloud.sim import Order
>>>
>>> class Momentum(BaseStrategy):
... def __init__(self, lookback: int = 10) -> None:
... self.lookback = lookback
...
... def decide(self, ctx: Context) -> list[Order]:
... if len(ctx.history) < self.lookback:
... return []
... orders: list[Order] = []
... close = ctx.history.xs("close", axis=1, level=0)
... ret = close.iloc[-1] / close.iloc[-self.lookback] - 1.0
... for asset, r in ret.items():
... side = "buy" if r > 0 else "sell"
... orders.append(Order(ts=ctx.ts, asset=str(asset), side=side, qty=1.0))
... return orders
close
¶
End-of-run hook.
Default: no-op. Override to compute summary stats, persist diagnostics, or release external resources. Not abstract because most strategies don't need it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
portfolio
|
Portfolio
|
Final :class: |
required |
Source code in python/fundcloud/strategies/base.py
decide
abstractmethod
¶
Return the orders this strategy wants executed.
Called once per bar. The simulator queues the returned orders
for execution under its :class:~fundcloud.sim.ExecutionModel
(default :class:~fundcloud.sim.NextBarOpen, so orders emitted
on bar t fill at the open of bar t + 1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx
|
Context
|
Per-bar :class: |
required |
Returns:
| Type | Description |
|---|---|
list of Order
|
Zero or more :class: |
Source code in python/fundcloud/strategies/base.py
init
¶
One-shot setup called before the first :meth:decide.
Default: no-op. Override to compute a warm-up window, schedule
cadences, or stash any state derived from the full bars
frame. Not abstract because most strategies don't need it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bars
|
DataFrame
|
The full Bars frame the simulator will iterate over —
|
required |
portfolio
|
Portfolio
|
The live :class: |
required |
Source code in python/fundcloud/strategies/base.py
Context
dataclass
¶
Context(
ts: Timestamp,
bar: Series,
history: DataFrame,
portfolio: Portfolio,
assets: tuple[str, ...],
extras: Mapping[str, Any] = dict(),
)
Per-bar context handed to :meth:BaseStrategy.decide.
Strategies are stateless on the bar boundary (they may carry their own state across bars, but the simulator hands them everything they need to decide on this bar via this object).
Attributes:
| Name | Type | Description |
|---|---|---|
ts |
Timestamp
|
Current bar timestamp. |
bar |
Series
|
Current bar. For a Bars MultiIndex frame this is a
:class: |
history |
DataFrame
|
Bars up to and including |
portfolio |
Portfolio
|
Live :class: |
assets |
tuple[str, ...]
|
Convenience: asset universe ordered by column appearance. |
extras |
Mapping[str, Any]
|
Optional dict for user-specified scheduled events, factor scores, etc. Populated by the simulator when configured, else an empty dict. |
Hold
¶
Hold(
weights: WeightsLike | None = None,
*,
rebalance: RebalanceSpec | None = None,
start: Timestamp | str | None = None,
)
Bases: BaseStrategy
Allocate to target weights once, hold; optionally rebalance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
WeightsLike | None
|
Target allocation. May be a When omitted ( |
None
|
rebalance
|
RebalanceSpec | None
|
If supplied, restore target weights at each cadence boundary.
See :class: |
None
|
start
|
Timestamp | str | None
|
Optional lock-out: don't place the first allocation before
|
None
|
Examples:
Buy-and-hold 60/40 equity / bonds, no rebalancing (weights drift with prices):
>>> from fundcloud.strategies import Hold
>>> Hold({"SPY": 0.6, "AGG": 0.4})
<fundcloud.strategies.hold.Hold object at ...>
Equal-weight default — distribute evenly over whatever assets are in the bars frame:
Quarterly-rebalanced 60/40 with a 5 %-drift tolerance:
>>> from fundcloud.strategies import RebalanceSpec
>>> Hold(
... {"SPY": 0.6, "AGG": 0.4},
... rebalance=RebalanceSpec(horizon="91D", tolerance=0.05),
... )
<fundcloud.strategies.hold.Hold object at ...>
Source code in python/fundcloud/strategies/hold.py
RebalanceSpec
dataclass
¶
Optional rebalance policy for :class:Hold.
DCA
¶
DCA(
amount: float | Mapping[str, float] | None = None,
*,
amount_pct: float | Mapping[str, float] | None = None,
horizon: HorizonName | Cadence | str = "monthly",
weights: Mapping[str, float] | None = None,
start: Timestamp | str | None = None,
end: Timestamp | str | None = None,
sell_on_end: bool = False,
)
Bases: BaseStrategy
Invest a fixed amount at a fixed cadence.
Exactly one of amount or amount_pct must be provided —
they're two ways to spell the same thing:
amount— explicit dollars per fire (scalar or per-asset).amount_pct— fraction of the current portfolio equity deployed at each fire (scalar or per-asset). The dollar size is recomputed at every fire fromPortfolio.equity_curve, so the deposit grows or shrinks with the portfolio. Each fire is also clipped to currently-available cash — DCA never borrows; once cash is exhausted, subsequent fires emit no orders. On the very first fire (no equity history yet) it falls back to starting cash.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amount
|
float | Mapping[str, float] | None
|
Dollars per fire. Either a scalar (distributed across
|
None
|
amount_pct
|
float | Mapping[str, float] | None
|
Equity fraction per fire. Either a scalar in |
None
|
horizon
|
HorizonName | Cadence | str
|
Cadence — |
'monthly'
|
weights
|
Mapping[str, float] | None
|
Optional. When omitted with a scalar |
None
|
start
|
Timestamp | str | None
|
Optional window inside which DCA fires. |
None
|
end
|
Timestamp | str | None
|
Optional window inside which DCA fires. |
None
|
sell_on_end
|
bool
|
When |
False
|
Examples:
Single-asset weekly DCA into SPY — the classic retail deposit:
>>> from fundcloud.strategies import DCA
>>> DCA(amount=500.0, horizon="weekly", weights={"SPY": 1.0})
<fundcloud.strategies.dca.DCA object at ...>
Multi-asset monthly allocation with explicit dollar buckets per leg:
>>> DCA({"SPY": 300.0, "AGG": 200.0}, horizon="monthly")
<fundcloud.strategies.dca.DCA object at ...>
Scalar amount with no weights — equal-weight over whatever assets the bars frame contains:
Percentage of current equity instead of fixed dollars — deploy 1 % of the portfolio each month, scaling automatically as equity grows:
Source code in python/fundcloud/strategies/dca.py
Scheduler
¶
Factory for :class:Cadence presets.
from_horizon
staticmethod
¶
from_horizon(
horizon: HorizonName | Cadence | str,
*,
anchor: Timestamp | None = None,
) -> Cadence
Resolve a user-facing horizon into a concrete :class:Cadence.
Accepted forms:
"daily"→ every trading day (1Dstep anchored atanchor)."weekly"→ every 7 calendar days fromanchor(not ISO weekday 1 — matches PRD's "(7 days)" wording)."monthly"→ same day-of-month asanchoreach month, falling back to the last trading day of the month if missing."quarterly"→ same rule as monthly, stepping every 3 months.- a :class:
Cadenceis returned as-is (withanchormerged in). - any pandas offset string (e.g.
"30D","3W") → :class:Cadence.
Source code in python/fundcloud/strategies/scheduler.py
Cadence
dataclass
¶
An explicit cadence step. Use :meth:Scheduler.from_horizon for presets.
step is any pandas-parseable offset like "7D", "14D", or
"1D". Anchor is the first timestamp the cadence fires on; if
unspecified the first trigger falls on the first timestamp in the passed
index.
triggers
¶
triggers(
index: DatetimeIndex,
*,
start: Timestamp | str | None = None,
end: Timestamp | str | None = None,
) -> pd.DatetimeIndex
Return the subset of index that the cadence fires on.
Source code in python/fundcloud/strategies/scheduler.py
register_strategy
¶
Decorator: make a strategy class discoverable by name.
Registered strategies can be looked up via
:func:registered_strategies — useful for config-driven runners
that pick the strategy class from a string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
String key the class will be stored under. Typically a short snake-case identifier. |
required |
Returns:
| Type | Description |
|---|---|
callable
|
The decorator that registers and returns the class unchanged. |
Examples:
>>> from fundcloud.strategies import BaseStrategy, register_strategy, registered_strategies
>>> @register_strategy("noop")
... class Noop(BaseStrategy):
... def decide(self, ctx):
... return []
>>> "noop" in registered_strategies()
True
Source code in python/fundcloud/strategies/base.py
registered_strategies
¶
Return a snapshot of the strategy registry.
Returns:
| Type | Description |
|---|---|
dict[str, type[BaseStrategy]]
|
Copy of the |