engine

package
v0.12.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

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:

  1. Fetches housekeeping data (close, adjusted close, dividends) for held assets, the benchmark, and the risk-free asset.
  2. Records dividend income for held positions.
  3. 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.
  4. Updates the simulated broker with the current price provider and date so orders can fill.
  5. Calls Compute. The strategy fetches data, computes signals, and tells the portfolio to rebalance.
  6. Fetches post-Compute prices for all held assets (including newly acquired positions) and updates the equity curve.
  7. Computes all registered performance metrics across standard windows (5yr, 3yr, 1yr, YTD, MTD, WTD, and since-inception).
  8. 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

Examples

Constants

This section is empty.

Variables

View Source
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

func ApplyParams(eng *Engine, preset string, params map[string]string) error

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

func ForwardFillTo(df *data.DataFrame, targetDate time.Time) (*data.DataFrame, error)

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 New

func New(strategy Strategy, opts ...Option) *Engine

New creates a new engine for the given strategy.

func (*Engine) Asset

func (e *Engine) Asset(ticker string) asset.Asset

Asset looks up an asset by ticker from the pre-loaded registry. Panics if the ticker cannot be resolved.

func (*Engine) Backtest

func (e *Engine) Backtest(ctx context.Context, start, end time.Time) (portfolio.Portfolio, error)

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

func (eng *Engine) ChildAllocations(overrides ...map[string]float64) (portfolio.Allocation, error)

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

func (eng *Engine) ChildPortfolios() map[string]portfolio.Portfolio

ChildPortfolios returns the child strategy portfolios keyed by name.

func (*Engine) Close

func (e *Engine) Close() error

Close releases all resources held by the engine, including closing the broker and all registered data providers.

func (*Engine) CurrentDate

func (e *Engine) CurrentDate() time.Time

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

func (e *Engine) IndexUniverse(indexName string) universe.Universe

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

func (e *Engine) IsUserParam(name string) bool

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

func (e *Engine) MarkUserParams(names ...string)

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

func (e *Engine) Now() time.Time

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

func (e *Engine) PredictedPortfolio(ctx context.Context) (portfolio.Portfolio, error)

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

func (e *Engine) Prices(ctx context.Context, assets ...asset.Asset) (*data.DataFrame, error)

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

func (e *Engine) RatedUniverse(analyst string, filter data.RatingFilter) universe.Universe

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

func (e *Engine) RunLive(ctx context.Context) (<-chan portfolio.PortfolioManager, error)

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

func (e *Engine) SetBenchmark(a asset.Asset)

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

func (e *Engine) SetFundamentalDimension(dim string)

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.

func (*Engine) Universe

func (e *Engine) Universe(assets ...asset.Asset) universe.Universe

Universe creates a static universe wired to this engine for data fetching.

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

func WithBenchmarkTicker(ticker string) Option

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

func WithBroker(b broker.Broker) Option

WithBroker sets the broker used for order execution. If not set, the engine defaults to a SimulatedBroker.

func WithCacheMaxBytes

func WithCacheMaxBytes(n int64) Option

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

func WithFillModel(base broker.BaseModel, adjusters ...broker.Adjuster) Option

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

func WithGrossMaintenanceLeverage(ratio float64) Option

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

func WithInitialDeposit(amount float64) Option

WithInitialDeposit sets the starting cash balance for the portfolio. Mutually exclusive with WithPortfolioSnapshot.

func WithMarginModel added in v0.10.0

func WithMarginModel(model portfolio.RegT) Option

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

func WithMaxLeverage(ratio float64) Option

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

func WithUserParams(names ...string) Option

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

func StrategyParameters(s Strategy) []Parameter

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) Balance

func (*SimulatedBroker) Cancel

func (b *SimulatedBroker) Cancel(_ context.Context, orderID string) error

func (*SimulatedBroker) Close

func (b *SimulatedBroker) Close() error

func (*SimulatedBroker) Connect

func (b *SimulatedBroker) Connect(_ context.Context) 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) Orders

func (b *SimulatedBroker) Orders(_ context.Context) ([]broker.Order, error)

func (*SimulatedBroker) Positions

func (b *SimulatedBroker) Positions(_ context.Context) ([]broker.Position, error)

func (*SimulatedBroker) Replace

func (b *SimulatedBroker) Replace(_ context.Context, _ string, _ broker.Order) error

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) Submit

func (b *SimulatedBroker) Submit(ctx context.Context, order broker.Order) error

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.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL