thirteenth version

This commit is contained in:
2026-06-09 21:04:01 +00:00
parent f877907b20
commit 4dec14f57c
19 changed files with 602 additions and 110 deletions
+31 -2
View File
@@ -27,6 +27,7 @@ type PipelineConfig struct {
EntryWindow timeutil.Window
ExitWindow timeutil.Window
IntervalVolumeLookback int
TradingDays []time.Time
Location *time.Location
}
@@ -39,6 +40,11 @@ func NewPipeline(repo repository.Repository, cfg PipelineConfig) Pipeline {
return Pipeline{repo: repo, cfg: cfg}
}
func (p Pipeline) WithTradingDays(days []time.Time) Pipeline {
p.cfg.TradingDays = days
return p
}
func (p Pipeline) Recompute(ctx context.Context, instrument domain.Instrument, tradeDate time.Time, spread SpreadResult) (domain.FeatureSet, error) {
from := tradeDate.AddDate(0, 0, -p.cfg.RollingLong-5)
to := dateOnly(tradeDate).AddDate(0, 0, -1)
@@ -94,8 +100,9 @@ func Compute(instrument domain.Instrument, candles []domain.Candle, tradeDate ti
var overnight []float64
var lastROn decimal.Decimal
var lastRDay decimal.Decimal
calendar := tradingCalendarFrom(cfg.TradingDays)
for i := 1; i < len(candles); i++ {
if !consecutiveDailyCandles(candles[i-1].TradeDate, candles[i].TradeDate) {
if !consecutiveDailyCandles(candles[i-1].TradeDate, candles[i].TradeDate, calendar) {
continue
}
rOn, err := OvernightReturn(candles[i].Open, candles[i-1].Close)
@@ -207,12 +214,34 @@ func historicalDailyCandles(candles []domain.Candle, tradeDate time.Time) []doma
return out
}
func consecutiveDailyCandles(previous, current time.Time) bool {
type tradingCalendar map[string]struct{}
func tradingCalendarFrom(days []time.Time) tradingCalendar {
if len(days) == 0 {
return nil
}
calendar := make(tradingCalendar, len(days))
for _, day := range days {
calendar[dateOnly(day).Format("2006-01-02")] = struct{}{}
}
return calendar
}
func consecutiveDailyCandles(previous, current time.Time, calendar tradingCalendar) bool {
prevDay := dateOnly(previous)
currentDay := dateOnly(current)
if !currentDay.After(prevDay) {
return false
}
if len(calendar) > 0 {
tradingDays := 0
for day := prevDay.AddDate(0, 0, 1); !day.After(currentDay); day = day.AddDate(0, 0, 1) {
if _, ok := calendar[day.Format("2006-01-02")]; ok {
tradingDays++
}
}
return tradingDays == 1
}
weekdays := 0
for day := prevDay.AddDate(0, 0, 1); !day.After(currentDay); day = day.AddDate(0, 0, 1) {
if day.Weekday() != time.Saturday && day.Weekday() != time.Sunday {
+41
View File
@@ -174,6 +174,47 @@ func TestComputeAllowsWeekendGap(t *testing.T) {
}
}
func TestComputeAllowsHolidayGapWithTradingCalendar(t *testing.T) {
monday := time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)
wednesday := monday.AddDate(0, 0, 2)
candles := []domain.Candle{
{InstrumentUID: "uid", TradeDate: monday, Open: decimal.NewFromInt(100), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(1)},
{InstrumentUID: "uid", TradeDate: wednesday, Open: decimal.NewFromInt(101), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(1)},
}
got, err := Compute(domain.Instrument{InstrumentUID: "uid", Lot: 1}, candles, wednesday.AddDate(0, 0, 1), SpreadResult{}, PipelineConfig{
RollingShort: 1,
RollingLong: 1,
EWMALambda: 0.08,
TradingDays: []time.Time{monday, wednesday},
}, decimal.Zero, decimal.Zero)
if err != nil {
t.Fatal(err)
}
want := decimal.RequireFromString("0.01")
if !got.ROn.Equal(want) {
t.Fatalf("ROn=%s, want %s across holiday gap", got.ROn, want)
}
}
func TestComputeRejectsMissingTradingDayWithTradingCalendar(t *testing.T) {
monday := time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC)
tuesday := monday.AddDate(0, 0, 1)
wednesday := monday.AddDate(0, 0, 2)
candles := []domain.Candle{
{InstrumentUID: "uid", TradeDate: monday, Open: decimal.NewFromInt(100), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(1)},
{InstrumentUID: "uid", TradeDate: wednesday, Open: decimal.NewFromInt(101), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(1)},
}
_, err := Compute(domain.Instrument{InstrumentUID: "uid", Lot: 1}, candles, wednesday.AddDate(0, 0, 1), SpreadResult{}, PipelineConfig{
RollingShort: 1,
RollingLong: 1,
EWMALambda: 0.08,
TradingDays: []time.Time{monday, tuesday, wednesday},
}, decimal.Zero, decimal.Zero)
if err == nil {
t.Fatal("expected missing trading day to make overnight pair unavailable")
}
}
func flatCandles(start time.Time, count int) []domain.Candle {
candles := make([]domain.Candle, 0, count)
for i := 0; i < count; i++ {