139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"overnight-trading-bot/internal/domain"
|
|
)
|
|
|
|
const moscowOffset = 3 * time.Hour
|
|
|
|
type modeDayRow struct {
|
|
Mode string `db:"mode"`
|
|
Days int `db:"days"`
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run() error {
|
|
dsn := flag.String("dsn", os.Getenv("DB_DSN"), "MySQL/MariaDB DSN")
|
|
fromRaw := flag.String("from", "", "optional start date YYYY-MM-DD")
|
|
toRaw := flag.String("to", "", "optional end date YYYY-MM-DD, inclusive")
|
|
check := flag.Bool("check", true, "fail when live readiness thresholds are not met")
|
|
minReadonly := flag.Int("min-readonly-days", 20, "minimum live_readonly days")
|
|
minPaper := flag.Int("min-paper-days", 20, "minimum paper days")
|
|
minSandbox := flag.Int("min-sandbox-days", 10, "minimum sandbox days")
|
|
flag.Parse()
|
|
if *dsn == "" {
|
|
return fmt.Errorf("DB_DSN is required")
|
|
}
|
|
from, err := parseOptionalDate(*fromRaw)
|
|
if err != nil {
|
|
return fmt.Errorf("from: %w", err)
|
|
}
|
|
to, err := parseOptionalDate(*toRaw)
|
|
if err != nil {
|
|
return fmt.Errorf("to: %w", err)
|
|
}
|
|
db, err := sqlx.Open("mysql", *dsn)
|
|
if err != nil {
|
|
return fmt.Errorf("open db: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = db.Close()
|
|
}()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := db.PingContext(ctx); err != nil {
|
|
return fmt.Errorf("ping db: %w", err)
|
|
}
|
|
counts, err := loadModeDayCounts(ctx, db, from, to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printCounts(counts)
|
|
if !*check {
|
|
return nil
|
|
}
|
|
thresholds := map[domain.Mode]int{
|
|
domain.ModeLiveReadonly: *minReadonly,
|
|
domain.ModePaper: *minPaper,
|
|
domain.ModeSandbox: *minSandbox,
|
|
}
|
|
return checkThresholds(counts, thresholds)
|
|
}
|
|
|
|
func loadModeDayCounts(ctx context.Context, db *sqlx.DB, from, to time.Time) (map[domain.Mode]int, error) {
|
|
query := `SELECT mode, COUNT(DISTINCT DATE(DATE_ADD(ts, INTERVAL 3 HOUR))) AS days FROM system_state_history WHERE DAYOFWEEK(DATE_ADD(ts, INTERVAL 3 HOUR)) BETWEEN 2 AND 6`
|
|
var args []any
|
|
if !from.IsZero() {
|
|
query += ` AND ts >= ?`
|
|
args = append(args, from.Add(-moscowOffset))
|
|
}
|
|
if !to.IsZero() {
|
|
query += ` AND ts < ?`
|
|
args = append(args, to.AddDate(0, 0, 1).Add(-moscowOffset))
|
|
}
|
|
query += ` GROUP BY mode`
|
|
var rows []modeDayRow
|
|
if err := db.SelectContext(ctx, &rows, query, args...); err != nil {
|
|
return nil, fmt.Errorf("query mode days: %w", err)
|
|
}
|
|
counts := make(map[domain.Mode]int, len(rows))
|
|
for _, row := range rows {
|
|
mode, err := domain.ParseMode(row.Mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
counts[mode] = row.Days
|
|
}
|
|
return counts, nil
|
|
}
|
|
|
|
func printCounts(counts map[domain.Mode]int) {
|
|
modes := make([]string, 0, len(counts))
|
|
for mode := range counts {
|
|
modes = append(modes, string(mode))
|
|
}
|
|
sort.Strings(modes)
|
|
for _, rawMode := range modes {
|
|
mode := domain.Mode(rawMode)
|
|
fmt.Printf("%s=%d\n", mode, counts[mode])
|
|
}
|
|
}
|
|
|
|
func checkThresholds(counts map[domain.Mode]int, thresholds map[domain.Mode]int) error {
|
|
var failed []string
|
|
for mode, threshold := range thresholds {
|
|
if counts[mode] < threshold {
|
|
failed = append(failed, fmt.Sprintf("%s=%d/%d", mode, counts[mode], threshold))
|
|
}
|
|
}
|
|
sort.Strings(failed)
|
|
if len(failed) > 0 {
|
|
return fmt.Errorf("mode day thresholds not met: %s", strings.Join(failed, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseOptionalDate(raw string) (time.Time, error) {
|
|
if raw == "" {
|
|
return time.Time{}, nil
|
|
}
|
|
return time.ParseInLocation("2006-01-02", raw, time.UTC)
|
|
}
|