third version

This commit is contained in:
2026-06-08 07:05:01 +00:00
parent 282c841e11
commit 52a935b8b4
20 changed files with 1371 additions and 151 deletions
+57 -1
View File
@@ -17,6 +17,7 @@ import (
"time"
"github.com/jmoiron/sqlx"
"github.com/shopspring/decimal"
"overnight-trading-bot/internal/config"
"overnight-trading-bot/internal/domain"
@@ -138,6 +139,9 @@ func Run(ctx context.Context, opts Options) error {
if closer != nil {
defer closer()
}
if err := seedPaperGateway(ctx, repo, gateway); err != nil {
return err
}
notifier, err := notify.NewTelegram(notify.TelegramConfig{
BotToken: cfg.Telegram.BotToken,
ChatID: cfg.Telegram.ChatID,
@@ -161,7 +165,8 @@ func Run(ctx context.Context, opts Options) error {
WithCommissionPolicy(cfg.Commission.RequireZeroCommission, cfg.Commission.QuarantineOnNonZero, cfg.Risk.CommissionToleranceRUB)
sm := statemachine.New(repo, cfg.App.Mode)
if _, err := sm.Recover(ctx, recon); err != nil {
log.Warn("state recovery did not resume trading", "err", err)
_ = notifier.Alert(ctx, fmt.Sprintf("state recovery failed: %s", err))
return fmt.Errorf("state recovery: %w", err)
}
health := healthcheck.New(db.DB, gateway, time.Duration(cfg.Risk.MaxClockDriftSec)*time.Second)
health.Start(cfg.App.HealthcheckAddr)
@@ -270,6 +275,7 @@ func buildScheduler(clock timeutil.Clock, sm statemachine.System, cfg config.Con
ExitWindowStart: cfg.Execution.ExitWindowStart,
ExitWindowEnd: cfg.Execution.ExitWindowEnd,
HardExitDeadline: cfg.Execution.HardExitDeadline,
MarketClose: cfg.Execution.MarketClose,
QuoteDepth: cfg.Execution.QuoteDepth,
MaxQuoteAge: time.Duration(cfg.Execution.MaxQuoteAgeSec) * time.Second,
OrderPollInterval: time.Duration(cfg.Execution.OrderPollIntervalMS) * time.Millisecond,
@@ -282,9 +288,23 @@ func buildScheduler(clock timeutil.Clock, sm statemachine.System, cfg config.Con
RequireZeroCommission: cfg.Commission.RequireZeroCommission,
QuarantineOnNonZero: cfg.Commission.QuarantineOnNonZero,
ReconciliationInterval: 5 * time.Minute,
MaxOpenPositions: minPositive(cfg.Strategy.MaxPositions, cfg.Risk.MaxOpenPositions),
}, services)
}
func minPositive(a, b int) int {
switch {
case a <= 0:
return b
case b <= 0:
return a
case a < b:
return a
default:
return b
}
}
func openDB(ctx context.Context, cfg config.Config) (*sqlx.DB, error) {
db, err := sqlx.Open("mysql", cfg.DB.DSN)
if err != nil {
@@ -340,6 +360,42 @@ func buildGateway(ctx context.Context, cfg config.Config, log *slog.Logger) (tin
}
}
func seedPaperGateway(ctx context.Context, repo interface {
ListInstruments(context.Context, bool) ([]domain.Instrument, error)
}, gateway tinvest.Gateway) error {
fake, ok := gateway.(*tinvest.FakeGateway)
if !ok {
return nil
}
instrumentsList, err := repo.ListInstruments(ctx, true)
if err != nil {
return err
}
for _, instrument := range instrumentsList {
remote := instrument
if remote.InstrumentUID == "" || strings.HasPrefix(remote.InstrumentUID, "PENDING:") {
remote.InstrumentUID = "paper-" + strings.ToUpper(remote.Ticker)
}
if remote.Figi == "" {
remote.Figi = remote.InstrumentUID
}
if remote.Lot <= 0 {
remote.Lot = 1
}
if !remote.MinPriceIncrement.IsPositive() {
remote.MinPriceIncrement = decimal.RequireFromString("0.01")
}
if remote.Currency == "" {
remote.Currency = "RUB"
}
remote.Enabled = true
remote.UpdatedAt = time.Now().UTC()
fake.Instruments[remote.InstrumentUID] = remote
fake.Statuses[remote.InstrumentUID] = domain.TradingStatusNormal
}
return nil
}
func accountHash(accountID string) string {
sum := sha256.Sum256([]byte(accountID))
return hex.EncodeToString(sum[:])