alt/services/worker/internal/backtest/strategies.go

58 lines
2 KiB
Go

package backtest
import (
"context"
"fmt"
"git.toki-labs.com/toki/alt/packages/domain/backtest"
"git.toki-labs.com/toki/alt/packages/domain/market"
)
// BuiltInStrategyID is the identifier of the worker's default bundled strategy.
// Command workflows and YAML scenarios reference it so a backtest can run
// end-to-end on imported bars without an operator authoring a custom strategy.
const BuiltInStrategyID backtest.StrategyID = "strategy-v1"
// BuiltInStrategyPort resolves the worker's bundled strategies. It is the
// production StrategyPort wired in cmd/alt-worker. Unknown ids return an error
// so a misconfigured run fails fast instead of silently producing an empty
// result.
type BuiltInStrategyPort struct{}
func NewBuiltInStrategyPort() *BuiltInStrategyPort {
return &BuiltInStrategyPort{}
}
func (p *BuiltInStrategyPort) GetStrategy(ctx context.Context, id backtest.StrategyID) (backtest.Strategy, error) {
switch id {
case BuiltInStrategyID:
return buyAndHoldStrategy{id: id}, nil
default:
return nil, fmt.Errorf("unknown strategy %q", id)
}
}
// buyAndHoldStrategy buys a single unit of each instrument the first time it is
// seen and then holds. It is intentionally minimal and deterministic so command
// validation can assert a stable terminal result from imported daily bars.
type buyAndHoldStrategy struct {
id backtest.StrategyID
}
func (s buyAndHoldStrategy) ID() backtest.StrategyID { return s.id }
func (s buyAndHoldStrategy) Decide(input backtest.StrategyInput) ([]backtest.OrderIntent, error) {
// Hold once a position exists. The engine passes the pre-trade portfolio for
// the current bar, so an existing position means this instrument was already
// bought on an earlier bar.
if _, held := input.Portfolio.Position(input.Bar.InstrumentID); held {
return nil, nil
}
return []backtest.OrderIntent{
{
InstrumentID: input.Bar.InstrumentID,
Side: backtest.OrderSideBuy,
Quantity: market.Quantity{Amount: market.Decimal{Value: "1"}},
},
}, nil
}