fifth version
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user