Documentation
¶
Overview ¶
Package engine is the main entry point for pvbt, a backtesting engine library. It handles infrastructure -- data fetching, order management, fill simulation, and performance metrics -- so you can focus on strategy logic.
To write a new strategy you bring two things:
- A Go struct that implements the Strategy interface
- A main function that creates the engine and runs the backtest
Parameters are defined as exported struct fields with struct tags. No external configuration files are needed.
Strategy Interface ¶
The Strategy interface requires three methods:
- Name returns a short identifier for the strategy (e.g. "adm").
- Setup runs once after the engine populates strategy fields from their default struct tags. It sets the trading schedule, benchmark, risk-free asset, and performs any other one-time initialization.
- Compute runs at each scheduled step. It receives a context, the engine (for data fetching), the portfolio (current holdings, read-only), and the batch (for accumulating orders and annotations). The strategy computes signals, selects assets, and writes orders to the batch.
Example ¶
The following example implements Accelerating Dual Momentum (ADM). It computes 1-, 3-, and 6-month momentum on a set of risk-on assets, averages the scores, and invests in the highest-scoring asset if it is above zero. Otherwise it moves to a risk-off asset.
package main
import (
"context"
"time"
"github.com/penny-vault/pvbt/engine"
"github.com/penny-vault/pvbt/portfolio"
"github.com/penny-vault/pvbt/signal"
"github.com/penny-vault/pvbt/universe"
"github.com/rs/zerolog/log"
)
type ADM struct {
RiskOn universe.Universe `pvbt:"riskOn" desc:"ETFs to invest in" default:"VOO,SCZ"`
RiskOff universe.Universe `pvbt:"riskOff" desc:"Out-of-market asset" default:"TLT"`
}
func (s *ADM) Name() string { return "adm" }
func (s *ADM) Setup(_ *engine.Engine) {}
func (s *ADM) Describe() engine.StrategyDescription {
return engine.StrategyDescription{
Schedule: "@monthend",
Benchmark: "VFINX",
}
}
func (s *ADM) Compute(ctx context.Context, eng *engine.Engine, port portfolio.Portfolio, batch *portfolio.Batch) error {
mom1 := signal.Momentum(ctx, s.RiskOn, portfolio.Months(1))
mom3 := signal.Momentum(ctx, s.RiskOn, portfolio.Months(3))
mom6 := signal.Momentum(ctx, s.RiskOn, portfolio.Months(6))
// Average the three momentum scores across all risk-on assets.
momentum := mom1.Add(mom3).Add(mom6).DivScalar(3)
if err := momentum.Err(); err != nil {
log.Error().Err(err).Msg("signal computation failed")
return err
}
// Pick the risk-on asset with the highest positive momentum.
// If none are positive, fall back to the risk-off asset (TLT).
riskOffDF, err := s.RiskOff.At(ctx, data.MetricClose)
if err != nil {
log.Error().Err(err).Msg("risk-off data fetch failed")
return err
}
portfolio.MaxAboveZero(data.MetricClose, riskOffDF).Select(momentum)
// Build an equal-weight plan and rebalance into it.
plan, err := portfolio.EqualWeight(momentum)
if err != nil {
log.Error().Err(err).Msg("EqualWeight failed")
return err
}
batch.RebalanceTo(ctx, plan...)
return nil
}
func main() {
eng := engine.New(&ADM{},
engine.WithInitialDeposit(10_000),
engine.WithDataProvider(provider),
engine.WithAssetProvider(provider),
)
defer eng.Close()
ctx := context.Background()
start := time.Date(2005, time.January, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
p, err := eng.Backtest(ctx, start, end)
}
Execution Model ¶
A backtest proceeds in four phases.
Phase 1: Initialization. The engine loads the asset registry from the AssetProvider, then uses reflection to populate exported strategy fields from their default struct tags. It builds a routing table mapping each data metric to its provider. Then it calls Setup, where the strategy sets the schedule, benchmark, risk-free asset, and does any other one-time initialization. The engine creates a portfolio account from the initial deposit (or snapshot, or pre-configured account), attaches a simulated broker, and initializes the per-column data cache.
Phase 2: Date enumeration. The engine walks the tradecron schedule from start to end, collecting every trading date. For ADM with @monthend, that is roughly 240 dates from 2005 to 2024.
Phase 3: Step loop. At each date the engine:
- Fetches housekeeping data (close, adjusted close, dividends) for held assets, the benchmark, and the risk-free asset.
- Records dividend income for held positions.
- Evaluates pending bracket/OCO orders against the current bar's high and low prices. Stop-loss orders trigger when the low breaches the stop price; take-profit orders trigger when the high reaches the target. If both could trigger on the same bar, the stop loss wins (pessimistic assumption). Triggered fills are queued for processing in the next step.
- Updates the simulated broker with the current price provider and date so orders can fill.
- Calls Compute. The strategy fetches data, computes signals, and tells the portfolio to rebalance.
- Fetches post-Compute prices for all held assets (including newly acquired positions) and updates the equity curve.
- Computes all registered performance metrics across standard windows (5yr, 3yr, 1yr, YTD, MTD, WTD, and since-inception).
- Evicts stale cache entries.
Phase 4: Return. After the final step, the portfolio contains the full transaction log and can compute performance metrics. It provides access to the equity curve, every trade via Transactions, individual metrics via PerformanceMetric, and convenient bundles like Summary and RiskMetrics.
Previewing Upcoming Trades ¶
Engine.PredictedPortfolio previews what trades a strategy would make on the next scheduled trade date using currently available data. This is useful for strategies that trade infrequently (e.g., monthly) where users want to see what trades are coming before the actual trade date.
The method clones the current portfolio, advances the engine's date to the next scheduled trade date, fills any data gaps by copying the last available prices forward day-by-day, and runs the strategy's Compute against the shadow copy. The strategy is completely unaware it is a prediction run.
Backtest runs a prediction automatically after its final step and stores the result on the returned portfolio; query it with Prediction:
result, err := eng.Backtest(ctx, start, end)
if err != nil {
log.Fatal(err)
}
pred := result.Prediction()
for _, tx := range pred.Transactions {
fmt.Printf("%s %s %.0f shares\n", tx.Type, tx.Asset.Ticker, tx.Qty)
}
The prediction is persisted to the SQLite output in the prediction, predicted_transactions, predicted_holdings, and predicted_annotations tables.
PredictedPortfolio can also be called directly after a backtest or during live operation:
predicted, err := eng.PredictedPortfolio(ctx)
if err != nil {
log.Fatal(err)
}
for _, tx := range predicted.Transactions() {
fmt.Printf("%s %s %.0f shares\n", tx.Type, tx.Asset.Ticker, tx.Qty)
}
The returned portfolio includes annotations, justifications, and all other portfolio state produced by the prediction run.
Configuration ¶
Strategy parameters are defined as exported struct fields with struct tags. The engine populates them via reflection before calling Setup.
Four tags control how a field is exposed:
- pvbt: CLI flag name. Defaults to the lowercase field name if omitted.
- desc: Description for help text.
- default: Default value, parsed from a string representation.
- suggest: Named presets, pipe-delimited as name=value pairs.
Supported field types:
- float64: decimal number (e.g. default:"0.05")
- int: integer (e.g. default:"12")
- string: plain text (e.g. default:"momentum")
- bool: true or false (e.g. default:"true")
- time.Duration: Go duration string (e.g. default:"720h")
- asset.Asset: ticker symbol (e.g. default:"SPY"), resolved via Engine.Asset
- universe.Universe: comma-separated tickers (e.g. default:"VOO,SCZ"), resolved and wrapped in a StaticUniverse via Engine.Universe
Hydration runs before Setup. The engine reflects over the strategy struct and processes each exported field with a default tag. If the field is already non-zero (set by the caller or CLI flags), it is not overwritten. Otherwise the default tag value is parsed into the field's type. For asset.Asset fields the ticker is resolved via Engine.Asset. For universe.Universe fields the comma-separated tickers are resolved and wrapped in a StaticUniverse.
The CLI uses the pvbt and desc tags to register cobra flags automatically. When a user passes --riskOn "SPY,QQQ", the field is populated before hydration runs, so the default tag is skipped. CLI overrides are supported for asset.Asset fields (single ticker, e.g. --bench QQQ) and for universe.Universe fields (comma-separated tickers, e.g. --riskOn "SPY,QQQ"). Overrides set only the Ticker; the engine re-resolves through the asset registry during hydration so downstream code has full metadata.
Metadata ¶
Strategies can optionally implement the Descriptor interface to provide additional metadata such as a shortcode, description, source URL, version, schedule, and benchmark via the StrategyDescription struct. When Schedule and Benchmark are declared in Describe(), the engine reads them during initialization and the strategy does not need to set them in Setup. Engine.SetBenchmark in Setup overrides the value from Describe(). The risk-free rate (DGS3MO) is resolved automatically by the engine.
DescribeStrategy takes a Strategy (not an engine) and produces a StrategyInfo struct that serializes to JSON. It collects name and description from the strategy, schedule and benchmark from Describe(), parameters from struct tags, and suggestions grouped by preset name. It does not require an engine or Setup to have run.
Logging ¶
Strategies use zerolog for structured logging. The logger is carried on the context passed to Compute. Use zerolog.Ctx(ctx) to retrieve it:
func (s *ADM) Compute(ctx context.Context, eng *engine.Engine, port portfolio.Portfolio, batch *portfolio.Batch) error {
log := zerolog.Ctx(ctx)
log.Info().Str("strategy", s.Name()).Msg("computing")
return nil
}
The engine attaches a pre-configured logger to the context before calling Compute. Strategies should use zerolog.Ctx(ctx) rather than creating their own logger. This ensures log output is consistent and correctly scoped to the current computation step.
Design Principles ¶
Two principles shaped the API.
Strategies should read like their plain-English descriptions. Accelerating Dual Momentum computes 1-, 3-, and 6-month momentum on a set of risk-on assets, averages the scores, and invests in the highest-scoring asset if it is above zero -- otherwise it moves to a risk-off asset. The code says exactly that, in roughly the same number of words.
The same code should work in a backtest and in production. A strategy that runs against 20 years of historical data should deploy to a live trading system without modification. The API never exposes whether you are in a simulation or operating in real time.
Example (Backtest) ¶
This example runs a buy-and-hold backtest with synthetic data from data.ExampleData.
package main
import (
"context"
"fmt"
"time"
"github.com/penny-vault/pvbt/data"
"github.com/penny-vault/pvbt/engine"
"github.com/penny-vault/pvbt/portfolio"
)
// BuyAndHold is a minimal strategy that buys SPY on the first step
// and holds it. It uses imperative ordering rather than the
// select-weight-rebalance pipeline.
type BuyAndHold struct {
bought bool
}
func (s *BuyAndHold) Name() string { return "buy-and-hold" }
func (s *BuyAndHold) Setup(_ *engine.Engine) {}
func (s *BuyAndHold) Describe() engine.StrategyDescription {
return engine.StrategyDescription{Schedule: "@monthend", Benchmark: "SPY"}
}
func (s *BuyAndHold) Compute(ctx context.Context, e *engine.Engine, p portfolio.Portfolio, batch *portfolio.Batch) error {
if s.bought {
return nil
}
spy := e.Asset("SPY")
batch.Order(ctx, spy, portfolio.Buy, 20)
s.bought = true
return nil
}
func main() {
dp, ap := data.ExampleData()
eng := engine.New(&BuyAndHold{},
engine.WithInitialDeposit(10_000),
engine.WithDataProvider(dp),
engine.WithAssetProvider(ap),
)
defer eng.Close()
ctx := context.Background()
start := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2024, time.June, 1, 0, 0, 0, 0, time.UTC)
p, err := eng.Backtest(ctx, start, end)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Final value: $%.2f\n", p.Value())
}
Output: Final value: $10177.60
Example (Momentum) ¶
This example runs a momentum rotation strategy with synthetic data.
package main
import (
"context"
"fmt"
"time"
"github.com/penny-vault/pvbt/data"
"github.com/penny-vault/pvbt/engine"
"github.com/penny-vault/pvbt/portfolio"
"github.com/penny-vault/pvbt/signal"
"github.com/penny-vault/pvbt/universe"
)
// MomentumStrategy picks the asset with the highest 3-month momentum
// each month, falling back to TLT when nothing has positive momentum.
// It demonstrates the select-weight-rebalance pipeline.
type MomentumStrategy struct {
RiskOn universe.Universe `pvbt:"riskOn" desc:"equity universe" default:"SPY,GLD"`
RiskOff universe.Universe `pvbt:"riskOff" desc:"safe-haven" default:"TLT"`
}
func (s *MomentumStrategy) Name() string { return "momentum" }
func (s *MomentumStrategy) Setup(_ *engine.Engine) {}
func (s *MomentumStrategy) Describe() engine.StrategyDescription {
return engine.StrategyDescription{Schedule: "@monthend", Benchmark: "SPY"}
}
func (s *MomentumStrategy) Compute(ctx context.Context, _ *engine.Engine, p portfolio.Portfolio, batch *portfolio.Batch) error {
mom := signal.Momentum(ctx, s.RiskOn, portfolio.Months(3), data.MetricClose)
if err := mom.Err(); err != nil {
return nil
}
riskOffDF, err := s.RiskOff.At(ctx, data.MetricClose)
if err != nil {
return nil
}
portfolio.MaxAboveZero(data.MetricClose, riskOffDF).Select(mom)
plan, err := portfolio.EqualWeight(mom)
if err != nil {
return nil
}
batch.RebalanceTo(ctx, plan...)
return nil
}
func main() {
dp, ap := data.ExampleData()
eng := engine.New(&MomentumStrategy{},
engine.WithInitialDeposit(10_000),
engine.WithDataProvider(dp),
engine.WithAssetProvider(ap),
)
defer eng.Close()
ctx := context.Background()
start := time.Date(2024, time.February, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2024, time.June, 1, 0, 0, 0, 0, time.UTC)
p, err := eng.Backtest(ctx, start, end)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Final value: $%.2f\n", p.Value())
}
Output: Final value: $9896.98
Index ¶
- Variables
- func ApplyParams(eng *Engine, preset string, params map[string]string) error
- func ForwardFillTo(df *data.DataFrame, targetDate time.Time) (*data.DataFrame, error)
- func IsTestOnlyField(field reflect.StructField) bool
- func ParameterName(field reflect.StructField) string
- type DateRangeMode
- type Descriptor
- type Engine
- func (e *Engine) Asset(ticker string) asset.Asset
- func (e *Engine) Backtest(ctx context.Context, start, end time.Time) (portfolio.Portfolio, error)
- func (eng *Engine) ChildAllocations(overrides ...map[string]float64) (portfolio.Allocation, error)
- func (eng *Engine) ChildPortfolios() map[string]portfolio.Portfolio
- func (e *Engine) Close() error
- func (e *Engine) CurrentDate() time.Time
- func (e *Engine) Fetch(ctx context.Context, assets []asset.Asset, lookback portfolio.Period, ...) (*data.DataFrame, error)
- func (e *Engine) FetchAt(ctx context.Context, assets []asset.Asset, timestamp time.Time, ...) (*data.DataFrame, error)
- func (e *Engine) FetchFundamentalsByDateKey(ctx context.Context, assets []asset.Asset, metrics []data.Metric, ...) (*data.DataFrame, error)
- func (e *Engine) IndexUniverse(indexName string) universe.Universe
- func (e *Engine) IsUserParam(name string) bool
- func (e *Engine) MarkUserParams(names ...string)
- func (e *Engine) Now() time.Time
- func (e *Engine) PredictedPortfolio(ctx context.Context) (portfolio.Portfolio, error)
- func (e *Engine) Prices(ctx context.Context, assets ...asset.Asset) (*data.DataFrame, error)
- func (e *Engine) RatedUniverse(analyst string, filter data.RatingFilter) universe.Universe
- func (e *Engine) RunLive(ctx context.Context) (<-chan portfolio.PortfolioManager, error)
- func (e *Engine) SetBenchmark(a asset.Asset)
- func (e *Engine) SetFundamentalDimension(dim string)
- func (e *Engine) SpliceUniverse(primary string, fallbacks ...universe.SplicePeriod) universe.Universe
- func (e *Engine) Universe(assets ...asset.Asset) universe.Universe
- type FundamentalsByDateKeyOption
- type IntradayProvider
- type MarginCallHandler
- type MiddlewareConfig
- type Option
- func WithAccount(acct portfolio.PortfolioManager) Option
- func WithAssetProvider(p data.AssetProvider) Option
- func WithBenchmarkTicker(ticker string) Option
- func WithBroker(b broker.Broker) Option
- func WithCacheMaxBytes(n int64) Option
- func WithDataProvider(providers ...data.DataProvider) Option
- func WithDateRangeMode(mode DateRangeMode) Option
- func WithFillModel(base broker.BaseModel, adjusters ...broker.Adjuster) Option
- func WithGrossMaintenanceLeverage(ratio float64) Option
- func WithInitialDeposit(amount float64) Option
- func WithMarginModel(model portfolio.RegT) Option
- func WithMaxLeverage(ratio float64) Option
- func WithMiddlewareConfig(cfg MiddlewareConfig) Option
- func WithPortfolioSnapshot(snap portfolio.PortfolioSnapshot) Option
- func WithProgressCallback(fn ProgressCallback) Option
- func WithUserParams(names ...string) Option
- type Parameter
- type ParameterInfo
- type ProgressCallback
- type ProgressEvent
- type Reconciler
- type SimulatedBroker
- func (b *SimulatedBroker) Balance(_ context.Context) (broker.Balance, error)
- func (b *SimulatedBroker) Cancel(_ context.Context, orderID string) error
- func (b *SimulatedBroker) Close() error
- func (b *SimulatedBroker) Connect(_ context.Context) error
- func (b *SimulatedBroker) EvaluatePending()
- func (b *SimulatedBroker) Fills() <-chan broker.Fill
- func (b *SimulatedBroker) Orders(_ context.Context) ([]broker.Order, error)
- func (b *SimulatedBroker) Positions(_ context.Context) ([]broker.Position, error)
- func (b *SimulatedBroker) Replace(_ context.Context, _ string, _ broker.Order) error
- func (b *SimulatedBroker) SetBorrowRate(rate float64)
- func (b *SimulatedBroker) SetDataFetcher(df broker.DataFetcher)
- func (b *SimulatedBroker) SetFillPipeline(pp *broker.Pipeline)
- func (b *SimulatedBroker) SetInitialMarginRate(rate float64)
- func (b *SimulatedBroker) SetMaxLeverage(ratio float64)
- func (b *SimulatedBroker) SetPortfolio(p portfolio.Portfolio)
- func (b *SimulatedBroker) SetPriceProvider(p broker.PriceProvider, date time.Time)
- func (b *SimulatedBroker) Submit(ctx context.Context, order broker.Order) error
- func (b *SimulatedBroker) SubmitGroup(ctx context.Context, orders []broker.Order, _ broker.GroupType) error
- func (b *SimulatedBroker) Transactions(ctx context.Context, _ time.Time) ([]broker.Transaction, error)
- type Strategy
- type StrategyDescription
- type StrategyInfo
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNoMinuteBar = errors.New("engine: no minute bar available for order fill")
ErrNoMinuteBar reports that no intra-day minute bar is available to fill an order at the firing moment -- for example an asset whose intraday data coverage begins after the backtest date. It is a recoverable, per-order condition: the broker resolves the order as failed rather than aborting the simulation, so callers must test for it with errors.Is.
Functions ¶
func ApplyParams ¶
ApplyParams resolves an optional preset and merges explicit parameter overrides onto the engine's strategy. When preset is non-empty, the strategy must implement Descriptor so that DescribeStrategy can produce the aggregated Suggestions map. Explicit params always take precedence over preset values. After all simple-type fields are set via applyParamValue, hydrateFields is called to resolve asset.Asset and universe.Universe fields.
func ForwardFillTo ¶
ForwardFillTo extends a DataFrame by copying the last row's values forward to the target date, spaced according to the DataFrame's frequency. Returns the original DataFrame unchanged if it already covers the target date. Returns an error for Tick frequency since it has no regular interval.
func IsTestOnlyField ¶ added in v0.7.0
func IsTestOnlyField(field reflect.StructField) bool
IsTestOnlyField reports whether the given strategy struct field is marked as test-only via a `testonly:"true"` struct tag. Test-only fields are hidden from every user-facing surface (CLI flags, describe output, TUI, presets, study sweeps) and cannot be set through ApplyParams. They remain exported so that test code can assign them directly.
The tag value must parse as a Go boolean. An unparseable value is a programming error in the strategy source code, so this function panics rather than silently treating it as false.
func ParameterName ¶ added in v0.7.0
func ParameterName(field reflect.StructField) string
ParameterName returns the parameter name used for a strategy struct field. It prefers an explicit `pvbt` struct tag and otherwise derives a slug from the Go field name by converting PascalCase or camelCase to kebab-case (for example `RiskOn` becomes `risk-on`). This function is the single source of truth for field-to-parameter-name derivation; both the engine and the CLI call it so the names used in `describe`, cobra flags, and `--preset` lookups always agree.
Types ¶
type DateRangeMode ¶
type DateRangeMode int
DateRangeMode controls how the engine handles a backtest date range when warmup data is insufficient.
const ( // DateRangeModeStrict returns an error if any asset lacks sufficient // warmup data before the requested start date. DateRangeModeStrict DateRangeMode = iota // DateRangeModePermissive adjusts the start date forward until all // assets have sufficient warmup data. Returns an error only if no // valid start date exists before the end date. DateRangeModePermissive )
type Descriptor ¶
type Descriptor interface {
Describe() StrategyDescription
}
Descriptor is an optional interface strategies can implement to provide metadata for serialization. Strategies that don't implement it get empty fields.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine orchestrates data access, computation scheduling, and portfolio management for both backtesting and live trading.
func (*Engine) Asset ¶
Asset looks up an asset by ticker from the pre-loaded registry. Panics if the ticker cannot be resolved.
func (*Engine) Backtest ¶
Backtest executes a full backtest over [start, end] using the engine's configured strategy and data providers. It returns the portfolio after running every scheduled trading date.
func (*Engine) ChildAllocations ¶
ChildAllocations returns an Allocation that expands each child strategy's current portfolio holdings into underlying asset weights, scaled by the child's target weight. Call with no arguments to use declared weights, or pass a map to override weights dynamically.
func (*Engine) ChildPortfolios ¶
ChildPortfolios returns the child strategy portfolios keyed by name.
func (*Engine) Close ¶
Close releases all resources held by the engine, including closing the broker and all registered data providers.
func (*Engine) CurrentDate ¶
CurrentDate returns the current simulation date (calendar date, with the time-of-day component set to the trading-day boundary used by the engine for end-of-day operations like dividend posting and equity recording).
func (*Engine) Fetch ¶
func (e *Engine) Fetch(ctx context.Context, assets []asset.Asset, lookback portfolio.Period, metrics []data.Metric) (*data.DataFrame, error)
Fetch implements data.DataSource.
func (*Engine) FetchAt ¶
func (e *Engine) FetchAt(ctx context.Context, assets []asset.Asset, timestamp time.Time, metrics []data.Metric) (*data.DataFrame, error)
FetchAt implements data.DataSource.
func (*Engine) FetchFundamentalsByDateKey ¶ added in v0.7.2
func (e *Engine) FetchFundamentalsByDateKey( ctx context.Context, assets []asset.Asset, metrics []data.Metric, dateKey time.Time, options ...FundamentalsByDateKeyOption, ) (*data.DataFrame, error)
FetchFundamentalsByDateKey returns fundamental data for a specific reporting period. dateKey identifies the normalized quarter boundary (e.g. 2024-03-31 for Q1 2024). All metrics must be fundamentals; non-fundamental metrics produce an error. Subject to point-in-time correctness: only filings with event_date <= eng.CurrentDate() are included by default, overridable via WithAsOfDate. Assets that have not filed for dateKey as of that cap get NaN values.
This call bypasses the engine's column cache and issues a fresh provider query each time. Strategies that need the result across multiple Compute invocations should cache it themselves.
func (*Engine) IndexUniverse ¶
IndexUniverse creates a universe whose membership is determined by index composition (e.g. S&P 500, Nasdaq 100). The engine finds an IndexProvider from its registered providers, creates the universe, and wires it with the engine's data source.
func (*Engine) IsUserParam ¶ added in v0.9.1
IsUserParam reports whether the named field has been marked as explicitly set by the caller. A nil receiver is treated as "no fields marked", which keeps hydrateFields safe to call without an engine in white-box tests.
func (*Engine) MarkUserParams ¶ added in v0.9.1
MarkUserParams records strategy field names (kebab-case) that the caller has explicitly set. hydrateFields uses this set to skip applying struct-tag defaults to those fields, so an explicit zero override survives. Names not matching any strategy field are stored without effect.
func (*Engine) Now ¶ added in v0.10.0
Now returns the current simulation timestamp. For daily-only strategies this matches CurrentDate(). For strategies firing intra-day on a cron schedule with hour/minute components, Now() returns the precise firing timestamp during a Compute call (e.g. 2026-05-13 10:00:00 ET); outside of an intra-day Compute it falls back to the end-of-day boundary.
func (*Engine) PredictedPortfolio ¶
PredictedPortfolio runs the strategy's Compute against a shadow copy of the current portfolio using the next scheduled trade date. Data is forward-filled from the last available date to the predicted date. The strategy is unaware it is a prediction run. Child strategy accounts are also cloned so that ChildAllocations returns correct weights during the prediction run.
func (*Engine) Prices ¶
Prices implements broker.PriceProvider. It returns close, high, low, and volume prices plus dividend/split data for the requested assets at the engine's current simulation date. High and low are needed by EvaluatePending for intrabar bracket order evaluation. Volume is needed by the MarketImpact fill adjuster.
When the engine is mid intra-day firing (currentTime carries an hour/minute component beyond the day boundary), Prices returns the next 1-minute bar after currentTime so that orders placed during the firing fill at the immediate next bar, consistent with daily next-bar fill semantics. Dividend and SplitFactor are NaN at minute resolution.
func (*Engine) RatedUniverse ¶
RatedUniverse creates a universe whose membership is determined by analyst ratings. The engine finds a RatingProvider from its registered providers, creates the universe, and wires it with the engine's data source.
func (*Engine) RunLive ¶
RunLive starts continuous live trading execution. It performs the same initialization as Backtest (loading assets, hydrating fields, building provider routing, calling Setup), then launches a goroutine that fires on each scheduled time. The returned channel receives the portfolio after each step; sends are non-blocking so a slow consumer does not block the loop. Cancel the context to stop execution and close the channel.
func (*Engine) SetBenchmark ¶
SetBenchmark sets the benchmark asset for performance comparison. Typically called by the runner (CLI) rather than the strategy itself. Strategies should suggest a benchmark via Describe() instead.
func (*Engine) SetFundamentalDimension ¶ added in v0.7.0
SetFundamentalDimension configures the fundamental data dimension used by this engine. Valid values: "ARQ", "ARY", "ART", "MRQ", "MRY", "MRT". Call this from Strategy.Setup. If not called, defaults to "ARQ".
func (*Engine) SpliceUniverse ¶ added in v0.9.0
func (e *Engine) SpliceUniverse(primary string, fallbacks ...universe.SplicePeriod) universe.Universe
SpliceUniverse creates a single-asset universe that substitutes proxy tickers for dates before the primary's listing. This lets backtests of strategies that name a recent ticker (e.g. TQQQ, listed in 2010) extend further into history by transparently using a proxy (e.g. QLD) for pre-listing bars. Each fallback covers dates strictly before its Before cutoff. Order routing and reporting use whichever ticker is live on the order date, so positions migrate naturally when the simulation crosses a cutoff.
type FundamentalsByDateKeyOption ¶ added in v0.7.3
type FundamentalsByDateKeyOption func(*fundamentalsByDateKeyOpts)
FundamentalsByDateKeyOption configures a call to Engine.FetchFundamentalsByDateKey.
func WithAsOfDate ¶ added in v0.7.3
func WithAsOfDate(asOfDate time.Time) FundamentalsByDateKeyOption
WithAsOfDate caps which filings are considered available to the call. Only filings with event_date <= asOfDate are returned. This lets strategies emulate a "formation date" earlier than the current rebalance date -- for example, screening on prior-year fundamentals that were available by March 31 even when the rebalance runs in June.
asOfDate must be non-zero and not later than Engine.CurrentDate(); otherwise the fetch returns an error. When the option is not set, Engine.CurrentDate() is used as the cap.
type IntradayProvider ¶ added in v0.10.0
type IntradayProvider interface {
IntradayFetch(
ctx context.Context,
assets []asset.Asset,
metrics []data.Metric,
start, end time.Time,
timesOfDay []data.TimeOfDay,
) (*data.DataFrame, error)
}
IntradayProvider is implemented by data providers capable of serving 1-minute bars from the pv-data ClickHouse store. The engine routes intraday Fetch calls to whichever registered provider satisfies this interface.
type MarginCallHandler ¶
type MarginCallHandler interface {
OnMarginCall(ctx context.Context, eng *Engine, port portfolio.Portfolio, batch *portfolio.Batch) error
}
MarginCallHandler is an optional interface that strategies may implement to handle margin calls. When a margin deficiency is detected, the engine calls OnMarginCall before falling back to automatic liquidation.
type MiddlewareConfig ¶ added in v0.5.0
type MiddlewareConfig struct {
Risk risk.RiskConfig
Tax tax.TaxConfig
}
MiddlewareConfig holds the complete middleware configuration for a run.
func (*MiddlewareConfig) HasMiddleware ¶ added in v0.5.0
func (cfg *MiddlewareConfig) HasMiddleware() bool
HasMiddleware reports whether the configuration results in any middleware being applied. It returns false only when profile is "none" (or empty) and no individual risk overrides are set and tax is disabled.
func (*MiddlewareConfig) ValidateAndApplyDefaults ¶ added in v0.5.0
func (cfg *MiddlewareConfig) ValidateAndApplyDefaults() error
ValidateAndApplyDefaults checks that all configuration values are within acceptable bounds and fills in defaults where needed.
type Option ¶
type Option func(*Engine)
Option configures the engine.
func WithAccount ¶
func WithAccount(acct portfolio.PortfolioManager) Option
WithAccount sets a pre-configured portfolio Account for the engine to use. When set, this takes priority over WithInitialDeposit, WithPortfolioSnapshot, and WithBroker.
func WithAssetProvider ¶
func WithAssetProvider(p data.AssetProvider) Option
WithAssetProvider sets the asset provider for ticker resolution.
func WithBenchmarkTicker ¶
WithBenchmarkTicker sets the benchmark asset by ticker. This is resolved to an asset during engine initialization and takes priority over any benchmark suggested by the strategy's Describe() method.
func WithBroker ¶
WithBroker sets the broker used for order execution. If not set, the engine defaults to a SimulatedBroker.
func WithCacheMaxBytes ¶
WithCacheMaxBytes sets the maximum memory for the data cache. Default is 512MB.
func WithDataProvider ¶
func WithDataProvider(providers ...data.DataProvider) Option
WithDataProvider registers one or more data providers with the engine.
func WithDateRangeMode ¶
func WithDateRangeMode(mode DateRangeMode) Option
WithDateRangeMode sets how the engine handles date ranges when warmup data is insufficient. Default is DateRangeModeStrict.
func WithFillModel ¶ added in v0.5.0
WithFillModel configures the fill model used by the SimulatedBroker. If WithBroker is used, the fill model is silently ignored.
func WithGrossMaintenanceLeverage ¶ added in v0.10.0
WithGrossMaintenanceLeverage triggers a margin call when gross leverage, (LongMarketValue + ShortMarketValue) / Equity, exceeds the given ratio. This takes priority over any value supplied by the strategy's Describe().GrossMaintenanceLeverage and over any preset applied by WithMarginModel. Values <= 0 are ignored, in which case the strategy's value applies if set; otherwise the account default of 4.0 (Reg T-style 25% maintenance) applies.
func WithInitialDeposit ¶
WithInitialDeposit sets the starting cash balance for the portfolio. Mutually exclusive with WithPortfolioSnapshot.
func WithMarginModel ¶ added in v0.10.0
WithMarginModel applies a packaged margin model to the engine's account. WithMarginModel is applied before any per-knob overrides, so WithMaxLeverage and WithGrossMaintenanceLeverage win when used together with it. Pass portfolio.RegT{Initial: 0.5, Maintenance: 0.25} for canonical Reg T behavior, or portfolio.RegT{Initial: 1.0, Maintenance: math.Inf(1)} for a cash account.
func WithMaxLeverage ¶ added in v0.10.0
WithMaxLeverage caps gross leverage for the engine's account, expressed as (LongMarketValue + ShortMarketValue) / Equity. This takes priority over any value supplied by the strategy's Describe().MaxLeverage. Values <= 0 are ignored, in which case the strategy's value (or the account default of 1.0) applies.
MaxLeverage is an entry-time order gate. Liquidation is driven by short-side maintenance margin and, when configured, gross maintenance leverage. See WithGrossMaintenanceLeverage.
func WithMiddlewareConfig ¶ added in v0.5.0
func WithMiddlewareConfig(cfg MiddlewareConfig) Option
WithMiddlewareConfig sets the middleware configuration. The engine constructs risk and tax middleware from this config during initialization. When set, config-driven middleware replaces any strategy-declared middleware.
func WithPortfolioSnapshot ¶
func WithPortfolioSnapshot(snap portfolio.PortfolioSnapshot) Option
WithPortfolioSnapshot restores the portfolio from a previous run's snapshot. Mutually exclusive with WithInitialDeposit.
func WithProgressCallback ¶ added in v0.7.0
func WithProgressCallback(fn ProgressCallback) Option
WithProgressCallback registers a callback that receives a ProgressEvent after each backtest step. The callback runs synchronously inside the step loop, so it must return quickly. Used by the CLI to drive a progress bar.
func WithUserParams ¶ added in v0.9.1
WithUserParams declares strategy field names (kebab-case) that the caller has already explicitly set on the strategy. hydrateFields will not re-apply struct-tag defaults to these fields, so an explicit zero value (e.g. --sector-cap 0) is preserved instead of being silently overwritten by the field's `default` tag.
type Parameter ¶
type Parameter struct {
Name string `json:"name"`
FieldName string `json:"fieldName"`
Description string `json:"description,omitempty"`
GoType reflect.Type `json:"-"`
Default string `json:"default,omitempty"`
Suggestions map[string]string `json:"suggestions,omitempty"` // preset name -> value
}
Parameter describes a single configurable field on a strategy struct.
func StrategyParameters ¶
StrategyParameters reflects over the strategy struct and returns metadata for each exported field. Used by the CLI to generate flags and by UIs to build configuration forms.
type ParameterInfo ¶
type ParameterInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Type string `json:"type"`
Default string `json:"default,omitempty"`
}
ParameterInfo is the JSON-serializable form of a Parameter.
type ProgressCallback ¶ added in v0.7.0
type ProgressCallback func(ProgressEvent)
ProgressCallback receives progress events from the engine during a backtest. Callbacks must not block; the engine continues to the next step as soon as the callback returns.
type ProgressEvent ¶ added in v0.7.0
type ProgressEvent struct {
// Step is the 1-based index of the step that just completed.
Step int
// TotalSteps is the total number of simulation steps in the run.
TotalSteps int
// Date is the simulation date that just finished processing.
Date time.Time
// Start is the first simulation date of the run (after warmup
// adjustment).
Start time.Time
// End is the last simulation date of the run.
End time.Time
// MeasurementsEvaluated is the cumulative number of performance metric
// rows that have been computed and appended to the portfolio since the
// run began.
MeasurementsEvaluated int
}
ProgressEvent reports the engine's position within a backtest run. The engine emits one ProgressEvent per simulation step after the step has been fully processed (housekeeping, strategy compute if scheduled, equity recording, and metric evaluation). Receivers can compute completion fraction from either step counts or the date span.
type Reconciler ¶ added in v0.11.0
type Reconciler interface {
Reconcile(ctx context.Context, eng *Engine, port portfolio.Portfolio, batch *portfolio.Batch) error
}
Reconciler is an optional interface a strategy can implement to react to how its batch resolved against the market. After every order in a batch has resolved -- filled or failed -- the engine calls Reconcile with the same batch, now carrying each order's outcome (read via batch.Outcomes or batch.FailedOrders). The strategy may amend the batch in place, for example resubmitting a failed order as a market order or simply logging the miss.
Any orders the strategy appends are executed, and Reconcile is then called again with their outcomes. This repeats until a pass appends no new orders, so the strategy can drive a batch to completion. Strategies that do not implement Reconciler see failed orders silently not fill, as before.
type SimulatedBroker ¶
type SimulatedBroker struct {
// contains filtered or unexported fields
}
SimulatedBroker fills all orders at the close price for backtesting. The engine sets a PriceProvider and date before each Compute step.
func NewSimulatedBroker ¶
func NewSimulatedBroker() *SimulatedBroker
NewSimulatedBroker creates a SimulatedBroker with no price provider set.
func (*SimulatedBroker) Cancel ¶
func (b *SimulatedBroker) Cancel(_ context.Context, orderID string) error
func (*SimulatedBroker) Close ¶
func (b *SimulatedBroker) Close() error
func (*SimulatedBroker) EvaluatePending ¶
func (b *SimulatedBroker) EvaluatePending()
EvaluatePending checks all pending stop-loss and take-profit orders against the current bar's high/low prices, filling or cancelling as needed. For each group with both a stop-loss and take-profit: if stop-loss triggers (pessimistic outcome wins on same-bar conflict), fill at stop price and cancel take-profit. If only take-profit triggers, fill at limit price and cancel stop-loss. Falls back to close price when high/low are unavailable.
func (*SimulatedBroker) Fills ¶
func (b *SimulatedBroker) Fills() <-chan broker.Fill
Fills returns the receive-only channel on which fill reports are delivered.
func (*SimulatedBroker) SetBorrowRate ¶ added in v0.5.0
func (b *SimulatedBroker) SetBorrowRate(rate float64)
SetBorrowRate sets the annualized borrow fee rate for short positions.
func (*SimulatedBroker) SetDataFetcher ¶ added in v0.5.0
func (b *SimulatedBroker) SetDataFetcher(df broker.DataFetcher)
SetDataFetcher propagates a DataFetcher to the fill pipeline's models.
func (*SimulatedBroker) SetFillPipeline ¶ added in v0.5.0
func (b *SimulatedBroker) SetFillPipeline(pp *broker.Pipeline)
SetFillPipeline replaces the default close-price fill pipeline.
func (*SimulatedBroker) SetInitialMarginRate ¶
func (b *SimulatedBroker) SetInitialMarginRate(rate float64)
SetInitialMarginRate sets the initial margin rate for short sells.
func (*SimulatedBroker) SetMaxLeverage ¶ added in v0.10.0
func (b *SimulatedBroker) SetMaxLeverage(ratio float64)
SetMaxLeverage sets the gross-leverage cap (LMV+SMV)/Equity used to reject orders that would push the account above the cap. Values <= 0 disable the broker-level check; the account-level default still applies via portfolio.Account.MaxLeverage.
func (*SimulatedBroker) SetPortfolio ¶
func (b *SimulatedBroker) SetPortfolio(p portfolio.Portfolio)
SetPortfolio sets the portfolio reference used for margin checks.
func (*SimulatedBroker) SetPriceProvider ¶
func (b *SimulatedBroker) SetPriceProvider(p broker.PriceProvider, date time.Time)
SetPriceProvider updates the price provider and simulation date.
func (*SimulatedBroker) SubmitGroup ¶
func (b *SimulatedBroker) SubmitGroup(ctx context.Context, orders []broker.Order, _ broker.GroupType) error
SubmitGroup submits a contingent order group by calling Submit for each order.
func (*SimulatedBroker) Transactions ¶ added in v0.5.0
func (b *SimulatedBroker) Transactions(ctx context.Context, _ time.Time) ([]broker.Transaction, error)
type Strategy ¶
type Strategy interface {
Name() string
Setup(eng *Engine)
Compute(ctx context.Context, eng *Engine, portfolio portfolio.Portfolio, batch *portfolio.Batch) error
}
Strategy is the interface that all strategies must implement.
type StrategyDescription ¶
type StrategyDescription struct {
ShortCode string `json:"shortcode,omitempty"`
Description string `json:"description,omitempty"`
Source string `json:"source,omitempty"`
Version string `json:"version,omitempty"`
VersionDate time.Time `json:"versionDate,omitzero"`
Schedule string `json:"schedule,omitempty"`
Benchmark string `json:"benchmark,omitempty"`
Warmup int `json:"warmup,omitempty"`
// MaxLeverage caps gross leverage for the account, expressed as
// (LongMarketValue + ShortMarketValue) / Equity. Used as an
// entry-time gate: orders that would push the account above this
// ratio are rejected. Zero means "unspecified" -- the engine falls
// back to the account's own setting or the global default of 2.0
// (Reg T-style 2x leverage). The CLI --max-leverage flag overrides
// this value when set.
MaxLeverage float64 `json:"maxLeverage,omitempty"`
// GrossMaintenanceLeverage triggers liquidation when gross
// leverage exceeds this ratio. Zero means "unspecified" -- the
// engine falls back to the account's own setting or the global
// default of 4.0 (Reg T-style 25% maintenance). The CLI
// --gross-maintenance-leverage flag overrides this value when set.
GrossMaintenanceLeverage float64 `json:"grossMaintenanceLeverage,omitempty"`
}
StrategyDescription holds optional metadata about a strategy.
type StrategyInfo ¶
type StrategyInfo struct {
Name string `json:"name"`
ShortCode string `json:"shortcode,omitempty"`
Description string `json:"description,omitempty"`
Source string `json:"source,omitempty"`
Version string `json:"version,omitempty"`
VersionDate time.Time `json:"versionDate,omitzero"`
Schedule string `json:"schedule,omitempty"`
Benchmark string `json:"benchmark,omitempty"`
RiskFree string `json:"riskFree,omitempty"`
Warmup int `json:"warmup,omitempty"`
MaxLeverage float64 `json:"maxLeverage,omitempty"`
GrossMaintenanceLeverage float64 `json:"grossMaintenanceLeverage,omitempty"`
Parameters []ParameterInfo `json:"parameters"`
Suggestions map[string]map[string]string `json:"suggestions,omitempty"`
}
StrategyInfo is the complete serializable description of a strategy.
func DescribeStrategy ¶
func DescribeStrategy(strategy Strategy) StrategyInfo
DescribeStrategy builds a StrategyInfo from the strategy's metadata.
Source Files
¶
- apply_params.go
- backtest.go
- child.go
- child_allocations.go
- data_cache.go
- date_range_mode.go
- descriptor.go
- doc.go
- engine.go
- hydrate.go
- intraday_fetch.go
- intraday_fill_cache.go
- live.go
- margin_call.go
- margin_config.go
- metrics.go
- middleware_config.go
- option.go
- parameter.go
- progress.go
- reconciler.go
- simulated_broker.go
- stale_data.go
- strategy.go
- warmup.go
Directories
¶
| Path | Synopsis |
|---|---|
|
middleware
|
|
|
risk
Package risk provides portfolio middleware for enforcing risk constraints.
|
Package risk provides portfolio middleware for enforcing risk constraints. |
|
tax
Package tax provides portfolio middleware for tax-loss harvesting.
|
Package tax provides portfolio middleware for tax-loss harvesting. |