Compare commits
14 Commits
750860ee74
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 57e723db65 | |||
| f46ec2abde | |||
|
a055e0606f
|
|||
| 6c2f868d21 | |||
| e8f25d06e7 | |||
| 4dfc2fe263 | |||
| 6bd6585c1f | |||
| b7c13c536f | |||
|
55e897909d
|
|||
| 7baa7395af | |||
|
4b37dc31a3
|
|||
| e693de933f | |||
|
3f7f5ac9cb
|
|||
| 3b68d24677 |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ Common formats:
|
|||||||
| `TINVEST_RETRY_COUNT` | `3` | Number of T-Invest SDK attempts. |
|
| `TINVEST_RETRY_COUNT` | `3` | Number of T-Invest SDK attempts. |
|
||||||
| `TINVEST_RETRY_BACKOFF_SEC` | `2` | Initial exponential backoff in seconds. |
|
| `TINVEST_RETRY_BACKOFF_SEC` | `2` | Initial exponential backoff in seconds. |
|
||||||
| `TINVEST_USE_SANDBOX` | `false` | Compatibility guard; valid only with `APP_MODE=sandbox`. |
|
| `TINVEST_USE_SANDBOX` | `false` | Compatibility guard; valid only with `APP_MODE=sandbox`. |
|
||||||
| `TINVEST_TRADING_CALENDAR_EXCHANGE` | `MOEX` | Exchange calendar used to load trading days. |
|
| `TINVEST_TRADING_CALENDAR_EXCHANGE` | `MOEX` | Deprecated compatibility setting; historical feature calendars are derived from loaded daily candles. |
|
||||||
|
|
||||||
### DB
|
### DB
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ APP_MODE=backtest go run ./cmd/bot
|
|||||||
| `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. |
|
| `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. |
|
||||||
| `TINVEST_RETRY_BACKOFF_SEC` | целое число секунд | `2` | рекомендуется `>= 0` | Начальный интервал exponential backoff для SDK-вызовов T-Invest. Больше значение снижает частоту повторов при сбоях, но дольше задерживает окончательную ошибку. |
|
| `TINVEST_RETRY_BACKOFF_SEC` | целое число секунд | `2` | рекомендуется `>= 0` | Начальный интервал exponential backoff для SDK-вызовов T-Invest. Больше значение снижает частоту повторов при сбоях, но дольше задерживает окончательную ошибку. |
|
||||||
| `TINVEST_USE_SANDBOX` | `true` или `false` | `false` | boolean; разрешено только при `APP_MODE=sandbox` | Защитный флаг совместимости. В `live_readonly` и `live_trade` запрещён валидацией, чтобы случайно не подменить фактическую среду исполнения. |
|
| `TINVEST_USE_SANDBOX` | `true` или `false` | `false` | boolean; разрешено только при `APP_MODE=sandbox` | Защитный флаг совместимости. В `live_readonly` и `live_trade` запрещён валидацией, чтобы случайно не подменить фактическую среду исполнения. |
|
||||||
| `TINVEST_TRADING_CALENDAR_EXCHANGE` | код биржевого календаря, например `MOEX` | `MOEX` | пустое значение заменяется на `MOEX` | Календарь торговых дней для загрузки истории и расчёта торгового цикла. |
|
| `TINVEST_TRADING_CALENDAR_EXCHANGE` | устаревший код биржевого календаря, например `MOEX` | `MOEX` | пустое значение заменяется на `MOEX` | Оставлен для совместимости; исторический календарь признаков строится из загруженных дневных свечей. |
|
||||||
|
|
||||||
### DB
|
### DB
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ go 1.26.2
|
|||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v11 v11.4.1
|
github.com/caarlos0/env/v11 v11.4.1
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0
|
github.com/cenkalti/backoff/v4 v4.3.0
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
|
||||||
github.com/go-sql-driver/mysql v1.10.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/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/russianinvestments/invest-api-go-sdk v1.40.1
|
github.com/russianinvestments/invest-api-go-sdk v1.40.1
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
github.com/testcontainers/testcontainers-go v0.42.0
|
github.com/testcontainers/testcontainers-go v0.43.0
|
||||||
github.com/testcontainers/testcontainers-go/modules/mariadb v0.42.0
|
github.com/testcontainers/testcontainers-go/modules/mariadb v0.43.0
|
||||||
google.golang.org/grpc v1.81.1
|
google.golang.org/grpc v1.82.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ require (
|
|||||||
github.com/magiconair/properties v1.8.10 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/go-archive v0.2.0 // indirect
|
github.com/moby/go-archive v0.2.0 // indirect
|
||||||
github.com/moby/moby/api v1.54.1 // indirect
|
github.com/moby/moby/api v1.54.2 // indirect
|
||||||
github.com/moby/moby/client v0.4.0 // indirect
|
github.com/moby/moby/client v0.4.0 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
@@ -57,7 +56,7 @@ require (
|
|||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
|
github.com/shirou/gopsutil/v4 v4.26.5 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
@@ -68,12 +67,12 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric 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/trace v1.43.0 // indirect
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
golang.org/x/net v0.55.0 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
golang.org/x/oauth2 v0.36.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sys v0.46.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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/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 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v6 v6.0.1/go.mod h1:5WCmPelT2zwAaNETjGJVKHDnZvjQdPsGeHHwm5lIPPI=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
@@ -93,6 +93,8 @@ 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/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 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
|
||||||
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
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=
|
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
|
||||||
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
|
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
|
||||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
@@ -123,6 +125,8 @@ github.com/russianinvestments/invest-api-go-sdk v1.40.1 h1:EZ9mA5fTlyspH8urdAMFX
|
|||||||
github.com/russianinvestments/invest-api-go-sdk v1.40.1/go.mod h1:rOu2P3GMTQEkQxRpQfp+wK5k71c3SUDHIke3Ijr8cOU=
|
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 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||||
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
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=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
@@ -133,8 +137,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
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 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
|
||||||
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
|
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 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.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=
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
@@ -155,10 +163,10 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
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 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.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -166,19 +174,21 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
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/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
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-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
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-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260615183401-62b3387ff324 h1:9HZDLIdYBJXAnaFOr9WHrKVycfpY+75s9HGadC0305A=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
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 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -251,9 +251,9 @@ func (s *Scheduler) prepareSignals(ctx context.Context, now time.Time) error {
|
|||||||
if err := s.svc.MarketData.BackfillDaily(ctx, instrumentsList, dailyFrom, tradeDate); err != nil {
|
if err := s.svc.MarketData.BackfillDaily(ctx, instrumentsList, dailyFrom, tradeDate); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tradingDays, err := s.svc.Gateway.GetTradingDays(ctx, s.cfg.TradingCalendarExchange, dailyFrom, tradeDate)
|
tradingDays, err := s.tradingDaysFromDailyCandles(ctx, instrumentsList, dailyFrom, tradeDate.AddDate(0, 0, -1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load trading calendar %s: %w", s.cfg.TradingCalendarExchange, err)
|
return err
|
||||||
}
|
}
|
||||||
s.svc.Features = s.svc.Features.WithTradingDays(tradingDays)
|
s.svc.Features = s.svc.Features.WithTradingDays(tradingDays)
|
||||||
minuteFrom := s.cfg.EntryWindowStart.On(tradeDate.AddDate(0, 0, -s.cfg.IntervalVolumeLookbackDays), s.cfg.Location)
|
minuteFrom := s.cfg.EntryWindowStart.On(tradeDate.AddDate(0, 0, -s.cfg.IntervalVolumeLookbackDays), s.cfg.Location)
|
||||||
@@ -297,6 +297,33 @@ func (s *Scheduler) prepareSignals(ctx context.Context, now time.Time) error {
|
|||||||
return s.transitionTo(ctx, domain.StateWaitEntryWindow)
|
return s.transitionTo(ctx, domain.StateWaitEntryWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Scheduler) tradingDaysFromDailyCandles(ctx context.Context, instrumentsList []domain.Instrument, from, to time.Time) ([]time.Time, error) {
|
||||||
|
if to.Before(from) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
seen := make(map[time.Time]struct{})
|
||||||
|
for _, instrument := range instrumentsList {
|
||||||
|
if !instrument.Enabled || instrument.Quarantine {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candles, err := s.svc.Repo.ListDailyCandles(ctx, instrument.InstrumentUID, from, to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load daily candles for trading calendar %s: %w", instrument.Ticker, err)
|
||||||
|
}
|
||||||
|
for _, candle := range candles {
|
||||||
|
seen[tradingDate(candle.TradeDate)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
days := make([]time.Time, 0, len(seen))
|
||||||
|
for day := range seen {
|
||||||
|
days = append(days, day)
|
||||||
|
}
|
||||||
|
sort.Slice(days, func(i, j int) bool {
|
||||||
|
return days[i].Before(days[j])
|
||||||
|
})
|
||||||
|
return days, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s Scheduler) generateInstrumentSignal(ctx context.Context, tradeDate time.Time, openPositionCount int, instrument domain.Instrument) (signalCandidate, error) {
|
func (s Scheduler) generateInstrumentSignal(ctx context.Context, tradeDate time.Time, openPositionCount int, instrument domain.Instrument) (signalCandidate, error) {
|
||||||
book, err := s.svc.MarketData.LatestQuote(ctx, instrument.InstrumentUID, s.cfg.QuoteDepth, s.cfg.MaxQuoteAge)
|
book, err := s.svc.MarketData.LatestQuote(ctx, instrument.InstrumentUID, s.cfg.QuoteDepth, s.cfg.MaxQuoteAge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1034,7 +1061,7 @@ func (s *Scheduler) recordInfrastructureFailure(ctx context.Context, err error)
|
|||||||
now := s.nowUTC()
|
now := s.nowUTC()
|
||||||
if s.infraFailedSince.IsZero() {
|
if s.infraFailedSince.IsZero() {
|
||||||
s.infraFailedSince = now
|
s.infraFailedSince = now
|
||||||
s.logWarn("infrastructure check failed; waiting for outage threshold", "err", err, "threshold", s.cfg.APIOutageHalt)
|
s.logWarn("infrastructure check failed; pausing scheduler step", "err", err, "threshold", s.cfg.APIOutageHalt)
|
||||||
if s.svc.Repo != nil {
|
if s.svc.Repo != nil {
|
||||||
if insertErr := s.svc.Repo.InsertRiskEvent(ctx, domain.RiskEvent{
|
if insertErr := s.svc.Repo.InsertRiskEvent(ctx, domain.RiskEvent{
|
||||||
TS: now,
|
TS: now,
|
||||||
@@ -1048,10 +1075,12 @@ func (s *Scheduler) recordInfrastructureFailure(ctx context.Context, err error)
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.cfg.APIOutageHalt <= 0 || now.Sub(s.infraFailedSince) >= s.cfg.APIOutageHalt {
|
elapsed := now.Sub(s.infraFailedSince)
|
||||||
return err
|
if s.cfg.APIOutageHalt > 0 && elapsed >= s.cfg.APIOutageHalt {
|
||||||
|
s.logWarn("infrastructure check still failing after outage threshold; continuing soft pause", "err", err, "elapsed", elapsed, "threshold", s.cfg.APIOutageHalt)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
s.logWarn("infrastructure check still failing", "err", err, "elapsed", now.Sub(s.infraFailedSince), "threshold", s.cfg.APIOutageHalt)
|
s.logWarn("infrastructure check still failing; pausing scheduler step", "err", err, "elapsed", elapsed, "threshold", s.cfg.APIOutageHalt)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,7 +1399,6 @@ func (s Scheduler) unknownBrokerState(ctx context.Context, portfolio domain.Port
|
|||||||
func isHardHaltPreTradeReason(reason string) bool {
|
func isHardHaltPreTradeReason(reason string) bool {
|
||||||
switch reason {
|
switch reason {
|
||||||
case "database_unavailable",
|
case "database_unavailable",
|
||||||
"server_time_unavailable",
|
|
||||||
"server_clock_drift_too_high",
|
"server_clock_drift_too_high",
|
||||||
"unknown_broker_order",
|
"unknown_broker_order",
|
||||||
"unknown_broker_position",
|
"unknown_broker_position",
|
||||||
|
|||||||
@@ -115,6 +115,55 @@ func TestClockDriftHardLimitHaltsImmediately(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerTimeUnavailableSoftPausesAfterOutageThreshold(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
gateway := tinvest.NewFakeGateway()
|
||||||
|
gateway.ServerTimeError = context.DeadlineExceeded
|
||||||
|
repo := testutil.NewMemoryRepository()
|
||||||
|
now := time.Date(2026, 6, 18, 9, 0, 0, 0, time.UTC)
|
||||||
|
s := &Scheduler{
|
||||||
|
clock: fixedClock{now: now},
|
||||||
|
cfg: Config{
|
||||||
|
Mode: domain.ModeSandbox,
|
||||||
|
MaxClockDrift: 2 * time.Second,
|
||||||
|
APIOutageHalt: 180 * time.Second,
|
||||||
|
},
|
||||||
|
svc: Services{
|
||||||
|
Repo: repo,
|
||||||
|
Gateway: gateway,
|
||||||
|
Risk: risk.NewManager(repo, risk.ManagerConfig{}),
|
||||||
|
Notifier: &countNotifier{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.checkInfrastructure(ctx); err != nil {
|
||||||
|
t.Fatalf("first infrastructure check err=%v, want soft pause", err)
|
||||||
|
}
|
||||||
|
if repo.Halted {
|
||||||
|
t.Fatalf("system halted on first server time outage: reason=%q", repo.HaltReason)
|
||||||
|
}
|
||||||
|
if len(repo.RiskEvents) != 1 || repo.RiskEvents[0].EventType != "infrastructure_outage_started" {
|
||||||
|
t.Fatalf("risk events=%+v", repo.RiskEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.clock = fixedClock{now: now.Add(5 * time.Minute)}
|
||||||
|
if err := s.checkInfrastructure(ctx); err != nil {
|
||||||
|
t.Fatalf("post-threshold infrastructure check err=%v, want soft pause", err)
|
||||||
|
}
|
||||||
|
if repo.Halted {
|
||||||
|
t.Fatalf("system halted after server time outage threshold: reason=%q", repo.HaltReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway.ServerTimeError = nil
|
||||||
|
gateway.ServerTime = now.Add(5 * time.Minute)
|
||||||
|
if err := s.checkInfrastructure(ctx); err != nil {
|
||||||
|
t.Fatalf("recovered infrastructure check err=%v", err)
|
||||||
|
}
|
||||||
|
if !s.infraFailedSince.IsZero() {
|
||||||
|
t.Fatalf("infraFailedSince=%s, want zero after recovery", s.infraFailedSince)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStepIsIdempotentAfterSignalPreparation(t *testing.T) {
|
func TestStepIsIdempotentAfterSignalPreparation(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
repo := testutil.NewMemoryRepository()
|
repo := testutil.NewMemoryRepository()
|
||||||
@@ -602,6 +651,45 @@ func TestPreTradeClockDriftBreachHalts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreTradeServerTimeUnavailableRejectsWithoutHalting(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repo := testutil.NewMemoryRepository()
|
||||||
|
now := time.Date(2026, 6, 8, 18, 20, 0, 0, time.UTC)
|
||||||
|
gateway := tinvest.NewFakeGateway()
|
||||||
|
gateway.ServerTimeError = context.DeadlineExceeded
|
||||||
|
notifier := &countNotifier{}
|
||||||
|
s := Scheduler{
|
||||||
|
cfg: Config{
|
||||||
|
Mode: domain.ModeSandbox,
|
||||||
|
Location: time.UTC,
|
||||||
|
MaxClockDrift: 2 * time.Second,
|
||||||
|
},
|
||||||
|
svc: Services{
|
||||||
|
Repo: repo,
|
||||||
|
Gateway: gateway,
|
||||||
|
Risk: risk.NewManager(repo, risk.ManagerConfig{}),
|
||||||
|
Notifier: notifier,
|
||||||
|
AccountIDHash: "hash",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := s.preTradeCheck(ctx, now, "uid", domain.Portfolio{
|
||||||
|
Equity: decimal.NewFromInt(10000),
|
||||||
|
Cash: decimal.NewFromInt(10000),
|
||||||
|
}, 0, false, domain.TradingStatusNormal, now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err=%v, want reject without hard halt error", err)
|
||||||
|
}
|
||||||
|
if result.Allowed || result.Reason != "server_time_unavailable" {
|
||||||
|
t.Fatalf("result=%+v, want server_time_unavailable reject", result)
|
||||||
|
}
|
||||||
|
if repo.Halted || repo.HaltReason != "" {
|
||||||
|
t.Fatalf("halted=%v reason=%q, want no halt", repo.Halted, repo.HaltReason)
|
||||||
|
}
|
||||||
|
if notifier.alerts != 0 {
|
||||||
|
t.Fatalf("alerts=%d, want 0", notifier.alerts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreTradeUsesPhaseDeadlineForMinTimeToClose(t *testing.T) {
|
func TestPreTradeUsesPhaseDeadlineForMinTimeToClose(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
repo := testutil.NewMemoryRepository()
|
repo := testutil.NewMemoryRepository()
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type FakeGateway struct {
|
|||||||
Portfolio domain.Portfolio
|
Portfolio domain.Portfolio
|
||||||
Operations []domain.Operation
|
Operations []domain.Operation
|
||||||
ServerTime time.Time
|
ServerTime time.Time
|
||||||
|
ServerTimeError error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFakeGateway() *FakeGateway {
|
func NewFakeGateway() *FakeGateway {
|
||||||
@@ -239,6 +240,9 @@ func (f *FakeGateway) GetOperations(_ context.Context, _ string, from, to time.T
|
|||||||
func (f *FakeGateway) GetServerTime(context.Context) (time.Time, error) {
|
func (f *FakeGateway) GetServerTime(context.Context) (time.Time, error) {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
|
if f.ServerTimeError != nil {
|
||||||
|
return time.Time{}, f.ServerTimeError
|
||||||
|
}
|
||||||
if f.ServerTime.IsZero() {
|
if f.ServerTime.IsZero() {
|
||||||
return time.Now().UTC(), nil
|
return time.Now().UTC(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user