fifth version
This commit is contained in:
@@ -109,14 +109,14 @@ func Compute(instrument domain.Instrument, candles []domain.Candle, tradeDate ti
|
||||
}
|
||||
short := Rolling(overnight, cfg.RollingShort, cfg.EWMALambda)
|
||||
long := Rolling(overnight, cfg.RollingLong, cfg.EWMALambda)
|
||||
q05Abs := rollingQ05Abs(overnight, cfg.RollingShort)
|
||||
adv := ADV(candles, instrument.Lot, 20)
|
||||
rawEdgeBps := decimal.NewFromFloat(short.Mean).Mul(decimal.NewFromInt(10_000))
|
||||
instrumentCommission := instrument.ExpectedCommissionBpsPerSide.Mul(decimal.NewFromInt(2))
|
||||
commission := roundTripCommissionBps(instrument, cfg)
|
||||
expectedCost := spread.SpreadBps.
|
||||
Add(cfg.EntrySlippageBps).
|
||||
Add(cfg.ExitSlippageBps).
|
||||
Add(cfg.CommissionRoundtripBps).
|
||||
Add(instrumentCommission).
|
||||
Add(commission).
|
||||
Add(cfg.RiskBufferBps)
|
||||
return domain.FeatureSet{
|
||||
InstrumentUID: instrument.InstrumentUID,
|
||||
@@ -126,6 +126,7 @@ func Compute(instrument domain.Instrument, candles []domain.Candle, tradeDate ti
|
||||
MuOn60: decimal.NewFromFloat(short.Mean),
|
||||
MuOn252: decimal.NewFromFloat(long.Mean),
|
||||
SigmaOn60: decimal.NewFromFloat(short.StdDev),
|
||||
Q05On60Abs: q05Abs,
|
||||
TStatOn60: decimal.NewFromFloat(short.TStat),
|
||||
WinOn60: decimal.NewFromFloat(short.WinRate),
|
||||
EWMAOn: decimal.NewFromFloat(short.EWMA),
|
||||
@@ -141,6 +142,26 @@ func Compute(instrument domain.Instrument, candles []domain.Candle, tradeDate ti
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rollingQ05Abs(values []float64, window int) decimal.Decimal {
|
||||
if window <= 0 || len(values) < window {
|
||||
return decimal.Zero
|
||||
}
|
||||
sample := values[len(values)-window:]
|
||||
q05 := decimal.NewFromFloat(Quantile(sample, 0.05))
|
||||
if q05.IsNegative() {
|
||||
return q05.Neg()
|
||||
}
|
||||
return q05
|
||||
}
|
||||
|
||||
func roundTripCommissionBps(instrument domain.Instrument, cfg PipelineConfig) decimal.Decimal {
|
||||
instrumentCommission := instrument.ExpectedCommissionBpsPerSide.Mul(decimal.NewFromInt(2))
|
||||
if instrumentCommission.IsPositive() {
|
||||
return instrumentCommission
|
||||
}
|
||||
return cfg.CommissionRoundtripBps
|
||||
}
|
||||
|
||||
func historicalDailyCandles(candles []domain.Candle, tradeDate time.Time) []domain.Candle {
|
||||
tradeDay := dateOnly(tradeDate)
|
||||
out := make([]domain.Candle, 0, len(candles))
|
||||
|
||||
@@ -41,14 +41,93 @@ func TestComputeExpectedCostIncludesCommissionAndSlippage(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.ExpectedCostBps.Equal(decimal.NewFromInt(26)) {
|
||||
t.Fatalf("expected cost=%s, want 26", got.ExpectedCostBps)
|
||||
if !got.ExpectedCostBps.Equal(decimal.NewFromInt(22)) {
|
||||
t.Fatalf("expected cost=%s, want 22", got.ExpectedCostBps)
|
||||
}
|
||||
if !got.EntryIntervalVolume.Equal(decimal.NewFromInt(10000)) || !got.ExitIntervalVolume.Equal(decimal.NewFromInt(9000)) {
|
||||
t.Fatalf("interval volumes were not preserved: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeExpectedCostFallsBackToConfigCommission(t *testing.T) {
|
||||
candles := flatCandles(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), 6)
|
||||
got, err := Compute(domain.Instrument{
|
||||
InstrumentUID: "uid",
|
||||
Lot: 1,
|
||||
}, candles, candles[5].TradeDate, SpreadResult{SpreadBps: decimal.NewFromInt(10)}, PipelineConfig{
|
||||
RollingShort: 2,
|
||||
RollingLong: 2,
|
||||
EWMALambda: 0.08,
|
||||
RiskBufferBps: decimal.NewFromInt(5),
|
||||
EntrySlippageBps: decimal.NewFromInt(2),
|
||||
ExitSlippageBps: decimal.NewFromInt(3),
|
||||
CommissionRoundtripBps: decimal.NewFromInt(4),
|
||||
}, decimal.Zero, decimal.Zero)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !got.ExpectedCostBps.Equal(decimal.NewFromInt(24)) {
|
||||
t.Fatalf("expected cost=%s, want 24", got.ExpectedCostBps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeStoresHistoricalQ05Abs(t *testing.T) {
|
||||
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
returns := []string{"-0.10", "0.01", "0.02", "0.03", "0.04"}
|
||||
candles := []domain.Candle{{
|
||||
InstrumentUID: "uid",
|
||||
TradeDate: start,
|
||||
Open: decimal.NewFromInt(100),
|
||||
Close: decimal.NewFromInt(100),
|
||||
VolumeLots: decimal.NewFromInt(1),
|
||||
}}
|
||||
for i, raw := range returns {
|
||||
r, err := decimal.NewFromString(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
open := decimal.NewFromInt(100).Mul(decimal.NewFromInt(1).Add(r))
|
||||
candles = append(candles, domain.Candle{
|
||||
InstrumentUID: "uid",
|
||||
TradeDate: start.AddDate(0, 0, i+1),
|
||||
Open: open,
|
||||
Close: decimal.NewFromInt(100),
|
||||
VolumeLots: decimal.NewFromInt(1),
|
||||
})
|
||||
}
|
||||
got, err := Compute(domain.Instrument{InstrumentUID: "uid", Lot: 1}, candles, start.AddDate(0, 0, 6), SpreadResult{}, PipelineConfig{
|
||||
RollingShort: 5,
|
||||
RollingLong: 5,
|
||||
EWMALambda: 0.08,
|
||||
}, decimal.Zero, decimal.Zero)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := decimal.NewFromFloat(0.078)
|
||||
diff := got.Q05On60Abs.Sub(want)
|
||||
if diff.IsNegative() {
|
||||
diff = diff.Neg()
|
||||
}
|
||||
if diff.GreaterThan(decimal.NewFromFloat(0.000001)) {
|
||||
t.Fatalf("Q05On60Abs=%s, want about %s", got.Q05On60Abs, want)
|
||||
}
|
||||
}
|
||||
|
||||
func flatCandles(start time.Time, count int) []domain.Candle {
|
||||
candles := make([]domain.Candle, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
price := decimal.NewFromInt(int64(100 + i))
|
||||
candles = append(candles, domain.Candle{
|
||||
InstrumentUID: "uid",
|
||||
TradeDate: start.AddDate(0, 0, i),
|
||||
Open: price,
|
||||
Close: price,
|
||||
VolumeLots: decimal.NewFromInt(1000),
|
||||
})
|
||||
}
|
||||
return candles
|
||||
}
|
||||
|
||||
func TestIntervalVolume(t *testing.T) {
|
||||
got := IntervalVolume([]domain.Candle{
|
||||
{Close: decimal.NewFromInt(100), VolumeLots: decimal.NewFromInt(10)},
|
||||
|
||||
Reference in New Issue
Block a user