Files
2026-06-07 21:01:40 +00:00

88 lines
2.5 KiB
Go

package signal
import (
"testing"
"time"
"github.com/shopspring/decimal"
"overnight-trading-bot/internal/domain"
)
func sd(raw string) decimal.Decimal {
v, err := decimal.NewFromString(raw)
if err != nil {
panic(err)
}
return v
}
func baseCandidate() Candidate {
return Candidate{
TradeDate: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
Instrument: domain.Instrument{
InstrumentUID: "uid",
Ticker: "TRUR",
ClassCode: "TQTF",
Lot: 1,
MinPriceIncrement: sd("0.01"),
Currency: "RUB",
Enabled: true,
},
Features: domain.FeatureSet{
MuOn60: sd("0.002"),
MuOn252: sd("0.001"),
SigmaOn60: sd("0.01"),
TStatOn60: sd("2"),
WinOn60: sd("0.60"),
NetEdgeBps: sd("20"),
SpreadBps: sd("5"),
TickBps: sd("1"),
ADV20: sd("10000000"),
},
TradingStatus: domain.TradingStatusNormal,
FreeOrderOK: true,
}
}
func TestEngineEnter(t *testing.T) {
engine := New(Config{
MinTStat60: sd("1.25"),
MinWinRate60: sd("0.55"),
MinNetEdgeBps: sd("10"),
MinADVRUB: sd("5000000"),
MaxSpreadBpsDefault: sd("20"),
MaxSpreadBpsMoneyMarket: sd("5"),
MaxSpreadBpsBondFunds: sd("10"),
MaxSpreadBpsEquityFunds: sd("25"),
MaxTickBps: sd("10"),
RequireZeroCommission: true,
MaxPositions: 5,
})
sig := engine.Evaluate(baseCandidate())
if sig.Decision != domain.DecisionEnter || sig.RejectReason != "" {
t.Fatalf("unexpected signal: %+v", sig)
}
}
func TestEngineFirstRejectReason(t *testing.T) {
engine := New(Config{MinTStat60: sd("1.25"), MinWinRate60: sd("0.55"), MinNetEdgeBps: sd("10"), MinADVRUB: sd("5000000"), MaxSpreadBpsDefault: sd("20"), MaxTickBps: sd("10"), RequireZeroCommission: true})
c := baseCandidate()
c.Features.MuOn60 = decimal.Zero
c.Features.NetEdgeBps = decimal.Zero
sig := engine.Evaluate(c)
if sig.RejectReason != ReasonMuShort {
t.Fatalf("reason=%s", sig.RejectReason)
}
}
func TestEngineUsesSkipForCapacityReasons(t *testing.T) {
engine := New(Config{MinTStat60: sd("1.25"), MinWinRate60: sd("0.55"), MinNetEdgeBps: sd("10"), MinADVRUB: sd("5000000"), MaxSpreadBpsDefault: sd("20"), MaxTickBps: sd("10"), RequireZeroCommission: true, MaxPositions: 1})
c := baseCandidate()
c.OpenPositions = 1
sig := engine.Evaluate(c)
if sig.Decision != domain.DecisionSkip || sig.RejectReason != ReasonMaxPositions {
t.Fatalf("unexpected skip signal: %+v", sig)
}
}