142 lines
4.1 KiB
Go
142 lines
4.1 KiB
Go
|
|
package position
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/shopspring/decimal"
|
||
|
|
|
||
|
|
"overnight-trading-bot/internal/domain"
|
||
|
|
"overnight-trading-bot/internal/testutil"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestOnEntryFillKeepsBuyCommission(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
manager := NewManager(testutil.NewMemoryRepository())
|
||
|
|
pos, err := manager.OnEntryFill(ctx, "hash", domain.Instrument{Lot: 1}, domain.Order{
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
TradeDate: time.Now().UTC(),
|
||
|
|
QuantityLots: 10,
|
||
|
|
FilledLots: 10,
|
||
|
|
AvgFillPrice: decimal.NewFromInt(100),
|
||
|
|
Commission: decimal.NewFromInt(3),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !pos.CommissionTotal.Equal(decimal.NewFromInt(3)) {
|
||
|
|
t.Fatalf("commission=%s, want 3", pos.CommissionTotal)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOnExitFillPartialUsesExecutedLots(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
manager := NewManager(testutil.NewMemoryRepository())
|
||
|
|
openAt := time.Now().UTC()
|
||
|
|
pos := domain.Position{
|
||
|
|
AccountIDHash: "hash",
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
OpenTradeDate: openAt,
|
||
|
|
Lots: 10,
|
||
|
|
Lot: 1,
|
||
|
|
AvgBuyPrice: decimal.NewFromInt(100),
|
||
|
|
Status: domain.PositionHoldingOvernight,
|
||
|
|
CommissionTotal: decimal.NewFromInt(2),
|
||
|
|
OpenedAt: &openAt,
|
||
|
|
}
|
||
|
|
updated, err := manager.OnExitFill(ctx, pos, domain.Order{
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
FilledLots: 4,
|
||
|
|
AvgFillPrice: decimal.NewFromInt(110),
|
||
|
|
Commission: decimal.NewFromInt(1),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if updated.Status != domain.PositionExitPartiallyFilled || updated.ClosedAt != nil {
|
||
|
|
t.Fatalf("unexpected partial status/closed_at: %+v", updated)
|
||
|
|
}
|
||
|
|
if updated.Lots != 6 {
|
||
|
|
t.Fatalf("remaining lots=%d, want 6", updated.Lots)
|
||
|
|
}
|
||
|
|
if !updated.GrossPnL.Equal(decimal.NewFromInt(40)) {
|
||
|
|
t.Fatalf("gross pnl=%s, want 40", updated.GrossPnL)
|
||
|
|
}
|
||
|
|
if updated.ExitFilledLots != 4 || !updated.AvgSellPrice.Equal(decimal.NewFromInt(110)) {
|
||
|
|
t.Fatalf("exit aggregation lots=%d avg=%s", updated.ExitFilledLots, updated.AvgSellPrice)
|
||
|
|
}
|
||
|
|
second, err := manager.OnExitFill(ctx, updated, domain.Order{
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
FilledLots: 3,
|
||
|
|
AvgFillPrice: decimal.NewFromInt(120),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
wantAvg := decimal.NewFromInt(800).Div(decimal.NewFromInt(7))
|
||
|
|
if second.ExitFilledLots != 7 || !second.AvgSellPrice.Equal(wantAvg) {
|
||
|
|
t.Fatalf("weighted avg sell=%s lots=%d, want %s/7", second.AvgSellPrice, second.ExitFilledLots, wantAvg)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOnExitFillUsesInstrumentLotForAbsolutePnL(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
manager := NewManager(testutil.NewMemoryRepository())
|
||
|
|
openAt := time.Now().UTC()
|
||
|
|
pos := domain.Position{
|
||
|
|
AccountIDHash: "hash",
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
OpenTradeDate: openAt,
|
||
|
|
Lots: 4,
|
||
|
|
Lot: 10,
|
||
|
|
AvgBuyPrice: decimal.NewFromInt(100),
|
||
|
|
Status: domain.PositionHoldingOvernight,
|
||
|
|
CommissionTotal: decimal.NewFromInt(2),
|
||
|
|
OpenedAt: &openAt,
|
||
|
|
}
|
||
|
|
updated, err := manager.OnExitFill(ctx, pos, domain.Order{
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
FilledLots: 4,
|
||
|
|
AvgFillPrice: decimal.NewFromInt(105),
|
||
|
|
Commission: decimal.NewFromInt(3),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !updated.GrossPnL.Equal(decimal.NewFromInt(200)) {
|
||
|
|
t.Fatalf("gross pnl=%s, want 200", updated.GrossPnL)
|
||
|
|
}
|
||
|
|
if !updated.NetPnL.Equal(decimal.NewFromInt(195)) {
|
||
|
|
t.Fatalf("net pnl=%s, want 195", updated.NetPnL)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOnExitFillUsesLotInRealizedEdgeCommissionBase(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
manager := NewManager(testutil.NewMemoryRepository())
|
||
|
|
openAt := time.Now().UTC()
|
||
|
|
pos := domain.Position{
|
||
|
|
AccountIDHash: "hash",
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
OpenTradeDate: openAt,
|
||
|
|
Lots: 1,
|
||
|
|
Lot: 100,
|
||
|
|
AvgBuyPrice: decimal.NewFromInt(100),
|
||
|
|
Status: domain.PositionHoldingOvernight,
|
||
|
|
OpenedAt: &openAt,
|
||
|
|
}
|
||
|
|
updated, err := manager.OnExitFill(ctx, pos, domain.Order{
|
||
|
|
InstrumentUID: "uid",
|
||
|
|
FilledLots: 1,
|
||
|
|
AvgFillPrice: decimal.NewFromInt(100),
|
||
|
|
Commission: decimal.NewFromInt(10),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !updated.RealizedEdgeBps.Equal(decimal.NewFromInt(-10)) {
|
||
|
|
t.Fatalf("realized edge=%s, want -10 bps", updated.RealizedEdgeBps)
|
||
|
|
}
|
||
|
|
}
|