Author SHA1 Message Date
renovate[bot] 46cbaee18b Update module github.com/cenkalti/backoff/v4 to v7 2026-08-01 00:07:49 +00:00
renovate[bot] cded037103 Update module google.golang.org/grpc to v1.83.0
Deploy / Test, build and deploy (push) Failing after 1m21s
2026-07-31 00:14:56 +00:00
renovate[bot] 59a667e4f0 Update actions/setup-go action to v7
Deploy / Test, build and deploy (push) Failing after 1m42s
2026-07-17 00:01:44 +00:00
renovate[bot] 8e0bb259d2 Update module google.golang.org/grpc to v1.82.1
Deploy / Test, build and deploy (push) Failing after 1m18s
2026-07-16 00:15:29 +00:00
Valentin Popov a7f3f65074 chore(deps): update go module dependencies
Deploy / Test, build and deploy (push) Failing after 1m14s
2026-07-08 01:01:16 +00:00
Valentin Popov b76fd546fe fix(telegram): add request timeout for notifications
Deploy / Test, build and deploy (push) Failing after 6m3s
2026-07-08 04:59:24 +04:00
renovate[bot] 57e723db65 Update module google.golang.org/grpc to v1.82.0
Deploy / Test, build and deploy (push) Successful in 2m26s
2026-07-01 00:06:49 +00:00
Valentin Popov f46ec2abde fix: restore backoff module requirement
Deploy / Test, build and deploy (push) Successful in 2m28s
2026-06-27 03:00:11 +04:00
Valentin Popov a055e0606f feat: seed active T-Capital fund whitelist
Deploy / Test, build and deploy (push) Failing after 52s
2026-06-27 01:55:03 +04:00
Valentin Popov 6c2f868d21 Merge pull request 'Update module github.com/cenkalti/backoff/v4 to v6' (!6) from renovate/github.com-cenkalti-backoff-v4-6.x into master
Deploy / Test, build and deploy (push) Failing after 48s
Reviewed-on: #6
2026-06-20 15:13:57 +04:00
Valentin Popov e8f25d06e7 Merge pull request 'Update actions/checkout action to v7' (!5) from renovate/actions-checkout-7.x into master
Deploy / Test, build and deploy (push) Successful in 57s
Reviewed-on: #5
2026-06-20 15:13:44 +04:00
renovate[bot] 6bd6585c1f Update module github.com/cenkalti/backoff/v4 to v6 2026-06-19 00:01:46 +00:00
renovate[bot] b7c13c536f Update actions/checkout action to v7 2026-06-19 00:01:07 +00:00
14 changed files with 329 additions and 63 deletions
+2
View File
@@ -26,6 +26,8 @@ TELEGRAM_NOTIFY_INFO=true
TELEGRAM_NOTIFY_WARN=true
TELEGRAM_NOTIFY_ALERT=true
TELEGRAM_NOTIFY_REPORT=true
TELEGRAM_REQUEST_TIMEOUT_SEC=10
TELEGRAM_QUEUE_SIZE=256
STRATEGY_ROLLING_SHORT=60
STRATEGY_ROLLING_LONG=252
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: true
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Run Renovate
run: |
+2
View File
@@ -86,6 +86,8 @@ Common formats:
| `TELEGRAM_NOTIFY_WARN` | `true` | Send warnings. |
| `TELEGRAM_NOTIFY_ALERT` | `true` | Send critical alerts. |
| `TELEGRAM_NOTIFY_REPORT` | `true` | Send daily reports. |
| `TELEGRAM_REQUEST_TIMEOUT_SEC` | `10` | HTTP timeout for Telegram API calls, including the startup `getMe` check and message sends. |
| `TELEGRAM_QUEUE_SIZE` | `256` | In-memory Telegram notification queue size. The queue smooths transient Telegram slowness but is not persisted across process restarts. |
### STRATEGY
+2
View File
@@ -77,6 +77,8 @@ APP_MODE=backtest go run ./cmd/bot
| `TELEGRAM_NOTIFY_WARN` | `true` или `false` | `true` | boolean | Включает предупреждения. |
| `TELEGRAM_NOTIFY_ALERT` | `true` или `false` | `true` | boolean | Включает alert-сообщения. Они считаются критичными для доставки и ждут место в очереди. |
| `TELEGRAM_NOTIFY_REPORT` | `true` или `false` | `true` | boolean | Включает дневные отчёты. |
| `TELEGRAM_REQUEST_TIMEOUT_SEC` | целое число секунд | `10` | должно быть `> 0` | Таймаут HTTP-запросов к Telegram API, включая стартовую проверку `getMe` и отправку сообщений. |
| `TELEGRAM_QUEUE_SIZE` | целое число | `256` | должно быть `> 0` | Размер in-memory очереди Telegram-уведомлений. Очередь сглаживает временную медленную доставку, но не сохраняется между рестартами процесса. |
### STRATEGY
+8 -8
View File
@@ -5,7 +5,7 @@ go 1.26.2
require (
github.com/caarlos0/env/v11 v11.4.1
github.com/cenkalti/backoff/v4 v4.3.0
github.com/cenkalti/backoff/v6 v6.0.1
github.com/cenkalti/backoff/v7 v7.0.0
github.com/go-sql-driver/mysql v1.10.0
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/golang-migrate/migrate/v4 v4.19.1
@@ -14,7 +14,7 @@ require (
github.com/shopspring/decimal v1.4.0
github.com/testcontainers/testcontainers-go v0.43.0
github.com/testcontainers/testcontainers-go/modules/mariadb v0.43.0
google.golang.org/grpc v1.81.1
google.golang.org/grpc v1.83.0
google.golang.org/protobuf v1.36.11
)
@@ -65,15 +65,15 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect
golang.org/x/text v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+19 -17
View File
@@ -15,7 +15,7 @@ github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSw
github.com/caarlos0/env/v11 v11.4.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v6 v6.0.1/go.mod h1:5WCmPelT2zwAaNETjGJVKHDnZvjQdPsGeHHwm5lIPPI=
github.com/cenkalti/backoff/v7 v7.0.0/go.mod h1:qcKBGwsu4hpxHtQ8tWYsQ+ifzx2+sS+Xx/3jfe30lI8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -91,8 +91,6 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
@@ -123,8 +121,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russianinvestments/invest-api-go-sdk v1.40.1 h1:EZ9mA5fTlyspH8urdAMFXTzfUnMcVbCI3O0C88CrkUo=
github.com/russianinvestments/invest-api-go-sdk v1.40.1/go.mod h1:rOu2P3GMTQEkQxRpQfp+wK5k71c3SUDHIke3Ijr8cOU=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
@@ -135,12 +131,8 @@ github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0 h1:ZfWUJSIDnbNgoLAXMV1fc7lqcxBIX3zdnhwjaVUo7N0=
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0/go.mod h1:0kV+yHee7zAgp0yccydxjNnHvlC1EOavTLCeg/lnRBY=
github.com/testcontainers/testcontainers-go/modules/mariadb v0.43.0 h1:cvZGnhieICwBODSeoPlqdpNrQpnFA8n0L5/4E591Az4=
github.com/testcontainers/testcontainers-go/modules/mariadb v0.43.0/go.mod h1:GIuI5uvfhjP44sScfPruPjundARJx1RwnzTBwGEI4qY=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
@@ -155,14 +147,20 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6h
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
@@ -176,17 +174,21 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 h1:g0RAkxK/smSu/iRwC/KIX1mwUoVJtk2OjbgaeS4DmUM=
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324/go.mod h1:Z4WJ5pJOYWFWcHEQUelD5QaZDknIQkpIL/+fyJOT9+A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4=
google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.0 h1:vguDnZUPjE26w09A63VoxZPnvPjB5Riyc0mkXPFmAIU=
google.golang.org/grpc v1.82.0/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+9 -7
View File
@@ -172,13 +172,15 @@ func Run(ctx context.Context, opts Options) error {
return err
}
notifier, err := notify.NewTelegram(notify.TelegramConfig{
BotToken: cfg.Telegram.BotToken,
ChatID: cfg.Telegram.ChatID,
NotifyInfo: cfg.Telegram.NotifyInfo,
NotifyWarn: cfg.Telegram.NotifyWarn,
NotifyAlert: cfg.Telegram.NotifyAlert,
NotifyReport: cfg.Telegram.NotifyReport,
AuditSink: repo,
BotToken: cfg.Telegram.BotToken,
ChatID: cfg.Telegram.ChatID,
NotifyInfo: cfg.Telegram.NotifyInfo,
NotifyWarn: cfg.Telegram.NotifyWarn,
NotifyAlert: cfg.Telegram.NotifyAlert,
NotifyReport: cfg.Telegram.NotifyReport,
RequestTimeout: time.Duration(cfg.Telegram.RequestTimeoutSec) * time.Second,
QueueSize: cfg.Telegram.QueueSize,
AuditSink: repo,
}, log)
if err != nil {
return fmt.Errorf("create notifier: %w", err)
+14 -6
View File
@@ -66,12 +66,14 @@ type DBConfig struct {
}
type TelegramConfig struct {
BotToken string `env:"BOT_TOKEN"`
ChatID int64 `env:"CHAT_ID"`
NotifyInfo bool `env:"NOTIFY_INFO" envDefault:"true"`
NotifyWarn bool `env:"NOTIFY_WARN" envDefault:"true"`
NotifyAlert bool `env:"NOTIFY_ALERT" envDefault:"true"`
NotifyReport bool `env:"NOTIFY_REPORT" envDefault:"true"`
BotToken string `env:"BOT_TOKEN"`
ChatID int64 `env:"CHAT_ID"`
NotifyInfo bool `env:"NOTIFY_INFO" envDefault:"true"`
NotifyWarn bool `env:"NOTIFY_WARN" envDefault:"true"`
NotifyAlert bool `env:"NOTIFY_ALERT" envDefault:"true"`
NotifyReport bool `env:"NOTIFY_REPORT" envDefault:"true"`
RequestTimeoutSec int `env:"REQUEST_TIMEOUT_SEC" envDefault:"10"`
QueueSize int `env:"QUEUE_SIZE" envDefault:"256"`
}
type StrategyConfig struct {
@@ -201,6 +203,12 @@ func (c *Config) Validate() error {
if c.TInvest.RequestTimeoutSec <= 0 {
return errors.New("TINVEST_REQUEST_TIMEOUT_SEC must be positive")
}
if c.Telegram.RequestTimeoutSec <= 0 {
return errors.New("TELEGRAM_REQUEST_TIMEOUT_SEC must be positive")
}
if c.Telegram.QueueSize <= 0 {
return errors.New("TELEGRAM_QUEUE_SIZE must be positive")
}
if c.TInvest.TradingCalendarExchange == "" {
c.TInvest.TradingCalendarExchange = "MOEX"
}
+37
View File
@@ -86,6 +86,39 @@ func TestLoadSchedulerKnobsFromEnv(t *testing.T) {
}
}
func TestLoadTelegramQueueDefaults(t *testing.T) {
t.Setenv("APP_MODE", "backtest")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if cfg.Telegram.RequestTimeoutSec != 10 {
t.Fatalf("telegram request timeout=%d, want 10", cfg.Telegram.RequestTimeoutSec)
}
if cfg.Telegram.QueueSize != 256 {
t.Fatalf("telegram queue size=%d, want 256", cfg.Telegram.QueueSize)
}
}
func TestValidateRejectsInvalidTelegramRequestTimeout(t *testing.T) {
cfg := minimalBrokerConfig(domain.ModeSandbox)
cfg.Telegram.RequestTimeoutSec = 0
err := cfg.Validate()
if err == nil || !strings.Contains(err.Error(), "TELEGRAM_REQUEST_TIMEOUT_SEC") {
t.Fatalf("Validate err=%v, want TELEGRAM_REQUEST_TIMEOUT_SEC requirement", err)
}
}
func TestValidateRejectsInvalidTelegramQueueSize(t *testing.T) {
cfg := minimalBrokerConfig(domain.ModeSandbox)
cfg.Telegram.QueueSize = 0
err := cfg.Validate()
if err == nil || !strings.Contains(err.Error(), "TELEGRAM_QUEUE_SIZE") {
t.Fatalf("Validate err=%v, want TELEGRAM_QUEUE_SIZE requirement", err)
}
}
func minimalBrokerConfig(mode domain.Mode) Config {
return Config{
App: AppConfig{
@@ -99,6 +132,10 @@ func minimalBrokerConfig(mode domain.Mode) Config {
RequestTimeoutSec: 10,
},
DB: DBConfig{DSN: "user:pass@tcp(localhost:3306)/bot"},
Telegram: TelegramConfig{
RequestTimeoutSec: 10,
QueueSize: 256,
},
Execution: ExecutionConfig{
EntrySignalTime: mustTOD("18:10:00"),
EntryWindowStart: mustTOD("18:20:00"),
+41 -22
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
@@ -13,7 +14,11 @@ import (
"overnight-trading-bot/internal/domain"
)
const mustDeliverEnqueueTimeout = 2 * time.Second
const (
defaultQueueSize = 256
defaultRequestTimeout = 10 * time.Second
mustDeliverEnqueueTimeout = 2 * time.Second
)
type Notifier interface {
Info(ctx context.Context, msg string) error
@@ -32,13 +37,15 @@ func (Noop) Report(context.Context, string) error { return nil }
func (Noop) Close() error { return nil }
type TelegramConfig struct {
BotToken string
ChatID int64
NotifyInfo bool
NotifyWarn bool
NotifyAlert bool
NotifyReport bool
AuditSink AuditSink
BotToken string
ChatID int64
NotifyInfo bool
NotifyWarn bool
NotifyAlert bool
NotifyReport bool
RequestTimeout time.Duration
QueueSize int
AuditSink AuditSink
}
type AuditSink interface {
@@ -46,12 +53,13 @@ type AuditSink interface {
}
type Telegram struct {
cfg TelegramConfig
bot *tgbotapi.BotAPI
log *slog.Logger
queue chan outbound
done chan struct{}
closed chan struct{}
cfg TelegramConfig
bot *tgbotapi.BotAPI
log *slog.Logger
queue chan outbound
done chan struct{}
closed chan struct{}
retryDelay func(error, int) time.Duration
}
type outbound struct {
@@ -61,23 +69,34 @@ type outbound struct {
}
func NewTelegram(cfg TelegramConfig, log *slog.Logger) (Notifier, error) {
return newTelegramWithEndpoint(cfg, log, tgbotapi.APIEndpoint)
}
func newTelegramWithEndpoint(cfg TelegramConfig, log *slog.Logger, apiEndpoint string) (Notifier, error) {
if cfg.BotToken == "" || cfg.ChatID == 0 {
if log != nil {
log.Warn("telegram notifier disabled; TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID is empty")
}
return Noop{}, nil
}
bot, err := tgbotapi.NewBotAPI(cfg.BotToken)
if cfg.RequestTimeout <= 0 {
cfg.RequestTimeout = defaultRequestTimeout
}
if cfg.QueueSize <= 0 {
cfg.QueueSize = defaultQueueSize
}
bot, err := tgbotapi.NewBotAPIWithClient(cfg.BotToken, apiEndpoint, &http.Client{Timeout: cfg.RequestTimeout})
if err != nil {
return nil, err
}
t := &Telegram{
cfg: cfg,
bot: bot,
log: log,
queue: make(chan outbound, 256),
done: make(chan struct{}),
closed: make(chan struct{}),
cfg: cfg,
bot: bot,
log: log,
queue: make(chan outbound, cfg.QueueSize),
done: make(chan struct{}),
closed: make(chan struct{}),
retryDelay: telegramRetryDelay,
}
go t.dispatch()
return t, nil
@@ -181,7 +200,7 @@ func (t *Telegram) send(item outbound) {
for attempt := 0; attempt < 3; attempt++ {
if _, err := t.bot.Send(msg); err != nil {
lastErr = err
delay := telegramRetryDelay(err, attempt)
delay := t.retryDelay(err, attempt)
if t.log != nil {
t.log.Warn("telegram send failed", "attempt", attempt+1, "err", err, "retry_in", delay)
}
+129
View File
@@ -1,6 +1,10 @@
package notify
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -44,6 +48,131 @@ func TestTelegramRetryDelayUsesRetryAfter(t *testing.T) {
}
}
func TestNewTelegramUsesRequestTimeout(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond)
}))
defer server.Close()
start := time.Now()
_, err := newTelegramWithEndpoint(TelegramConfig{
BotToken: "token",
ChatID: 123,
RequestTimeout: 20 * time.Millisecond,
QueueSize: 1,
}, nil, telegramTestEndpoint(server))
elapsed := time.Since(start)
if err == nil {
t.Fatal("NewTelegram err=nil, want timeout")
}
if elapsed > 150*time.Millisecond {
t.Fatalf("NewTelegram took %s, want request timeout to fire quickly", elapsed)
}
}
func TestNewTelegramUsesConfiguredQueueSize(t *testing.T) {
server := telegramTestServer(t, func(w http.ResponseWriter, r *http.Request) {
writeTelegramOK(w, r)
})
defer server.Close()
notifier, err := newTelegramWithEndpoint(TelegramConfig{
BotToken: "token",
ChatID: 123,
RequestTimeout: time.Second,
QueueSize: 3,
}, nil, telegramTestEndpoint(server))
if err != nil {
t.Fatal(err)
}
defer notifier.Close()
telegram, ok := notifier.(*Telegram)
if !ok {
t.Fatalf("notifier type=%T, want *Telegram", notifier)
}
if got := cap(telegram.queue); got != 3 {
t.Fatalf("queue cap=%d, want 3", got)
}
}
func TestTelegramSendTimeoutAuditsMustDeliverFailure(t *testing.T) {
audit := &recordingAuditSink{events: make(chan domain.RiskEvent, 1)}
server := telegramTestServer(t, func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/sendMessage") {
time.Sleep(50 * time.Millisecond)
writeTelegramOK(w, r)
return
}
writeTelegramOK(w, r)
})
defer server.Close()
notifier, err := newTelegramWithEndpoint(TelegramConfig{
BotToken: "token",
ChatID: 123,
NotifyAlert: true,
RequestTimeout: 10 * time.Millisecond,
QueueSize: 1,
AuditSink: audit,
}, nil, telegramTestEndpoint(server))
if err != nil {
t.Fatal(err)
}
telegram := notifier.(*Telegram)
telegram.retryDelay = func(error, int) time.Duration { return time.Millisecond }
defer notifier.Close()
if err := notifier.Alert(context.Background(), "critical"); err != nil {
t.Fatal(err)
}
select {
case event := <-audit.events:
if event.EventType != "notification_undeliverable" {
t.Fatalf("event type=%s, want notification_undeliverable", event.EventType)
}
if event.Severity != domain.SeverityCritical {
t.Fatalf("event severity=%s, want critical", event.Severity)
}
case <-time.After(500 * time.Millisecond):
t.Fatal("timed out waiting for must-deliver audit event")
}
}
type assertErr struct{}
func (assertErr) Error() string { return "boom" }
type recordingAuditSink struct {
events chan domain.RiskEvent
}
func (s *recordingAuditSink) InsertRiskEvent(ctx context.Context, event domain.RiskEvent) error {
select {
case s.events <- event:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
func telegramTestEndpoint(server *httptest.Server) string {
return server.URL + "/bot%s/%s"
}
func telegramTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func writeTelegramOK(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch {
case strings.HasSuffix(r.URL.Path, "/getMe"):
_, _ = w.Write([]byte(`{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"test","username":"test_bot"}}`))
default:
_, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":1,"date":0,"chat":{"id":123,"type":"private"},"text":"ok"}}`))
}
}
@@ -0,0 +1,20 @@
UPDATE instruments
SET enabled=0,
quarantine=1,
quarantine_reason='rollback_tcapital_active_whitelist',
exclude_reason='rollback_tcapital_active_whitelist',
updated_at=UTC_TIMESTAMP(3)
WHERE instrument_uid IN (
'e8acd2fb-6de6-4ea4-9bfb-0daad9b2ed7b',
'd5cba263-cda7-440c-a21d-134fb5d334f6',
'de82be66-3b9b-4612-9572-61e3c6039013',
'2f243f46-34ce-4d50-a931-c6f8a67eb758',
'498ec3ff-ef27-4729-9703-a5aac48d5789',
'f509af83-6e71-462f-901f-bcb073f6773b',
'c5049184-ded4-49d0-8e14-bffefc40a223',
'4597c92e-128c-44de-abd2-a1d88d163b0c',
'5293ef3c-37bb-4d6f-8d43-802c57560881',
'd16d8124-ce0c-4869-9efb-98700332feab'
);
UPDATE schema_meta SET meta_value='0011' WHERE meta_key='schema_version';
@@ -0,0 +1,43 @@
INSERT INTO instruments (
instrument_uid, figi, ticker, class_code, name, lot, min_price_increment, currency,
enabled, fund_type, expected_commission_bps_per_side, free_order_limit_per_day,
quarantine, quarantine_reason, exclude_reason, updated_at
) VALUES
('e8acd2fb-6de6-4ea4-9bfb-0daad9b2ed7b', 'TCS60A1039N1', 'TBRU@', 'SPBRU', 'Российские облигации', 1, 0.01, 'RUB', 1, 'bonds', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('d5cba263-cda7-440c-a21d-134fb5d334f6', 'TCS10A107563', 'TDIV@', 'SPBRU', 'Дивидендные акции', 1, 0.01, 'RUB', 1, 'equity', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('de82be66-3b9b-4612-9572-61e3c6039013', 'TCS80A101X50', 'TGLD@', 'SPBRU', 'Золото', 1, 0.01, 'RUB', 1, 'commodity', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('2f243f46-34ce-4d50-a931-c6f8a67eb758', 'TCS20A107597', 'TLCB@', 'SPBRU', 'Локальные валютные облигации', 1, 0.01, 'RUB', 1, 'bonds', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('498ec3ff-ef27-4729-9703-a5aac48d5789', 'TCS70A106DL2', 'TMON@', 'SPBRU', 'Денежный рынок', 1, 0.01, 'RUB', 1, 'money_market', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('f509af83-6e71-462f-901f-bcb073f6773b', 'TCS60A101X76', 'TMOS@', 'SPBRU', 'Крупнейшие компании РФ', 1, 0.01, 'RUB', 1, 'equity', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('c5049184-ded4-49d0-8e14-bffefc40a223', 'TCS70A10A1L8', 'TOFZ@', 'SPBRU', 'Т-Капитал ОФЗ', 1, 0.01, 'RUB', 1, 'bonds', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('4597c92e-128c-44de-abd2-a1d88d163b0c', 'TCSM25708WX3', 'TPAY', 'TQBR', 'Пассивный доход', 1, 0.01, 'RUB', 1, 'bonds', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('5293ef3c-37bb-4d6f-8d43-802c57560881', 'TCS20A10B0G9', 'TRND@', 'SPBRU', 'Трендовые акции', 1, 0.01, 'RUB', 1, 'equity', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3)),
('d16d8124-ce0c-4869-9efb-98700332feab', 'TCS60A1011U5', 'TRUR@', 'SPBRU', 'Вечный портфель', 1, 0.01, 'RUB', 1, 'mixed', 0, 15, 0, NULL, NULL, UTC_TIMESTAMP(3))
ON DUPLICATE KEY UPDATE
figi=VALUES(figi),
ticker=VALUES(ticker),
class_code=VALUES(class_code),
name=VALUES(name),
lot=VALUES(lot),
min_price_increment=VALUES(min_price_increment),
currency=VALUES(currency),
enabled=VALUES(enabled),
fund_type=VALUES(fund_type),
expected_commission_bps_per_side=VALUES(expected_commission_bps_per_side),
free_order_limit_per_day=VALUES(free_order_limit_per_day),
quarantine=VALUES(quarantine),
quarantine_reason=VALUES(quarantine_reason),
exclude_reason=VALUES(exclude_reason),
updated_at=UTC_TIMESTAMP(3);
INSERT INTO risk_events (ts, severity, event_type, instrument_uid, message, raw_context_json)
VALUES (
UTC_TIMESTAMP(3),
'INFO',
'tcapital_whitelist_seeded',
NULL,
'Seeded verified T-Capital ETF whitelist for monitoring',
'{"tickers":["TBRU@","TDIV@","TGLD@","TLCB@","TMON@","TMOS@","TOFZ@","TPAY","TRND@","TRUR@"],"free_order_limit_per_day":15}'
);
UPDATE schema_meta SET meta_value='0012' WHERE meta_key='schema_version';