thirteenth version

This commit is contained in:
2026-06-09 21:04:01 +00:00
parent f877907b20
commit 4dec14f57c
19 changed files with 602 additions and 110 deletions
+89 -45
View File
@@ -29,42 +29,40 @@ import (
"overnight-trading-bot/internal/tinvest"
)
const (
sizeReductionWindowTrades = 20
sizeReductionFactor = 0.5
sizeReductionTriggerBps = -10
intervalVolumeLookbackDays = 20
)
type Config struct {
Mode domain.Mode
Location *time.Location
RollingLong int
TickInterval time.Duration
EntrySignalTime timeutil.TimeOfDay
EntryWindowStart timeutil.TimeOfDay
EntryWindowEnd timeutil.TimeOfDay
NoNewEntryAfter timeutil.TimeOfDay
ExitWatchStart timeutil.TimeOfDay
ExitNotBefore timeutil.TimeOfDay
ExitWindowStart timeutil.TimeOfDay
ExitWindowEnd timeutil.TimeOfDay
HardExitDeadline timeutil.TimeOfDay
MarketClose timeutil.TimeOfDay
QuoteDepth int32
MaxQuoteAge time.Duration
OrderPollInterval time.Duration
PassiveImproveTicks int
MaxEntryOrderAttempts int
MaxExitOrderAttempts int
MinTimeToClose time.Duration
MaxClockDrift time.Duration
APIOutageHalt time.Duration
RequireZeroCommission bool
QuarantineOnNonZero bool
FreeOrderCountPolicy string
ReconciliationInterval time.Duration
MaxOpenPositions int
Mode domain.Mode
Location *time.Location
RollingLong int
IntervalVolumeLookbackDays int
TickInterval time.Duration
EntrySignalTime timeutil.TimeOfDay
EntryWindowStart timeutil.TimeOfDay
EntryWindowEnd timeutil.TimeOfDay
NoNewEntryAfter timeutil.TimeOfDay
ExitWatchStart timeutil.TimeOfDay
ExitNotBefore timeutil.TimeOfDay
ExitWindowStart timeutil.TimeOfDay
ExitWindowEnd timeutil.TimeOfDay
HardExitDeadline timeutil.TimeOfDay
MarketClose timeutil.TimeOfDay
QuoteDepth int32
MaxQuoteAge time.Duration
OrderPollInterval time.Duration
PassiveImproveTicks int
MaxEntryOrderAttempts int
MaxExitOrderAttempts int
MinTimeToClose time.Duration
MaxClockDrift time.Duration
APIOutageHalt time.Duration
RequireZeroCommission bool
QuarantineOnNonZero bool
FreeOrderCountPolicy string
ReconciliationInterval time.Duration
MaxOpenPositions int
SizeReductionWindowTrades int
SizeReductionFactor decimal.Decimal
SizeReductionTriggerBps decimal.Decimal
TradingCalendarExchange string
}
type Services struct {
@@ -113,6 +111,21 @@ func New(clock timeutil.Clock, sm statemachine.System, cfg Config, svc Services)
if cfg.ReconciliationInterval <= 0 {
cfg.ReconciliationInterval = 5 * time.Minute
}
if cfg.IntervalVolumeLookbackDays <= 0 {
cfg.IntervalVolumeLookbackDays = 20
}
if cfg.SizeReductionWindowTrades <= 0 {
cfg.SizeReductionWindowTrades = 20
}
if !cfg.SizeReductionFactor.IsPositive() {
cfg.SizeReductionFactor = decimal.RequireFromString("0.5")
}
if cfg.SizeReductionTriggerBps.IsZero() {
cfg.SizeReductionTriggerBps = decimal.NewFromInt(-10)
}
if cfg.TradingCalendarExchange == "" {
cfg.TradingCalendarExchange = "MOEX"
}
return Scheduler{clock: clock, sm: sm, cfg: cfg, svc: svc}
}
@@ -234,10 +247,16 @@ func (s *Scheduler) prepareSignals(ctx context.Context, now time.Time) error {
if err != nil {
return err
}
if err := s.svc.MarketData.BackfillDaily(ctx, instrumentsList, tradeDate.AddDate(0, 0, -s.cfg.RollingLong-10), tradeDate); err != nil {
dailyFrom := tradeDate.AddDate(0, 0, -s.cfg.RollingLong-10)
if err := s.svc.MarketData.BackfillDaily(ctx, instrumentsList, dailyFrom, tradeDate); err != nil {
return err
}
minuteFrom := s.cfg.EntryWindowStart.On(tradeDate.AddDate(0, 0, -intervalVolumeLookbackDays), s.cfg.Location)
tradingDays, err := s.svc.Gateway.GetTradingDays(ctx, s.cfg.TradingCalendarExchange, dailyFrom, tradeDate)
if err != nil {
return fmt.Errorf("load trading calendar %s: %w", s.cfg.TradingCalendarExchange, err)
}
s.svc.Features = s.svc.Features.WithTradingDays(tradingDays)
minuteFrom := s.cfg.EntryWindowStart.On(tradeDate.AddDate(0, 0, -s.cfg.IntervalVolumeLookbackDays), s.cfg.Location)
minuteTo := s.cfg.ExitWindowEnd.On(tradeDate, s.cfg.Location)
if err := s.svc.MarketData.BackfillMinute(ctx, instrumentsList, minuteFrom, minuteTo); err != nil {
s.logWarn("minute backfill failed; liquidity will fall back to ADV", "err", err)
@@ -904,15 +923,17 @@ func (s *Scheduler) sendDailyReport(ctx context.Context, now time.Time, riskStat
}
func (s *Scheduler) applySizeReductionRule(ctx context.Context, tradeDate time.Time, emitEvent bool) error {
averageError, count, ok, err := s.averageExpectedErrorBps(ctx, tradeDate, sizeReductionWindowTrades)
window := s.sizeReductionWindowTrades()
trigger := s.sizeReductionTriggerBps()
averageError, count, ok, err := s.averageExpectedErrorBps(ctx, tradeDate, window)
if err != nil {
return err
}
if !ok || count < sizeReductionWindowTrades || averageError.GreaterThanOrEqual(decimal.NewFromInt(sizeReductionTriggerBps)) {
if !ok || count < window || averageError.GreaterThanOrEqual(trigger) {
s.svc.Sizer = s.svc.Sizer.WithSizeFactor(decimal.NewFromInt(1))
return nil
}
factor := decimal.NewFromFloat(sizeReductionFactor)
factor := s.sizeReductionFactor()
s.svc.Sizer = s.svc.Sizer.WithSizeFactor(factor)
if emitEvent {
if err := s.svc.Repo.InsertRiskEvent(ctx, domain.RiskEvent{
@@ -1438,11 +1459,13 @@ func (s *Scheduler) handleLiveReadonlyAfterSizeReduction(ctx context.Context, tr
if s.cfg.Mode != domain.ModeLiveTrade {
return nil
}
previousAverage, previousCount, previousOK, err := s.averageExpectedErrorBpsWindow(ctx, tradeDate, sizeReductionWindowTrades, sizeReductionWindowTrades)
window := s.sizeReductionWindowTrades()
trigger := s.sizeReductionTriggerBps()
previousAverage, previousCount, previousOK, err := s.averageExpectedErrorBpsWindow(ctx, tradeDate, window, window)
if err != nil {
return err
}
if previousOK && previousCount == sizeReductionWindowTrades && previousAverage.LessThan(decimal.NewFromInt(sizeReductionTriggerBps)) {
if previousOK && previousCount == window && previousAverage.LessThan(trigger) {
return s.activateLiveReadonly(ctx, averageError, count, previousAverage, previousCount, factor)
}
if !emitRecommendation {
@@ -1466,9 +1489,9 @@ func (s *Scheduler) activateLiveReadonly(ctx context.Context, averageError decim
return nil
}
message := fmt.Sprintf(
"average expected_error_bps stayed below %d for two consecutive %d-trade windows; switching to live_readonly",
sizeReductionTriggerBps,
sizeReductionWindowTrades,
"average expected_error_bps stayed below %s for two consecutive %d-trade windows; switching to live_readonly",
s.sizeReductionTriggerBps().String(),
s.sizeReductionWindowTrades(),
)
s.cfg.Mode = domain.ModeLiveReadonly
if s.svc.Execution != nil {
@@ -1563,6 +1586,27 @@ func (s Scheduler) maxOrderAttemptsPerTrade() int {
return needed
}
func (s Scheduler) sizeReductionWindowTrades() int {
if s.cfg.SizeReductionWindowTrades <= 0 {
return 20
}
return s.cfg.SizeReductionWindowTrades
}
func (s Scheduler) sizeReductionFactor() decimal.Decimal {
if !s.cfg.SizeReductionFactor.IsPositive() {
return decimal.RequireFromString("0.5")
}
return s.cfg.SizeReductionFactor
}
func (s Scheduler) sizeReductionTriggerBps() decimal.Decimal {
if s.cfg.SizeReductionTriggerBps.IsZero() {
return decimal.NewFromInt(-10)
}
return s.cfg.SizeReductionTriggerBps
}
func (s Scheduler) orderBudgetNeededForAttempts(attempts int) int {
if attempts <= 0 {
attempts = 1
+2
View File
@@ -21,6 +21,8 @@ import (
"overnight-trading-bot/internal/tinvest"
)
const sizeReductionWindowTrades = 20
func TestPhaseUsesMoscowWindows(t *testing.T) {
loc := time.FixedZone("MSK", 3*60*60)
s := Scheduler{cfg: Config{