fifth version

This commit is contained in:
2026-06-08 09:03:37 +00:00
parent b9efa98758
commit 2d57c4ff1f
26 changed files with 896 additions and 159 deletions
+84
View File
@@ -1,6 +1,7 @@
package backtest
import (
"strings"
"testing"
"time"
@@ -69,3 +70,86 @@ func TestMinuteExecutionRequiresReachableLimitAndParticipation(t *testing.T) {
t.Fatal("sell limit should be unreachable")
}
}
func TestEvaluateCandidateUsesInstrumentLotAndTick(t *testing.T) {
engine := New(Config{
RollingShort: 2,
RollingLong: 2,
MinTStat60: decimal.NewFromInt(-1),
MinWinRate60: decimal.NewFromFloat(0.1),
MinNetEdgeBps: decimal.NewFromInt(-1000),
MinADVRUB: decimal.NewFromInt(1),
Lot: 1,
LotsByInstrument: map[string]int64{"uid": 10},
MinPriceIncrementsByInstrument: map[string]decimal.Decimal{"uid": decimal.NewFromFloat(0.05)},
EntrySlippageBps: decimal.NewFromInt(13),
ExitSlippageBps: decimal.NewFromInt(13),
})
candles := candidateCandles("uid")
got, ok, err := engine.evaluateCandidate("uid", candles, 3)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("expected candidate")
}
if got.lot != 10 {
t.Fatalf("lot=%d, want 10", got.lot)
}
if !got.adv.Equal(decimal.NewFromInt(10_000)) {
t.Fatalf("adv=%s, want 10000", got.adv)
}
if !got.buy.Equal(decimal.NewFromFloat(100.15)) {
t.Fatalf("buy=%s, want rounded 100.15", got.buy)
}
if !got.sell.Equal(decimal.NewFromFloat(104.85)) {
t.Fatalf("sell=%s, want rounded 104.85", got.sell)
}
}
func TestWindowCapacityUsesMinuteEntryAndExitWindows(t *testing.T) {
engine := New(Config{
Lot: 10,
MaxParticipationRate: decimal.NewFromFloat(0.10),
})
entryDate := time.Date(2024, 1, 2, 18, 25, 0, 0, time.UTC)
exitDate := time.Date(2024, 1, 3, 10, 5, 0, 0, time.UTC)
got := engine.windowCapacity(candidate{
instrumentUID: "uid",
entry: domain.Candle{TradeDate: entryDate},
exit: domain.Candle{TradeDate: exitDate},
}, []domain.Candle{
{TradeDate: entryDate, Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(20)},
{TradeDate: exitDate, Close: decimal.NewFromInt(200), VolumeLots: decimal.NewFromInt(5)},
{TradeDate: time.Date(2024, 1, 3, 12, 0, 0, 0, time.UTC), Close: decimal.NewFromInt(999), VolumeLots: decimal.NewFromInt(999)},
})
if !got.Equal(decimal.NewFromInt(1000)) {
t.Fatalf("capacity=%s, want min(entry=20000, exit=10000)*0.10 = 1000", got)
}
}
func TestLoadCandlesCSVWithMetadata(t *testing.T) {
raw := strings.NewReader(`instrument_uid,trade_date,open,high,low,close,volume_lots,lot,min_price_increment
uid,2024-01-02,100,101,99,100,10,10,0.05
`)
candles, metadata, err := LoadCandlesCSVWithMetadata(raw)
if err != nil {
t.Fatal(err)
}
if len(candles["uid"]) != 1 {
t.Fatalf("candles=%+v", candles)
}
if metadata["uid"].Lot != 10 || !metadata["uid"].MinPriceIncrement.Equal(decimal.NewFromFloat(0.05)) {
t.Fatalf("metadata=%+v", metadata["uid"])
}
}
func candidateCandles(uid string) []domain.Candle {
start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
return []domain.Candle{
{InstrumentUID: uid, TradeDate: start, Open: decimal.NewFromInt(100), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(10)},
{InstrumentUID: uid, TradeDate: start.AddDate(0, 0, 1), Open: decimal.NewFromInt(101), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(10)},
{InstrumentUID: uid, TradeDate: start.AddDate(0, 0, 2), Open: decimal.NewFromInt(102), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(10)},
{InstrumentUID: uid, TradeDate: start.AddDate(0, 0, 3), Open: decimal.NewFromInt(105), Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(10)},
}
}