This commit is contained in:
@@ -2,6 +2,8 @@ package position
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -11,6 +13,8 @@ import (
|
||||
"overnight-trading-bot/internal/repository"
|
||||
)
|
||||
|
||||
var ErrExitFillExceedsPositionLots = errors.New("exit fill exceeds local position lots")
|
||||
|
||||
type Manager struct {
|
||||
repo repository.Repository
|
||||
}
|
||||
@@ -109,7 +113,21 @@ func (m Manager) OnExitFill(ctx context.Context, pos domain.Position, exitOrder
|
||||
if lot <= 0 {
|
||||
lot = 1
|
||||
}
|
||||
executedLots := min(exitOrder.FilledLots, pos.Lots)
|
||||
if exitOrder.FilledLots > pos.Lots {
|
||||
err := fmt.Errorf("%w: filled_lots=%d position_lots=%d instrument_uid=%s", ErrExitFillExceedsPositionLots, exitOrder.FilledLots, pos.Lots, pos.InstrumentUID)
|
||||
if m.repo != nil {
|
||||
_ = m.repo.InsertRiskEvent(ctx, domain.RiskEvent{
|
||||
TS: now,
|
||||
Severity: domain.SeverityCritical,
|
||||
EventType: "exit_overfill",
|
||||
InstrumentUID: pos.InstrumentUID,
|
||||
Message: err.Error(),
|
||||
ContextJSON: fmt.Sprintf(`{"filled_lots":%d,"position_lots":%d}`, exitOrder.FilledLots, pos.Lots),
|
||||
})
|
||||
}
|
||||
return pos, err
|
||||
}
|
||||
executedLots := exitOrder.FilledLots
|
||||
if executedLots < 0 {
|
||||
executedLots = 0
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package position
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -183,3 +184,34 @@ func TestOnExitFillUsesLotInRealizedEdgeCommissionBase(t *testing.T) {
|
||||
t.Fatalf("realized edge=%s, want -10 bps", updated.RealizedEdgeBps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnExitFillRejectsOverfillWithCriticalRiskEvent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := testutil.NewMemoryRepository()
|
||||
manager := NewManager(repo)
|
||||
openAt := time.Now().UTC()
|
||||
pos := domain.Position{
|
||||
AccountIDHash: "hash",
|
||||
InstrumentUID: "uid",
|
||||
OpenTradeDate: openAt,
|
||||
Lots: 3,
|
||||
Lot: 1,
|
||||
AvgBuyPrice: decimal.NewFromInt(100),
|
||||
Status: domain.PositionHoldingOvernight,
|
||||
OpenedAt: &openAt,
|
||||
}
|
||||
updated, err := manager.OnExitFill(ctx, pos, domain.Order{
|
||||
InstrumentUID: "uid",
|
||||
FilledLots: 5,
|
||||
AvgFillPrice: decimal.NewFromInt(110),
|
||||
})
|
||||
if !errors.Is(err, ErrExitFillExceedsPositionLots) {
|
||||
t.Fatalf("err=%v, want ErrExitFillExceedsPositionLots", err)
|
||||
}
|
||||
if updated.Lots != 3 || updated.Status != domain.PositionHoldingOvernight {
|
||||
t.Fatalf("position was mutated despite overfill: %+v", updated)
|
||||
}
|
||||
if len(repo.RiskEvents) != 1 || repo.RiskEvents[0].Severity != domain.SeverityCritical || repo.RiskEvents[0].EventType != "exit_overfill" {
|
||||
t.Fatalf("risk events=%+v, want one critical exit_overfill", repo.RiskEvents)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user