fifth version

This commit is contained in:
2026-06-08 09:03:37 +00:00
parent b9efa98758
commit 2d57c4ff1f
26 changed files with 896 additions and 159 deletions
+36 -4
View File
@@ -130,6 +130,16 @@ func (s *Scheduler) Run(ctx context.Context) error {
}
}
func (s Scheduler) GracefulShutdown(ctx context.Context) error {
if s.svc.Repo == nil || s.svc.Execution == nil {
return nil
}
if err := s.cancelActiveOrders(ctx, domain.SideBuy, domain.OrderStatusCancelled, "shutdown_cancel_active_orders"); err != nil {
return err
}
return s.cancelActiveOrders(ctx, domain.SideSell, domain.OrderStatusCancelled, "shutdown_cancel_active_orders")
}
func (s *Scheduler) Step(ctx context.Context) error {
if err := s.checkInfrastructure(ctx); err != nil {
return err
@@ -370,7 +380,7 @@ func (s Scheduler) sizeSignal(portfolio domain.Portfolio, instrument domain.Inst
Lot: instrument.Lot,
EntryIntervalVolume: feature.EntryIntervalVolume,
ExitIntervalVolume: feature.ExitIntervalVolume,
Q05OvernightAbs: money.Abs(feature.SigmaOn60).Mul(decimal.NewFromFloat(1.65)),
Q05OvernightAbs: feature.Q05On60Abs,
}), nil
}
@@ -544,7 +554,7 @@ func (s *Scheduler) monitorEntryOrders(ctx context.Context, now time.Time) error
return s.svc.MarketData.LatestQuote(ctx, instrumentUID, s.cfg.QuoteDepth, s.cfg.MaxQuoteAge)
},
RepostCheck: func(ctx context.Context, order domain.Order, instrument domain.Instrument, book domain.OrderBook) error {
return s.repostPreTradeCheck(ctx, now, order, instrument, book)
return s.repostPreTradeCheck(ctx, s.nowUTC().In(s.cfg.Location), order, instrument, book)
},
})
if err != nil {
@@ -570,9 +580,31 @@ func (s *Scheduler) holdOvernight(ctx context.Context) error {
if err := s.closeEntryWindow(ctx); err != nil {
return err
}
if err := s.promoteEntryFilledPositions(ctx); err != nil {
return err
}
return s.periodicReconcile(ctx)
}
func (s Scheduler) promoteEntryFilledPositions(ctx context.Context) error {
positionsList, err := s.svc.Repo.ListOpenPositions(ctx, s.svc.AccountIDHash)
if err != nil {
return err
}
now := s.nowUTC()
for _, pos := range positionsList {
if pos.Status != domain.PositionEntryFilled {
continue
}
pos.Status = domain.PositionHoldingOvernight
pos.UpdatedAt = now
if err := s.svc.Repo.UpsertPosition(ctx, pos); err != nil {
return err
}
}
return nil
}
func (s *Scheduler) placeExitOrders(ctx context.Context, now time.Time) error {
if err := s.transitionTo(ctx, domain.StatePlaceExitOrders); err != nil {
return err
@@ -694,7 +726,7 @@ func (s *Scheduler) monitorExitOrders(ctx context.Context, now time.Time) error
return s.svc.MarketData.LatestQuote(ctx, instrumentUID, s.cfg.QuoteDepth, s.cfg.MaxQuoteAge)
},
RepostCheck: func(ctx context.Context, order domain.Order, instrument domain.Instrument, book domain.OrderBook) error {
return s.repostPreTradeCheck(ctx, now, order, instrument, book)
return s.repostPreTradeCheck(ctx, s.nowUTC().In(s.cfg.Location), order, instrument, book)
},
})
if err != nil {
@@ -1080,7 +1112,7 @@ func (s *Scheduler) failOpenPositionsAtHardDeadline(ctx context.Context) error {
now := s.nowUTC()
for _, pos := range positionsList {
switch pos.Status {
case domain.PositionHoldingOvernight, domain.PositionExitPartiallyFilled, domain.PositionExitOrderSent:
case domain.PositionEntryFilled, domain.PositionHoldingOvernight, domain.PositionExitPartiallyFilled, domain.PositionExitOrderSent:
pos.Status = domain.PositionExitFailed
pos.UpdatedAt = now
if err := s.svc.Repo.UpsertPosition(ctx, pos); err != nil {
+43
View File
@@ -650,6 +650,49 @@ func TestPlaceExitUsesCurrentTradeDateForOrderAndFreeCounter(t *testing.T) {
}
}
func TestGracefulShutdownCancelsActiveOrders(t *testing.T) {
ctx := context.Background()
repo := testutil.NewMemoryRepository()
gateway := tinvest.NewFakeGateway()
tradeDate := time.Date(2026, 6, 6, 0, 0, 0, 0, time.UTC)
order := domain.Order{
ClientOrderID: "shutdown-order",
BrokerOrderID: "broker-shutdown-order",
AccountIDHash: "hash",
InstrumentUID: "uid",
TradeDate: tradeDate,
Side: domain.SideBuy,
OrderType: domain.OrderTypeLimit,
LimitPrice: decimal.NewFromInt(100),
QuantityLots: 1,
Status: domain.OrderStatusSent,
RawStateJSON: "{}",
}
if err := repo.UpsertOrder(ctx, order); err != nil {
t.Fatal(err)
}
gateway.Orders[order.BrokerOrderID] = order
execEngine := execution.NewEngine(domain.ModeSandbox, "account", gateway, repo)
s := Scheduler{
cfg: Config{Mode: domain.ModeSandbox},
svc: Services{
Repo: repo,
Execution: &execEngine,
AccountIDHash: "hash",
},
}
if err := s.GracefulShutdown(ctx); err != nil {
t.Fatal(err)
}
orders, err := repo.ListOrders(ctx, "hash", tradeDate, tradeDate)
if err != nil {
t.Fatal(err)
}
if len(orders) != 1 || orders[0].Status != domain.OrderStatusCancelled {
t.Fatalf("orders=%+v, want cancelled", orders)
}
}
func mustTOD(raw string) timeutil.TimeOfDay {
tod, err := timeutil.ParseTimeOfDay(raw)
if err != nil {