2026-06-08 14:25:44 +00:00
|
|
|
package report
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
|
|
|
|
|
|
"overnight-trading-bot/internal/domain"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestAverageAdverseSlippageBpsUsesLocalQuoteMid(t *testing.T) {
|
|
|
|
|
orders := []domain.Order{{
|
|
|
|
|
InstrumentUID: "uid",
|
|
|
|
|
Side: domain.SideBuy,
|
|
|
|
|
LimitPrice: decimal.NewFromInt(100),
|
|
|
|
|
FilledLots: 2,
|
|
|
|
|
AvgFillPrice: decimal.NewFromFloat(100.5),
|
|
|
|
|
RawStateJSON: `{"local":{"local_quote":{"mid":"100"}}}`,
|
|
|
|
|
UpdatedAt: time.Now().UTC(),
|
|
|
|
|
}}
|
|
|
|
|
got := AverageAdverseSlippageBps(orders, 0)
|
|
|
|
|
if !got.Equal(decimal.NewFromInt(50)) {
|
|
|
|
|
t.Fatalf("slippage=%s, want 50", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 14:58:56 +00:00
|
|
|
func TestAverageAdverseSlippageBpsPrefersFillQuoteMid(t *testing.T) {
|
|
|
|
|
orders := []domain.Order{{
|
|
|
|
|
InstrumentUID: "uid",
|
|
|
|
|
Side: domain.SideBuy,
|
|
|
|
|
LimitPrice: decimal.NewFromInt(100),
|
|
|
|
|
FilledLots: 1,
|
|
|
|
|
AvgFillPrice: decimal.NewFromFloat(102),
|
|
|
|
|
RawStateJSON: `{"local":{"local_quote":{"mid":"100"},"fill_quotes":[{"mid":"101"}]}}`,
|
|
|
|
|
UpdatedAt: time.Now().UTC(),
|
|
|
|
|
}}
|
|
|
|
|
got := AverageAdverseSlippageBps(orders, 0)
|
|
|
|
|
want := decimal.NewFromInt(10_000).Div(decimal.NewFromInt(101))
|
|
|
|
|
if got.Sub(want).Abs().GreaterThan(decimal.NewFromFloat(0.000001)) {
|
|
|
|
|
t.Fatalf("slippage=%s, want fill-mid based slippage", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 14:25:44 +00:00
|
|
|
func TestAverageAdverseSlippageBpsFallsBackToLimit(t *testing.T) {
|
|
|
|
|
orders := []domain.Order{{
|
|
|
|
|
InstrumentUID: "uid",
|
|
|
|
|
Side: domain.SideSell,
|
|
|
|
|
LimitPrice: decimal.NewFromInt(100),
|
|
|
|
|
FilledLots: 1,
|
|
|
|
|
AvgFillPrice: decimal.NewFromFloat(99.5),
|
|
|
|
|
RawStateJSON: `{}`,
|
|
|
|
|
UpdatedAt: time.Now().UTC(),
|
|
|
|
|
}}
|
|
|
|
|
got := AverageAdverseSlippageBps(orders, 0)
|
|
|
|
|
if !got.Equal(decimal.NewFromInt(50)) {
|
|
|
|
|
t.Fatalf("slippage=%s, want 50", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestComposeDailyComputesSlippageWhenInputIsZero(t *testing.T) {
|
|
|
|
|
msg := ComposeDaily(DailyInput{
|
|
|
|
|
Date: time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC),
|
|
|
|
|
Mode: domain.ModePaper,
|
|
|
|
|
Orders: []domain.Order{{
|
|
|
|
|
Side: domain.SideBuy,
|
|
|
|
|
LimitPrice: decimal.NewFromInt(100),
|
|
|
|
|
FilledLots: 1,
|
|
|
|
|
AvgFillPrice: decimal.NewFromFloat(100.5),
|
|
|
|
|
RawStateJSON: `{"local":{"local_quote":{"mid":"100"}}}`,
|
|
|
|
|
UpdatedAt: time.Now().UTC(),
|
|
|
|
|
}},
|
|
|
|
|
RiskStatus: "ok",
|
|
|
|
|
})
|
|
|
|
|
if !strings.Contains(msg, "Среднее проскальзывание: 50.00 bps") {
|
|
|
|
|
t.Fatalf("report did not include computed slippage:\n%s", msg)
|
|
|
|
|
}
|
|
|
|
|
}
|