fifth version
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"overnight-trading-bot/internal/domain"
|
||||
"overnight-trading-bot/internal/money"
|
||||
"overnight-trading-bot/internal/repository"
|
||||
"overnight-trading-bot/internal/timeutil"
|
||||
"overnight-trading-bot/internal/tinvest"
|
||||
)
|
||||
|
||||
@@ -29,6 +30,7 @@ type Engine struct {
|
||||
commissionTolerance decimal.Decimal
|
||||
requireZeroCommission bool
|
||||
quarantineOnNonZero bool
|
||||
clock timeutil.Clock
|
||||
}
|
||||
|
||||
func New(repo repository.Repository, gateway tinvest.Gateway, accountID, accountIDHash string) Engine {
|
||||
@@ -40,6 +42,7 @@ func New(repo repository.Repository, gateway tinvest.Gateway, accountID, account
|
||||
accountIDHash: accountIDHash,
|
||||
window: 72 * time.Hour,
|
||||
commissionTolerance: defaultCommissionTolerance,
|
||||
clock: timeutil.RealClock{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +69,13 @@ func (e Engine) WithCommissionPolicy(requireZero, quarantineOnNonZero bool, tole
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Engine) WithClock(clock timeutil.Clock) Engine {
|
||||
if clock != nil {
|
||||
e.clock = clock
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e Engine) Run(ctx context.Context) ([]domain.ReconciliationDiff, error) {
|
||||
if e.mu != nil {
|
||||
e.mu.Lock()
|
||||
@@ -79,7 +89,7 @@ func (e Engine) Run(ctx context.Context) ([]domain.ReconciliationDiff, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
now := e.nowUTC()
|
||||
localByBroker := make(map[string]domain.Order, len(localOrders))
|
||||
brokerByID := make(map[string]domain.Order, len(brokerOrders))
|
||||
for _, order := range localOrders {
|
||||
@@ -175,7 +185,7 @@ func (e Engine) Run(ctx context.Context) ([]domain.ReconciliationDiff, error) {
|
||||
}
|
||||
if err := e.repo.QuarantineInstrument(ctx, diff.InstrumentUID, diff.Message); err != nil {
|
||||
_ = e.repo.InsertRiskEvent(ctx, domain.RiskEvent{
|
||||
TS: time.Now().UTC(),
|
||||
TS: now,
|
||||
Severity: domain.SeverityCritical,
|
||||
EventType: "quarantine_failed",
|
||||
InstrumentUID: diff.InstrumentUID,
|
||||
@@ -192,6 +202,13 @@ func (e Engine) Run(ctx context.Context) ([]domain.ReconciliationDiff, error) {
|
||||
return diffs, nil
|
||||
}
|
||||
|
||||
func (e Engine) nowUTC() time.Time {
|
||||
if e.clock == nil {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
return e.clock.Now().UTC()
|
||||
}
|
||||
|
||||
func (e Engine) isInFlight(order domain.Order, now time.Time) bool {
|
||||
if e.inFlightGrace <= 0 || order.CreatedAt.IsZero() {
|
||||
return false
|
||||
|
||||
@@ -171,6 +171,50 @@ func TestReconciliationSkipsFreshInFlightLocalOrders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconciliationUsesInjectedClockForInFlightGrace(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := testutil.NewMemoryRepository()
|
||||
gateway := tinvest.NewFakeGateway()
|
||||
clock := &fixedClock{now: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC)}
|
||||
if err := repo.UpsertOrder(ctx, domain.Order{
|
||||
ClientOrderID: "fresh",
|
||||
AccountIDHash: "hash",
|
||||
InstrumentUID: "uid",
|
||||
TradeDate: clock.now,
|
||||
Side: domain.SideBuy,
|
||||
OrderType: domain.OrderTypeLimit,
|
||||
QuantityLots: 1,
|
||||
Status: domain.OrderStatusSent,
|
||||
CreatedAt: clock.now.Add(-5 * time.Second),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
diffs, err := New(repo, gateway, "account", "hash").
|
||||
WithClock(clock).
|
||||
WithInFlightGrace(10 * time.Second).
|
||||
Run(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, diff := range diffs {
|
||||
if diff.Kind == "local_order_without_broker_id" || diff.Kind == "missing_local_order" {
|
||||
t.Fatalf("fresh in-flight order produced diff: %+v", diffs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fixedClock struct {
|
||||
now time.Time
|
||||
}
|
||||
|
||||
func (c *fixedClock) Now() time.Time {
|
||||
return c.now
|
||||
}
|
||||
|
||||
func (c *fixedClock) Sleep(<-chan struct{}, time.Duration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestReconciliationFindsCashMismatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := testutil.NewMemoryRepository()
|
||||
|
||||
Reference in New Issue
Block a user