updated READMEs
Deploy / Test, build and deploy (push) Successful in 54s

This commit is contained in:
2026-06-09 21:15:18 +00:00
parent 4dec14f57c
commit f887f29be2
4 changed files with 506 additions and 176 deletions
+9
View File
@@ -12,6 +12,7 @@ TINVEST_REQUEST_TIMEOUT_SEC=10
TINVEST_RETRY_COUNT=3 TINVEST_RETRY_COUNT=3
TINVEST_RETRY_BACKOFF_SEC=2 TINVEST_RETRY_BACKOFF_SEC=2
TINVEST_USE_SANDBOX=false TINVEST_USE_SANDBOX=false
TINVEST_TRADING_CALENDAR_EXCHANGE=MOEX
DB_DSN=bot:change-me@tcp(db.example.internal:3306)/overnight_bot?parseTime=true&loc=UTC&multiStatements=true DB_DSN=bot:change-me@tcp(db.example.internal:3306)/overnight_bot?parseTime=true&loc=UTC&multiStatements=true
DB_MAX_OPEN_CONNS=20 DB_MAX_OPEN_CONNS=20
@@ -29,10 +30,14 @@ TELEGRAM_NOTIFY_REPORT=true
STRATEGY_ROLLING_SHORT=60 STRATEGY_ROLLING_SHORT=60
STRATEGY_ROLLING_LONG=252 STRATEGY_ROLLING_LONG=252
STRATEGY_EWMA_LAMBDA=0.08 STRATEGY_EWMA_LAMBDA=0.08
STRATEGY_ALLOCATION_METHOD=equal_weight
STRATEGY_MIN_TSTAT_60=1.25 STRATEGY_MIN_TSTAT_60=1.25
STRATEGY_MIN_WIN_RATE_60=0.55 STRATEGY_MIN_WIN_RATE_60=0.55
STRATEGY_MIN_NET_EDGE_BPS=10 STRATEGY_MIN_NET_EDGE_BPS=10
STRATEGY_RISK_BUFFER_BPS=5 STRATEGY_RISK_BUFFER_BPS=5
STRATEGY_EXPECTED_ENTRY_SLIPPAGE_BPS=8
STRATEGY_EXPECTED_EXIT_SLIPPAGE_BPS=8
STRATEGY_INTERVAL_VOLUME_LOOKBACK_DAYS=20
STRATEGY_MAX_POSITIONS=5 STRATEGY_MAX_POSITIONS=5
EXEC_ENTRY_SIGNAL_TIME=18:10:00 EXEC_ENTRY_SIGNAL_TIME=18:10:00
@@ -44,6 +49,7 @@ EXEC_EXIT_NOT_BEFORE=10:03:00
EXEC_EXIT_WINDOW_START=10:05:00 EXEC_EXIT_WINDOW_START=10:05:00
EXEC_EXIT_WINDOW_END=10:25:00 EXEC_EXIT_WINDOW_END=10:25:00
EXEC_HARD_EXIT_DEADLINE=10:45:00 EXEC_HARD_EXIT_DEADLINE=10:45:00
EXEC_MARKET_CLOSE=18:50:00
EXEC_MIN_TIME_TO_CLOSE_SEC=90 EXEC_MIN_TIME_TO_CLOSE_SEC=90
EXEC_ALLOW_MARKET_ORDERS=false EXEC_ALLOW_MARKET_ORDERS=false
EXEC_MAX_ENTRY_ORDER_ATTEMPTS=3 EXEC_MAX_ENTRY_ORDER_ATTEMPTS=3
@@ -70,6 +76,9 @@ RISK_COMMISSION_TOLERANCE_RUB=0.01
RISK_CASH_USAGE_BUFFER=0.95 RISK_CASH_USAGE_BUFFER=0.95
RISK_RISK_BUDGET_PER_INSTRUMENT_PCT=0.005 RISK_RISK_BUDGET_PER_INSTRUMENT_PCT=0.005
RISK_MIN_ORDER_NOTIONAL_RUB=1000 RISK_MIN_ORDER_NOTIONAL_RUB=1000
RISK_SIZE_REDUCTION_WINDOW_TRADES=20
RISK_SIZE_REDUCTION_FACTOR=0.5
RISK_SIZE_REDUCTION_TRIGGER_BPS=-10
LIQ_MIN_ADV_RUB=5000000 LIQ_MIN_ADV_RUB=5000000
LIQ_MAX_PARTICIPATION_RATE=0.01 LIQ_MAX_PARTICIPATION_RATE=0.01
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Valentin Popov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+183 -176
View File
@@ -1,6 +1,14 @@
# Overnight Trading Bot # Overnight Trading Bot
Go-бот для overnight-стратегии `close -> next open` на фондах T-Капитала через T-Invest API. [English](README.md) / [Русский](README.ru.md)
A Go research bot for studying a `close -> next open` overnight strategy on T-Capital funds through the T-Invest API.
The project is intended for statistical research, backtesting, paper trading, sandbox checks, and tightly controlled live-readonly/live-trade experiments. It is not designed for market manipulation, price impact, or evading legal requirements. Orders must have a genuine execution intent, use limit-only execution, and pass liquidity, spread, commission, reconciliation, and risk controls.
One research thread is the overnight/intraday returns anomaly discussed by Bruce Knuteson in [“Nothing to See Here: How to Say It When You Need to”](https://ssrn.com/abstract=4619084) (`ssrn-4619084`).
License: [MIT](LICENSE).
## Quick Start ## Quick Start
@@ -10,172 +18,189 @@ make test
APP_MODE=backtest go run ./cmd/bot APP_MODE=backtest go run ./cmd/bot
``` ```
Для daemon-режимов (`paper`, `sandbox`, `live_readonly`, `live_trade`) нужен `DB_DSN` MariaDB/MySQL. `live_trade` дополнительно требует `LIVE_TRADE_ACK=I_ACCEPT_RISK` и выполненные pre-flight условия из секции `LIVE`. Daemon modes (`paper`, `sandbox`, `live_readonly`, `live_trade`) require a MariaDB/MySQL `DB_DSN`. `live_trade` also requires `LIVE_TRADE_ACK=I_ACCEPT_RISK` and the live pre-flight checks listed below.
## Environment Variables ## Modes
Конфигурация читается из ENV через `.env`. Если значение не парсится в нужный тип, бот падает на старте с ошибкой `load ENV config`. | Mode | Purpose |
| --- | --- |
| `backtest` | Offline research mode. Does not require a database or T-Invest credentials when run through `cmd/bot`. |
| `paper` | Simulated orders. Without `TINVEST_TOKEN`, uses a fake gateway; with a token, uses real market data/status and simulated execution. |
| `sandbox` | T-Invest sandbox API. Requires token and account id. |
| `live_readonly` | Live API access without broker order placement. Used for observation and reconciliation. |
| `live_trade` | Real limit-order trading. Guarded by explicit risk acknowledgement and pre-flight requirements. |
Общие форматы: ## Configuration
- Время указывается в формате `HH:MM:SS` и трактуется в `Europe/Moscow`. Configuration is read from environment variables, usually through `.env`. If a value cannot be parsed, startup fails with `load ENV config`.
- Доли указываются десятичной дробью: `0.10` означает 10%, `0.005` означает 0.5%.
- `bps` - базисные пункты: `10` означает 0.10%. Common formats:
- Boolean-значения: `true` или `false`.
- В колонке "Дефолт" указан дефолт из кода. Если дефолта в коде нет, но в `.env.example` есть пример, это отмечено отдельно. - Times use `HH:MM:SS` and are interpreted in `Europe/Moscow`.
- Границы делятся на жёсткую валидацию старта и практические ограничения. Там, где валидации пока нет, указано рекомендуемое значение. - Percentages are decimal fractions: `0.10` means 10%, `0.005` means 0.5%.
- `bps` means basis points: `10` means 0.10%.
- Boolean values are `true` or `false`.
- Defaults below match `.env.example` and the code defaults.
### APP ### APP
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `APP_MODE` | `backtest`, `paper`, `sandbox`, `live_readonly`, `live_trade` | нет, в `.env.example`: `paper` | обязательна; только перечисленные значения | Режим работы. `backtest` не требует БД и API в `cmd/bot`; `paper` без `TINVEST_TOKEN` использует fake gateway, а с токеном берёт реальные market data/status через T-Invest при симулированных заявках; `sandbox`, `live_readonly`, `live_trade` подключаются к T-Invest API; `live_trade` может отправлять брокерские заявки. | | `APP_MODE` | `paper` | One of `backtest`, `paper`, `sandbox`, `live_readonly`, `live_trade`; required by the code. |
| `APP_TIMEZONE` | `Europe/Moscow` | `Europe/Moscow` | жёстко только `Europe/Moscow` | Таймзона расписания торговых окон. Изменить нельзя без изменения валидации. | | `APP_TIMEZONE` | `Europe/Moscow` | Trading schedule timezone; validation currently allows only `Europe/Moscow`. |
| `APP_LOG_LEVEL` | `debug`, `info`, `warn`, `warning`, `error` | `info` | неизвестное значение трактуется как `info` | Уровень JSON-логов. Ниже уровень - больше диагностических записей. | | `APP_LOG_LEVEL` | `info` | JSON log level: `debug`, `info`, `warn`, `warning`, `error`. |
| `APP_HEALTHCHECK_ADDR` | HTTP listen address, например `:3300` или `127.0.0.1:3300` | `:3300` | без отдельной валидации | Адрес `/health` и `/ready`; CLI `-healthcheck` по умолчанию проверяет `/ready`. При изменении меняется порт или интерфейс healthcheck-сервера. | | `APP_HEALTHCHECK_ADDR` | `:3300` | HTTP address for `/health` and `/ready`. |
| `APP_SHUTDOWN_TIMEOUT_SEC` | целое число секунд | `30` | должно быть `> 0` | Таймаут graceful shutdown для HTTP healthcheck при остановке. | | `APP_SHUTDOWN_TIMEOUT_SEC` | `30` | Graceful shutdown timeout in seconds. |
### TINVEST ### TINVEST
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `TINVEST_TOKEN` | токен T-Invest API | пусто | обязателен для `sandbox`, `live_readonly`, `live_trade`; опционален для `paper` | Доступ к реальному или sandbox API. В `paper` без токена используется fake gateway, с токеном - реальные market data и симулированные заявки. | | `TINVEST_TOKEN` | empty | API token. Required for `sandbox`, `live_readonly`, and `live_trade`; optional in `paper`. |
| `TINVEST_ACCOUNT_ID` | идентификатор брокерского счёта | пусто | обязателен для `sandbox`, `live_readonly`, `live_trade` | Счёт для портфеля, заявок и сверки. Для API-режимов бот падает на старте, если account id не указан. | | `TINVEST_ACCOUNT_ID` | empty | Broker account id. Required for API-backed modes. |
| `TINVEST_ENDPOINT` | gRPC endpoint T-Invest, обычно `host:port` | `invest-public-api.tinkoff.ru:443` | строка; валидации формата нет | Endpoint для API. В `sandbox` код принудительно использует sandbox endpoint. | | `TINVEST_ENDPOINT` | `invest-public-api.tinkoff.ru:443` | T-Invest gRPC endpoint; sandbox mode overrides this where needed. |
| `TINVEST_APP_NAME` | имя приложения | `overnight-trading-bot` | строка | Передаётся в SDK как имя клиента. Меняет идентификацию приложения на стороне API/логов. | | `TINVEST_APP_NAME` | `overnight-trading-bot` | Application/client name passed to the SDK. |
| `TINVEST_REQUEST_TIMEOUT_SEC` | целое число секунд | `10` | должно быть `> 0` | Таймаут API-запросов к T-Invest, включая retry-последовательность. Меньше значение быстрее освобождает торговый цикл при зависшем API, но повышает шанс timeout на медленной сети. | | `TINVEST_REQUEST_TIMEOUT_SEC` | `10` | API request timeout, including retry sequences. |
| `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. | | `TINVEST_RETRY_COUNT` | `3` | Number of T-Invest SDK attempts. |
| `TINVEST_RETRY_BACKOFF_SEC` | целое число секунд | `2` | рекомендуется `>= 0` | Начальный интервал exponential backoff для SDK-вызовов T-Invest. Больше значение снижает частоту повторов при сбоях, но дольше задерживает окончательную ошибку. | | `TINVEST_RETRY_BACKOFF_SEC` | `2` | Initial exponential backoff in seconds. |
| `TINVEST_USE_SANDBOX` | `true` или `false` | `false` | boolean; разрешено только при `APP_MODE=sandbox` | Защитный флаг совместимости. В `live_readonly` и `live_trade` запрещён валидацией, чтобы случайно не подменить фактическую среду исполнения. | | `TINVEST_USE_SANDBOX` | `false` | Compatibility guard; valid only with `APP_MODE=sandbox`. |
| `TINVEST_TRADING_CALENDAR_EXCHANGE` | `MOEX` | Exchange calendar used to load trading days. |
### DB ### DB
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `DB_DSN` | MySQL/MariaDB DSN, например `bot:change-me@tcp(db.example.internal:3306)/overnight_bot?parseTime=true&loc=UTC&multiStatements=true` | нет, пример есть в `.env.example` | обязателен во всех режимах, кроме `backtest`; должен открываться драйвером MySQL | Подключение к БД. В БД хранятся инструменты, свечи, сигналы, заявки, позиции, состояния, события риска и отчёты. | | `DB_DSN` | example DSN | MySQL/MariaDB DSN. Required outside `backtest`. Stores instruments, candles, signals, orders, positions, risk events, and reports. |
| `DB_MAX_OPEN_CONNS` | целое число | `20` | валидации нет; `<= 0` для `database/sql` означает без лимита | Максимум открытых соединений с БД. Больше - выше параллелизм, но больше нагрузка на MariaDB. | | `DB_MAX_OPEN_CONNS` | `20` | Maximum open database connections. |
| `DB_MAX_IDLE_CONNS` | целое число | `5` | валидации нет; `<= 0` отключает idle pool | Размер пула простаивающих соединений. Больше - меньше переподключений, но больше удерживаемых соединений. | | `DB_MAX_IDLE_CONNS` | `5` | Idle connection pool size. |
| `DB_CONN_MAX_LIFETIME_MIN` | целое число минут | `30` | валидации нет; `<= 0` отключает лимит lifetime | Сколько живёт соединение до пересоздания. Меньше - чаще переподключения, больше - дольше используются старые соединения. | | `DB_CONN_MAX_LIFETIME_MIN` | `30` | Connection lifetime in minutes. |
| `DB_MIGRATIONS_AUTO_APPLY` | `true` или `false` | `true` | boolean | Автоматически применяет миграции при старте daemon-режима. `false` требует запускать миграции вручную через `cmd/migrate`. | | `DB_MIGRATIONS_AUTO_APPLY` | `true` | Apply migrations automatically at daemon startup. |
### TELEGRAM ### TELEGRAM
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `TELEGRAM_BOT_TOKEN` | токен Telegram-бота | пусто | строка | Если токен или `TELEGRAM_CHAT_ID` пустые, уведомления отключены и используется noop notifier. | | `TELEGRAM_BOT_TOKEN` | empty | Telegram bot token. Empty token or chat id disables notifications. |
| `TELEGRAM_CHAT_ID` | числовой chat id | `0` | `int64`; `0` отключает Telegram | Чат, куда отправляются уведомления. | | `TELEGRAM_CHAT_ID` | `0` | Telegram chat id; `0` disables Telegram. |
| `TELEGRAM_NOTIFY_INFO` | `true` или `false` | `true` | boolean | Включает информационные сообщения, например старт бота и события заявок. При переполнении очереди такие сообщения могут быть отброшены. | | `TELEGRAM_NOTIFY_INFO` | `true` | Send informational messages. |
| `TELEGRAM_NOTIFY_WARN` | `true` или `false` | `true` | boolean | Включает предупреждения. | | `TELEGRAM_NOTIFY_WARN` | `true` | Send warnings. |
| `TELEGRAM_NOTIFY_ALERT` | `true` или `false` | `true` | boolean | Включает alert-сообщения. Они считаются критичными для доставки и ждут место в очереди. | | `TELEGRAM_NOTIFY_ALERT` | `true` | Send critical alerts. |
| `TELEGRAM_NOTIFY_REPORT` | `true` или `false` | `true` | boolean | Включает дневные отчёты. | | `TELEGRAM_NOTIFY_REPORT` | `true` | Send daily reports. |
### STRATEGY ### STRATEGY
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `STRATEGY_ROLLING_SHORT` | количество торговых дней | `60` | рекомендуется `> 0` | Короткое окно статистики overnight-доходности. Больше - стабильнее оценка, но медленнее реакция; меньше - быстрее реакция, но больше шум. | | `STRATEGY_ROLLING_SHORT` | `60` | Short rolling window for overnight-return statistics. |
| `STRATEGY_ROLLING_LONG` | количество торговых дней | `252` | рекомендуется `>= STRATEGY_ROLLING_SHORT` и `> 0` | Длинное окно для проверки положительного долгосрочного edge и глубины backfill. Больше требует больше истории. | | `STRATEGY_ROLLING_LONG` | `252` | Long rolling window for persistent edge checks and backfill depth. |
| `STRATEGY_EWMA_LAMBDA` | дробь для EWMA | `0.08` | рабочий диапазон `(0, 1]`; вне диапазона EWMA-функция использует `0.08` | Вес новых наблюдений в EWMA. Больше - свежее движение влияет сильнее. | | `STRATEGY_EWMA_LAMBDA` | `0.08` | EWMA weight for fresh overnight observations. |
| `STRATEGY_ALLOCATION_METHOD` | `equal_weight` | `equal_weight` | сейчас поддерживается только `equal_weight` | Метод распределения капитала между выбранными сигналами. Текущая реализация делит лимит экспозиции поровну между выбранными инструментами. | | `STRATEGY_ALLOCATION_METHOD` | `equal_weight` | Capital allocation method; only `equal_weight` is currently supported. |
| `STRATEGY_MIN_TSTAT_60` | decimal t-stat | `1.25` | валидации нет; обычно `>= 0` | Минимальная статистическая значимость короткого edge. Выше - меньше входов, ниже - больше входов. | | `STRATEGY_MIN_TSTAT_60` | `1.25` | Minimum short-window t-statistic. |
| `STRATEGY_MIN_WIN_RATE_60` | доля прибыльных overnight-дней | `0.55` | рекомендуется `0..1` | Минимальная доля положительных overnight-наблюдений. Выше - строже фильтр сигналов. | | `STRATEGY_MIN_WIN_RATE_60` | `0.55` | Minimum positive overnight observation share. |
| `STRATEGY_MIN_NET_EDGE_BPS` | bps | `10` | валидации нет; обычно `>= 0` | Минимальный ожидаемый edge после издержек. Выше - меньше, но потенциально качественнее сигналы. | | `STRATEGY_MIN_NET_EDGE_BPS` | `10` | Minimum expected net edge after costs. |
| `STRATEGY_RISK_BUFFER_BPS` | bps | `5` | валидации нет; обычно `>= 0` | Дополнительная надбавка к ожидаемым издержкам в расчёте `NetEdgeBps`. Больше - консервативнее отбор. | | `STRATEGY_RISK_BUFFER_BPS` | `5` | Extra cost buffer subtracted from expected edge. |
| `STRATEGY_MAX_POSITIONS` | целое число позиций | `5` | `> 0` включает лимит; `<= 0` фактически отключает signal-level лимит | Максимум одновременно открытых позиций на уровне генерации сигналов. Больше - больше диверсификация и нагрузка на капитал. | | `STRATEGY_EXPECTED_ENTRY_SLIPPAGE_BPS` | `8` | Expected entry slippage used in signal costs and app-level backtest config. |
| `STRATEGY_EXPECTED_EXIT_SLIPPAGE_BPS` | `8` | Expected exit slippage used in signal costs and app-level backtest config. |
| `STRATEGY_INTERVAL_VOLUME_LOOKBACK_DAYS` | `20` | Lookback for entry/exit interval volume used by participation sizing. |
| `STRATEGY_MAX_POSITIONS` | `5` | Maximum selected/open positions at signal level. |
### EXEC ### EXEC
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `EXEC_ENTRY_SIGNAL_TIME` | `HH:MM:SS` | `18:10:00` | должно парситься как время | Время старта подготовки данных и генерации сигналов. | | `EXEC_ENTRY_SIGNAL_TIME` | `18:10:00` | Time to prepare data and generate entry signals. |
| `EXEC_ENTRY_WINDOW_START` | `HH:MM:SS` | `18:20:00` | `ENTRY_WINDOW_START < ENTRY_WINDOW_END <= NO_NEW_ENTRY_AFTER` | Начало окна постановки заявок на вход. Позже - меньше времени на исполнение входа. | | `EXEC_ENTRY_WINDOW_START` | `18:20:00` | Start of the entry order window. |
| `EXEC_ENTRY_WINDOW_END` | `HH:MM:SS` | `18:38:30` | см. правило окна входа | Конец активной постановки заявок на вход и market close для pre-trade проверки входа. | | `EXEC_ENTRY_WINDOW_END` | `18:38:30` | End of active entry order placement. |
| `EXEC_NO_NEW_ENTRY_AFTER` | `HH:MM:SS` | `18:38:30` | не раньше `EXEC_ENTRY_WINDOW_END` | После этого времени новые входы не ставятся, бот переходит в overnight hold. | | `EXEC_NO_NEW_ENTRY_AFTER` | `18:38:30` | No new entry orders after this time. |
| `EXEC_EXIT_WATCH_START` | `HH:MM:SS` | `09:50:00` | `EXIT_WATCH_START <= EXIT_NOT_BEFORE <= EXIT_WINDOW_START < EXIT_WINDOW_END <= HARD_EXIT_DEADLINE` | Начало утреннего наблюдения перед выходом. До `EXEC_EXIT_WINDOW_START` заявки на выход ещё не ставятся. | | `EXEC_EXIT_WATCH_START` | `09:50:00` | Morning watch start before exit. |
| `EXEC_EXIT_NOT_BEFORE` | `HH:MM:SS` | `10:03:00` | см. правило окна выхода; сейчас используется только валидацией | Нижняя граница "не выходить раньше". На текущий scheduler напрямую не влияет, потому что заявки начинаются с `EXEC_EXIT_WINDOW_START`. | | `EXEC_EXIT_NOT_BEFORE` | `10:03:00` | Lower bound for exit timing validation. |
| `EXEC_EXIT_WINDOW_START` | `HH:MM:SS` | `10:05:00` | см. правило окна выхода | Начало постановки заявок на выход. Раньше - больше шанс выйти быстрее, но ближе к открытию рынка. | | `EXEC_EXIT_WINDOW_START` | `10:05:00` | Start of exit order placement. |
| `EXEC_EXIT_WINDOW_END` | `HH:MM:SS` | `10:25:00` | см. правило окна выхода | Конец постановки новых exit-заявок, после него идёт мониторинг до hard deadline. | | `EXEC_EXIT_WINDOW_END` | `10:25:00` | End of new exit order placement. |
| `EXEC_HARD_EXIT_DEADLINE` | `HH:MM:SS` | `10:45:00` | не раньше `EXEC_EXIT_WINDOW_END` | Крайний срок выхода. После него запускаются reconciliation и report; незакрытая позиция ведёт к ручной обработке/HALT-сценарию. | | `EXEC_HARD_EXIT_DEADLINE` | `10:45:00` | Final exit deadline before reconciliation/reporting and HALT handling. |
| `EXEC_MIN_TIME_TO_CLOSE_SEC` | целое число секунд | `90` | `> 0` включает проверку; `<= 0` отключает | Минимальный запас до конца торгового окна для pre-trade. Больше - меньше риск ставить заявку слишком поздно. | | `EXEC_MARKET_CLOSE` | `18:50:00` | Market close reference for pre-trade time-to-close checks. |
| `EXEC_ALLOW_MARKET_ORDERS` | только `false` | `false` | жёстко должно быть `false` | Защитный флаг стратегии LIMIT-only. `true` запрещён валидацией. | | `EXEC_MIN_TIME_TO_CLOSE_SEC` | `90` | Minimum remaining time before close required for pre-trade checks. |
| `EXEC_MAX_ENTRY_ORDER_ATTEMPTS` | целое число | `3` | рекомендуется `>= 1` | Максимальное число постановок входной заявки в `MonitorUntil`: после polling/repost остаток отменяется к дедлайну окна входа. | | `EXEC_ALLOW_MARKET_ORDERS` | `false` | Must remain `false`; the strategy is limit-only. |
| `EXEC_MAX_EXIT_ORDER_ATTEMPTS` | целое число | `3` | рекомендуется `>= 1` | Максимальное число постановок выходной заявки в `MonitorUntil`: после polling/repost остаток отменяется к hard deadline. | | `EXEC_MAX_ENTRY_ORDER_ATTEMPTS` | `3` | Maximum entry repost attempts. |
| `EXEC_PASSIVE_IMPROVE_TICKS` | целое число тиков | `1` | отрицательное значение в pricing приравнивается к `0` | Насколько улучшать passive limit price от лучшего bid/ask. Больше - цена агрессивнее, но код не пересекает spread. | | `EXEC_MAX_EXIT_ORDER_ATTEMPTS` | `3` | Maximum exit repost attempts. |
| `EXEC_QUOTE_DEPTH` | целое число уровней стакана | `20` | `1..50` | Глубина стакана для оценки bid/ask и spread. Больше - больше данных из API, но для цены используется лучший уровень. | | `EXEC_PASSIVE_IMPROVE_TICKS` | `1` | Tick improvement from best bid/ask when pricing passive limits. |
| `EXEC_MAX_QUOTE_AGE_SEC` | целое число секунд | `3` | `> 0` включает проверку; `<= 0` отключает | Максимальный возраст котировки. Меньше - строже к свежести данных, но больше отказов `quote age exceeds`. | | `EXEC_QUOTE_DEPTH` | `20` | Order-book depth, validated in `1..50`. |
| `EXEC_ORDER_POLL_INTERVAL_MS` | целое число миллисекунд | `500` | рекомендуется `> 0` | Частота polling статусов заявок в `MonitorUntil`; также задаёт нижнюю границу интервала между repost-попытками. | | `EXEC_MAX_QUOTE_AGE_SEC` | `3` | Maximum acceptable quote age. |
| `EXEC_ORDER_POLL_INTERVAL_MS` | `500` | Order-status polling interval. |
### RISK ### RISK
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `RISK_USE_MARGIN` | только `false` | `false` | жёстко должно быть `false` | Защитный запрет маржинальной торговли. | | `RISK_USE_MARGIN` | `false` | Must remain `false`; margin is disabled. |
| `RISK_ALLOW_SHORT` | только `false` | `false` | жёстко должно быть `false` | Защитный запрет коротких позиций. | | `RISK_ALLOW_SHORT` | `false` | Must remain `false`; short positions are disabled. |
| `RISK_MAX_TOTAL_EXPOSURE_PCT` | доля equity | `0.50` | рекомендуется `0..1`; `0` фактически запрещает новые позиции | Общий лимит экспозиции, делится на выбранные инструменты при sizing. Больше - больше капитал в рынке. | | `RISK_MAX_TOTAL_EXPOSURE_PCT` | `0.50` | Total exposure cap as a fraction of equity. |
| `RISK_MAX_POSITION_PCT` | доля equity | `0.10` | рекомендуется `0..1`; `0` запрещает размер позиции | Максимальный размер одной позиции от equity. | | `RISK_MAX_POSITION_PCT` | `0.10` | Per-position exposure cap as a fraction of equity. |
| `RISK_MAX_DAILY_LOSS_PCT` | доля equity | `0.01` | `> 0` включает лимит; `<= 0` отключает | Дневной стоп по убытку. При достижении pre-trade отклоняет новые заявки. | | `RISK_MAX_DAILY_LOSS_PCT` | `0.01` | Daily loss stop. |
| `RISK_MAX_WEEKLY_LOSS_PCT` | доля equity | `0.03` | `> 0` включает лимит; `<= 0` отключает | Недельный стоп по убытку. | | `RISK_MAX_WEEKLY_LOSS_PCT` | `0.03` | Weekly loss stop. |
| `RISK_MAX_MONTHLY_DRAWDOWN_PCT` | доля equity | `0.07` | `> 0` включает лимит; `<= 0` отключает | Месячный лимит просадки. | | `RISK_MAX_MONTHLY_DRAWDOWN_PCT` | `0.07` | Monthly drawdown stop. |
| `RISK_MAX_OPEN_POSITIONS` | целое число | `5` | `> 0` включает лимит; `<= 0` отключает | Risk-level максимум открытых позиций перед постановкой заявки. | | `RISK_MAX_OPEN_POSITIONS` | `5` | Risk-level open-position cap. |
| `RISK_MAX_AVG_SLIPPAGE_BPS_10_TRADES` | bps | `15` | `> 0` включает лимит; `<= 0` отключает | Блокирует новые заявки при слишком большом среднем slippage за 10 сделок. | | `RISK_MAX_AVG_SLIPPAGE_BPS_10_TRADES` | `15` | Blocks new orders after excessive average slippage over 10 trades. |
| `RISK_API_OUTAGE_HALT_SEC` | целое число секунд | `180` | должно быть `> 0` | Если инфраструктурный/API сбой длится дольше, бот переводится в HALT. Больше - терпимее к сбоям, меньше - быстрее останавливается. | | `RISK_API_OUTAGE_HALT_SEC` | `180` | API/infrastructure outage duration before HALT. |
| `RISK_MAX_CLOCK_DRIFT_SEC` | целое число секунд | `2` | `> 0` включает проверку drift; `<= 0` отключает | Максимальный рассинхрон локального времени и серверного времени API в `/ready`. | | `RISK_MAX_CLOCK_DRIFT_SEC` | `2` | Maximum local/API server time drift accepted by readiness checks. |
| `RISK_RECONCILIATION_WINDOW_HOURS` | целое число часов | `72` | должно быть `> 0` | Глубина сверки последних заявок и операций брокера. Больше - больше история сверки, но тяжелее запросы. | | `RISK_RECONCILIATION_WINDOW_HOURS` | `72` | Broker/local reconciliation window. |
| `RISK_RECONCILIATION_SKEW_SEC` | целое число секунд | `10` | `>= 0` | Grace-window для только что отправленных локальных заявок: свежие in-flight orders не считаются diff, пока брокерский active-list догоняет запись. | | `RISK_RECONCILIATION_SKEW_SEC` | `10` | Grace period for fresh in-flight orders during reconciliation. |
| `RISK_COMMISSION_TOLERANCE_RUB` | сумма в рублях | `0.01` | `>= 0` | Допуск для reconciliation по расхождению локальной и брокерской комиссии. Ненулевая брокерская комиссия всё равно считается нарушением при `COMM_REQUIRE_ZERO_COMMISSION=true`. | | `RISK_COMMISSION_TOLERANCE_RUB` | `0.01` | Commission comparison tolerance. Non-zero broker commission still violates zero-commission policy when required. |
| `RISK_CASH_USAGE_BUFFER` | доля cash | `0.95` | рекомендуется `0..1`; `0` запрещает использование cash | Какая часть свободных денег может идти в sizing. Меньше - больше денежный буфер. | | `RISK_CASH_USAGE_BUFFER` | `0.95` | Fraction of available cash usable for sizing. |
| `RISK_RISK_BUDGET_PER_INSTRUMENT_PCT` | доля equity | `0.005` | рекомендуется `> 0` | Риск-бюджет на инструмент, используется вместе с оценкой неблагоприятного overnight-движения. Больше - крупнее позиции при прочих равных. | | `RISK_RISK_BUDGET_PER_INSTRUMENT_PCT` | `0.005` | Per-instrument risk budget used with adverse overnight move estimates. |
| `RISK_MIN_ORDER_NOTIONAL_RUB` | сумма в рублях | `1000` | `> 0` включает минимум; `<= 0` фактически отключает | Минимальный notional заявки. Если рассчитанная позиция меньше, сигнал отклоняется по sizing. | | `RISK_MIN_ORDER_NOTIONAL_RUB` | `1000` | Minimum order notional. |
| `RISK_SIZE_REDUCTION_WINDOW_TRADES` | `20` | Closed-trade window for realized-vs-expected edge checks. |
| `RISK_SIZE_REDUCTION_FACTOR` | `0.5` | Sizing multiplier applied after sustained edge deterioration. |
| `RISK_SIZE_REDUCTION_TRIGGER_BPS` | `-10` | Average error threshold that triggers size reduction. |
Если средний `realized_edge_bps - expected_net_edge_bps` по последним 20 закрытым сделкам ниже `-10 bps`, scheduler пишет `risk_event(WARN, size_reduction_rule_triggered)` и до восстановления качества режет sizing до `0.5x`. Если два таких окна по 20 сделок идут подряд в `live_trade`, бот автоматически переключает persisted/runtime mode в `live_readonly` и блокирует новые брокерские заявки до ручного вмешательства. If the average `realized_edge_bps - expected_net_edge_bps` over the configured closed-trade window is below the trigger, the scheduler emits a risk event and reduces sizing. Repeated deterioration in `live_trade` can switch the runtime mode to `live_readonly`.
### LIQ ### LIQ
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `LIQ_MIN_ADV_RUB` | сумма в рублях | `5000000` | рекомендуется `>= 0` | Минимальный средний дневной оборот за 20 дней. Выше - отсекает менее ликвидные фонды. | | `LIQ_MIN_ADV_RUB` | `5000000` | Minimum 20-day average daily RUB volume. |
| `LIQ_MAX_PARTICIPATION_RATE` | доля объёма | `0.01` | рекомендуется `0..1` | Максимальная доля объёма входного/выходного окна, которую может занять бот при sizing. Больше - крупнее позиции, но выше рыночное воздействие. | | `LIQ_MAX_PARTICIPATION_RATE` | `0.01` | Maximum share of entry/exit interval volume usable by the bot. |
| `LIQ_MAX_SPREAD_BPS_DEFAULT` | bps | `20` | рекомендуется `>= 0` | Максимальный spread для фондов без специальной категории. Ниже - строже фильтр ликвидности. | | `LIQ_MAX_SPREAD_BPS_DEFAULT` | `20` | Default spread cap. |
| `LIQ_MAX_SPREAD_BPS_MONEY_MARKET` | bps | `5` | рекомендуется `>= 0` | Максимальный spread для money market фондов. | | `LIQ_MAX_SPREAD_BPS_MONEY_MARKET` | `5` | Money-market fund spread cap. |
| `LIQ_MAX_SPREAD_BPS_BOND_FUNDS` | bps | `10` | рекомендуется `>= 0` | Максимальный spread для bond/corporate bond фондов. | | `LIQ_MAX_SPREAD_BPS_BOND_FUNDS` | `10` | Bond fund spread cap. |
| `LIQ_MAX_SPREAD_BPS_EQUITY_FUNDS` | bps | `25` | рекомендуется `>= 0` | Максимальный spread для equity фондов. | | `LIQ_MAX_SPREAD_BPS_EQUITY_FUNDS` | `25` | Equity fund spread cap. |
| `LIQ_MAX_TICK_BPS` | bps | `10` | рекомендуется `>= 0` | Максимальный размер минимального шага цены относительно цены. Ниже - отсекает инструменты с грубым тиком. | | `LIQ_MAX_TICK_BPS` | `10` | Maximum tick size relative to price. |
### COMM ### COMM
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `COMM_REQUIRE_ZERO_COMMISSION` | `true` или `false` | `true` | boolean | При `true` сигналы по инструментам с ожидаемой комиссией `> 0` отклоняются. | | `COMM_REQUIRE_ZERO_COMMISSION` | `true` | Rejects signals with expected commission above zero. |
| `COMM_QUARANTINE_ON_NONZERO` | `true` или `false` | `true` | boolean | При фактической брокерской комиссии `> 0` инструмент переводится в quarantine, а система останавливается через HALT по zero-commission policy. | | `COMM_QUARANTINE_ON_NONZERO` | `true` | Quarantines instruments and halts on actual non-zero broker commission. |
| `COMM_FREE_ORDER_COUNT_POLICY` | `submitted` или `cancel_counts` | `submitted` | одно из двух значений | Политика учёта бесплатных заявок: `submitted` считает только отправку новой заявки, `cancel_counts` дополнительно считает успешные отмены перед repost. | | `COMM_FREE_ORDER_COUNT_POLICY` | `submitted` | Free-order accounting policy: `submitted` or `cancel_counts`. |
В справочнике инструментов `free_order_limit_per_day=0` означает, что политика бесплатных заявок не настроена и новые входы запрещены; `-1` означает явно подтверждённое отсутствие дневного лимита. For instruments, `free_order_limit_per_day=0` means the free-order policy is not configured and new entries are blocked; `-1` means the absence of a daily free-order limit has been explicitly confirmed.
### BT ### BT
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `BT_DATE_FROM` | дата `YYYY-MM-DD` | пусто | сейчас не применяется | Зарезервировано под фильтр периода backtest. На текущий `cmd/bot` и `cmd/backtest` не влияет. | | `BT_DATE_FROM` | empty | Reserved period filter. |
| `BT_DATE_TO` | дата `YYYY-MM-DD` | пусто | сейчас не применяется | Зарезервировано под фильтр периода backtest. На текущий `cmd/bot` и `cmd/backtest` не влияет. | | `BT_DATE_TO` | empty | Reserved period filter. |
| `BT_ENTRY_SLIPPAGE_BPS` | bps | `8` | рекомендуется `>= 0` | Модельная издержка входа. Используется в расчёте `ExpectedCostBps`/`NetEdgeBps`; больше - строже отбор сигналов. | | `BT_ENTRY_SLIPPAGE_BPS` | `8` | Backtest entry slippage. |
| `BT_EXIT_SLIPPAGE_BPS` | bps | `8` | рекомендуется `>= 0` | Модельная издержка выхода. Больше - снижает ожидаемый net edge. | | `BT_EXIT_SLIPPAGE_BPS` | `8` | Backtest exit slippage. |
| `BT_COMMISSION_ROUNDTRIP_BPS` | bps | `0` | рекомендуется `>= 0` | Модельная комиссия за полный круг. Увеличение снижает `NetEdgeBps`; при zero-commission политике ненулевые комиссии могут отсеивать сделки в backtest engine. | | `BT_COMMISSION_ROUNDTRIP_BPS` | `0` | Backtest round-trip commission. |
| `BT_USE_MINUTE_MODEL` | `true` или `false` | `false` | boolean | Включает консервативную minute-модель backtest: лимитная цена должна быть достижима внутри минутной свечи, размер ограничивается participation cap по минутному объёму. В CLI соответствует `-use-minute-model` и требует `-minute-candles`. | | `BT_USE_MINUTE_MODEL` | `false` | Enables conservative minute-candle execution modeling. |
| `BT_OUTPUT_DIR` | путь к директории | `./backtest_out` | сейчас не применяется в `cmd/backtest`, где используется флаг `-out` | Зарезервированный ENV-путь для результатов backtest. | | `BT_OUTPUT_DIR` | `./backtest_out` | Reserved output directory; the CLI currently uses `-out`. |
### LIVE ### LIVE
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется | | Variable | Default | Description |
| --- | --- | --- | --- | --- | | --- | --- | --- |
| `LIVE_TRADE_ACK` | ровно `I_ACCEPT_RISK` | пусто | обязателен только для `APP_MODE=live_trade` | Ручное подтверждение риска для режима реальной торговли. Без него `live_trade` не стартует. | | `LIVE_TRADE_ACK` | empty | Must be exactly `I_ACCEPT_RISK` for `APP_MODE=live_trade`. |
| `LIVE_READONLY_DAYS` | целое число торговых дней | `0` | для `live_trade` должно быть `>= 20` | Подтверждает накопленный период работы в `live_readonly` перед реальной торговлей. | | `LIVE_READONLY_DAYS` | `0` | Must be at least `20` for `live_trade`. |
| `LIVE_PAPER_DAYS` | целое число торговых дней | `0` | для `live_trade` должно быть `>= 20` | Подтверждает период `paper`-прогона с bid/ask моделью. | | `LIVE_PAPER_DAYS` | `0` | Must be at least `20` for `live_trade`. |
| `LIVE_SANDBOX_DAYS` | целое число торговых дней | `0` | для `live_trade` должно быть `>= 10` | Подтверждает период sandbox без критических ошибок. | | `LIVE_SANDBOX_DAYS` | `0` | Must be at least `10` for `live_trade`. |
| `LIVE_COMMISSION_WHITELIST_CHECKED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Ручное подтверждение актуальных комиссий и whitelist инструментов. | | `LIVE_COMMISSION_WHITELIST_CHECKED` | `false` | Must be `true` for `live_trade`. |
| `LIVE_TELEGRAM_TESTED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает тест доставки Telegram-уведомлений. | | `LIVE_TELEGRAM_TESTED` | `false` | Must be `true` for `live_trade`. |
| `LIVE_KILL_SWITCH_TESTED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает тест ручного halt/unhalt сценария. | | `LIVE_KILL_SWITCH_TESTED` | `false` | Must be `true` for `live_trade`. |
| `LIVE_SERVER_TIME_CHECKED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает проверку server-time/drift в sandbox. | | `LIVE_SERVER_TIME_CHECKED` | `false` | Must be `true` for `live_trade`. |
| `LIVE_SMALL_CAPITAL` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает запуск реальной торговли с малым стартовым капиталом. | | `LIVE_SMALL_CAPITAL` | `false` | Must be `true` for `live_trade`. |
## Commands ## Commands
@@ -186,11 +211,14 @@ make lint
make test make test
make race make race
make build make build
go run ./cmd/migrate -direction=up go run ./cmd/migrate -direction=up
go run ./cmd/migrate up go run ./cmd/migrate up
go run ./cmd/mode-days -check=true go run ./cmd/mode-days -check=true
go run ./cmd/backtest -candles candles.csv -out ./backtest_out go run ./cmd/backtest -candles candles.csv -out ./backtest_out
go run ./cmd/backtest -candles candles.csv -minute-candles minute.csv -use-minute-model -out ./backtest_out go run ./cmd/backtest -candles candles.csv -minute-candles minute.csv -use-minute-model -out ./backtest_out
go run ./cmd/bot -mode=paper go run ./cmd/bot -mode=paper
go run ./cmd/bot -halt -reason="manual kill switch" go run ./cmd/bot -halt -reason="manual kill switch"
go run ./cmd/bot -unhalt -reason="manual reconciliation complete" go run ./cmd/bot -unhalt -reason="manual reconciliation complete"
@@ -204,74 +232,53 @@ instrument_uid,trade_date,open,high,low,close,volume_lots,lot,min_price_incremen
TRUR,2024-01-09,100,101,99,100.5,10000,10,0.01 TRUR,2024-01-09,100,101,99,100.5,10000,10,0.01
``` ```
Для minute-модели используется тот же формат, но `trade_date` может быть timestamp (`2024-01-09T18:25:00Z` или `2024-01-09 18:25:00`). CLI backtest требует `lot` и `min_price_increment` для каждого `instrument_uid`; metadata можно дать в daily CSV или в minute CSV. The minute model uses the same format, but `trade_date` may be a timestamp such as `2024-01-09T18:25:00Z` or `2024-01-09 18:25:00`. The backtest CLI requires `lot` and `min_price_increment` for every `instrument_uid`; metadata may come from either the daily CSV or the minute CSV.
`cmd/mode-days` считает distinct-дни по `system_state_history` и проверяет пороги `live_readonly >= 20`, `paper >= 20`, `sandbox >= 10`. История пишется после миграции `0010`; дни до неё автоматически восстановить нельзя, потому что старая схема хранила только текущий `system_state`. `cmd/mode-days` counts distinct days from `system_state_history` and checks the `live_readonly >= 20`, `paper >= 20`, and `sandbox >= 10` live-trade gates. The history table is written after migration `0010`.
`ClientOrderID` детерминирован по `(date, instrument_uid, side, attempt)`, укладывается в лимит T-Invest `order_id <= 36` и содержит SHA-256 suffix. При ручных массовых перезапусках с теми же параметрами id остаётся тем же, что намеренно подавляет дубли. `ClientOrderID` is deterministic by `(date, instrument_uid, side, attempt)`, fits the T-Invest `order_id <= 36` limit, and contains a SHA-256 suffix to suppress duplicate broker orders after restarts.
## Deploy ## Deploy
`.gitea/workflows/deploy.yml` собирает статический бинарь и кладёт его на сервер. Никакого Docker/Podman ни в CI, ни на сервере не используется — служба запускается напрямую через `systemd` внутри LXC-контейнера. БД (MariaDB/MySQL) живёт на отдельном хосте и подключается через `DB_DSN`. The Gitea workflow builds static Linux binaries for `cmd/bot`, `cmd/migrate`, and `cmd/backtest`, ships them to the target host, installs the systemd unit from `deploy/systemd/overnight-trading-bot.service`, restarts the service, and verifies health with `overnight-trading-bot -healthcheck`.
Pipeline на push в `master` (один job `deploy`): Required Gitea secrets:
1. `actions/setup-go@v5` (версия из `go.mod`), `go mod download`, `go vet ./...`, `go test ./...`. | Secret | Description |
2. Кросс-компиляция `linux/amd64` с `CGO_ENABLED=0` и `-trimpath -ldflags="-s -w"` для `cmd/bot`, `cmd/migrate`, `cmd/backtest`. Бинари именуются `overnight-trading-bot`, `overnight-trading-bot-migrate`, `overnight-trading-bot-backtest` — чтобы безопасно жить в `/usr/local/bin`. | --- | --- |
3. Сборка `out/release.tar.gz` с бинарями и `deploy/systemd/overnight-trading-bot.service`. | `secrets.DEPLOY_HOST` | Target host IP or DNS name. |
4. SSH-ключ из base64 → `~/.ssh/deploy_key`, `ssh-keyscan` целевого хоста. | `secrets.DEPLOY_SSH_PRIVATE_KEY_BASE64` | Root deployment SSH private key encoded with `base64 -w0 < id_ed25519`. |
5. `scp` архива в `/var/tmp/overnight-trading-bot-deploy/release.tar.gz` на сервере.
6. На сервере: проверка наличия env-файла, идемпотентное создание системного пользователя `overnight-bot`, распаковка во временный staging-каталог `/var/tmp/overnight-trading-bot-deploy/stage`, атомарная замена `/usr/local/bin/overnight-trading-bot{,-migrate,-backtest}` через `*.new``mv -f`, копирование systemd unit в `/etc/systemd/system/`, `systemctl daemon-reload && enable && restart`. После рестарта workflow ждёт `is-active` (до 60 с), затем проверяет `overnight-trading-bot -healthcheck` (до 30 с); при провале печатает последние 100 строк `journalctl` и завершается с ошибкой.
Переменные Gitea: Before the first deployment, create the production env file on the server:
- `secrets.DEPLOY_HOST` - IP сервера.
- `secrets.DEPLOY_SSH_PRIVATE_KEY_BASE64` - приватный SSH-ключ root в base64 (`base64 -w0 < id_ed25519`).
На сервере (debian 13 LXC) заранее должны быть установлены `systemd` (и стандартные утилиты `tar`, `useradd`, `journalctl` — все из coreutils/util-linux/systemd, в базовом образе debian 13 уже есть). Перед первым деплоем нужно создать production env-файл:
```sh ```sh
install -d -m 0750 /etc/overnight-trading-bot install -d -m 0750 /etc/overnight-trading-bot
install -m 0640 .env.example /etc/overnight-trading-bot/overnight-trading-bot.env install -m 0640 .env.example /etc/overnight-trading-bot/overnight-trading-bot.env
``` ```
В `/etc/overnight-trading-bot/overnight-trading-bot.env` нужно заменить `DB_DSN` на адрес внешней БД и заполнить секреты T-Invest/Telegram. Workflow при каждом деплое перевыставляет владельцем группу `overnight-bot` и режим `0640`, чтобы env читался службой, но не был world-readable. Если файла нет, workflow падает до перезапуска службы. Then replace `DB_DSN` with the external database address and fill T-Invest/Telegram secrets. The service runs as the unprivileged `overnight-bot` user with basic systemd hardening. Logs are available through:
Systemd unit (`deploy/systemd/overnight-trading-bot.service`) запускает `/usr/local/bin/overnight-trading-bot` под непривилегированным пользователем `overnight-bot` с базовым systemd hardening (`NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, пустой `CapabilityBoundingSet` и т.д.). Логи смотрятся через `journalctl -u overnight-trading-bot.service`. ```sh
journalctl -u overnight-trading-bot.service
```
## Runbook ## Runbook
API недоступен утром: API unavailable in the morning:
1. Бот повторяет запросы с retry/backoff. 1. The bot retries requests with backoff.
2. Если сбой дольше `RISK_API_OUTAGE_HALT_SEC`, состояние переводится в `HALTED`. 2. If the outage exceeds `RISK_API_OUTAGE_HALT_SEC`, the system enters `HALTED`.
3. После восстановления сначала запускается reconciliation. 3. After recovery, run reconciliation first.
4. Ручной вывод из HALT: `go run ./cmd/bot -unhalt -reason="..."`. 4. Manual recovery uses `go run ./cmd/bot -unhalt -reason="..."`.
Позиция не закрыта до hard deadline: Position not closed before the hard deadline:
1. Scheduler отменяет активные sell-заявки, помечает незакрытые `HOLDING_OVERNIGHT`/`EXIT_ORDER_SENT`/`EXIT_PARTIALLY_FILLED` как `EXIT_FAILED` и отправляет critical alert. 1. The scheduler cancels active sell orders and marks unresolved positions as failed exit states.
2. Новые входы блокируются через HALT (`hard_exit_deadline_missed`). 2. New entries are blocked through `HALTED` with `hard_exit_deadline_missed`.
3. Требуется ручная сверка брокерского портфеля, активных заявок и локальной БД; `-unhalt` выполнит reconciliation перед снятием HALT и откажется продолжать при critical diff. 3. Manually reconcile broker portfolio, active orders, and the local database before unhalting.
Ненулевая комиссия: Non-zero commission:
1. Reconciliation фиксирует критическое расхождение по комиссии. 1. Reconciliation records a critical commission mismatch.
2. Бот уходит в `HALTED` через событие риска `reconciliation_critical`. 2. The instrument is quarantined when configured.
3. Инструмент нужно вручную перевести в quarantine или выключить до выяснения причины. Автоматический quarantine по `COMM_QUARANTINE_ON_NONZERO` сейчас не подключён. 3. The bot enters `HALTED` through the zero-commission policy.
Превышен лимит риска при открытой позиции:
1. `HALTED` блокирует любые новые заявки, включая автоматический exit.
2. Оператор делает ручную сверку брокерского портфеля, активных заявок и локальных `positions`/`orders`.
3. Если позицию нужно закрывать ботом, сначала выполняется `go run ./cmd/bot -unhalt -reason="manual reconciliation before exit"` после успешной reconciliation; если расхождения остаются, закрытие выполняется вручную у брокера и затем синхронизируется в БД.
Полевой sandbox-чек времени сервера:
1. Перед `live_readonly` выполнить sandbox-запуск, в котором `GetServerTime` получает `Date` из gRPC metadata.
2. Если SDK/API не возвращает `Date`, `GetServerTime` отдаёт явную ошибку; в `paper` она глушится, в API-режимах учитывается через `RISK_API_OUTAGE_HALT_SEC`.
3. До исправления источника server-time не переводить этот аккаунт в `live_trade`.
## Live Preconditions
Перед `live_trade` должны быть выполнены условия из ТЗ: минимум 20 торговых дней `live_readonly`, 20 дней `paper`, 10 дней `sandbox`, ручная проверка комиссий и whitelist, Telegram-тест, kill-switch-тест, успешный server-time check в sandbox и малый стартовый капитал.
+293
View File
@@ -0,0 +1,293 @@
# Overnight Trading Bot
[English](README.md) / [Русский](README.ru.md)
Go-бот для исследовательской overnight-стратегии `close -> next open` на фондах T-Капитала через T-Invest API.
Проект предназначен для проверки статистической гипотезы об overnight-доходностях, аккуратного backtest/paper/sandbox-прогона и контролируемого live-readonly/live-trade эксперимента с жёсткими risk limits. Он не предназначен для манипулирования рынком, воздействия на цену или обхода закона: все заявки должны иметь реальное намерение исполнения, использовать лимитный тип, проходить контроль ликвидности, спреда, комиссий, сверки и ручных pre-flight условий перед `live_trade`.
Одна из исследуемых идей - anomaly overnight/intraday returns, обсуждаемая в статье Bruce Knuteson, [“Nothing to See Here: How to Say It When You Need to”](https://ssrn.com/abstract=4619084) (`ssrn-4619084`).
Лицензия: [MIT](LICENSE).
## Quick Start
```sh
cp .env.example .env
make test
APP_MODE=backtest go run ./cmd/bot
```
Для daemon-режимов (`paper`, `sandbox`, `live_readonly`, `live_trade`) нужен `DB_DSN` MariaDB/MySQL. `live_trade` дополнительно требует `LIVE_TRADE_ACK=I_ACCEPT_RISK` и выполненные pre-flight условия из секции `LIVE`.
## Environment Variables
Конфигурация читается из ENV через `.env`. Если значение не парсится в нужный тип, бот падает на старте с ошибкой `load ENV config`.
Общие форматы:
- Время указывается в формате `HH:MM:SS` и трактуется в `Europe/Moscow`.
- Доли указываются десятичной дробью: `0.10` означает 10%, `0.005` означает 0.5%.
- `bps` - базисные пункты: `10` означает 0.10%.
- Boolean-значения: `true` или `false`.
- В колонке "Дефолт" указан дефолт из кода. Если дефолта в коде нет, но в `.env.example` есть пример, это отмечено отдельно.
- Границы делятся на жёсткую валидацию старта и практические ограничения. Там, где валидации пока нет, указано рекомендуемое значение.
### APP
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `APP_MODE` | `backtest`, `paper`, `sandbox`, `live_readonly`, `live_trade` | нет, в `.env.example`: `paper` | обязательна; только перечисленные значения | Режим работы. `backtest` не требует БД и API в `cmd/bot`; `paper` без `TINVEST_TOKEN` использует fake gateway, а с токеном берёт реальные market data/status через T-Invest при симулированных заявках; `sandbox`, `live_readonly`, `live_trade` подключаются к T-Invest API; `live_trade` может отправлять брокерские заявки. |
| `APP_TIMEZONE` | `Europe/Moscow` | `Europe/Moscow` | жёстко только `Europe/Moscow` | Таймзона расписания торговых окон. Изменить нельзя без изменения валидации. |
| `APP_LOG_LEVEL` | `debug`, `info`, `warn`, `warning`, `error` | `info` | неизвестное значение трактуется как `info` | Уровень JSON-логов. Ниже уровень - больше диагностических записей. |
| `APP_HEALTHCHECK_ADDR` | HTTP listen address, например `:3300` или `127.0.0.1:3300` | `:3300` | без отдельной валидации | Адрес `/health` и `/ready`; CLI `-healthcheck` по умолчанию проверяет `/ready`. При изменении меняется порт или интерфейс healthcheck-сервера. |
| `APP_SHUTDOWN_TIMEOUT_SEC` | целое число секунд | `30` | должно быть `> 0` | Таймаут graceful shutdown для HTTP healthcheck при остановке. |
### TINVEST
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `TINVEST_TOKEN` | токен T-Invest API | пусто | обязателен для `sandbox`, `live_readonly`, `live_trade`; опционален для `paper` | Доступ к реальному или sandbox API. В `paper` без токена используется fake gateway, с токеном - реальные market data и симулированные заявки. |
| `TINVEST_ACCOUNT_ID` | идентификатор брокерского счёта | пусто | обязателен для `sandbox`, `live_readonly`, `live_trade` | Счёт для портфеля, заявок и сверки. Для API-режимов бот падает на старте, если account id не указан. |
| `TINVEST_ENDPOINT` | gRPC endpoint T-Invest, обычно `host:port` | `invest-public-api.tinkoff.ru:443` | строка; валидации формата нет | Endpoint для API. В `sandbox` код принудительно использует sandbox endpoint. |
| `TINVEST_APP_NAME` | имя приложения | `overnight-trading-bot` | строка | Передаётся в SDK как имя клиента. Меняет идентификацию приложения на стороне API/логов. |
| `TINVEST_REQUEST_TIMEOUT_SEC` | целое число секунд | `10` | должно быть `> 0` | Таймаут API-запросов к T-Invest, включая retry-последовательность. Меньше значение быстрее освобождает торговый цикл при зависшем API, но повышает шанс timeout на медленной сети. |
| `TINVEST_RETRY_COUNT` | целое число попыток | `3` | `<= 0` трактуется как одна попытка | Общее число попыток для SDK-вызовов T-Invest через exponential backoff. Больше значение повышает устойчивость к кратким сбоям, но может дольше задерживать окончательную ошибку. |
| `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_TRADING_CALENDAR_EXCHANGE` | код биржевого календаря, например `MOEX` | `MOEX` | пустое значение заменяется на `MOEX` | Календарь торговых дней для загрузки истории и расчёта торгового цикла. |
### DB
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `DB_DSN` | MySQL/MariaDB DSN, например `bot:change-me@tcp(db.example.internal:3306)/overnight_bot?parseTime=true&loc=UTC&multiStatements=true` | нет, пример есть в `.env.example` | обязателен во всех режимах, кроме `backtest`; должен открываться драйвером MySQL | Подключение к БД. В БД хранятся инструменты, свечи, сигналы, заявки, позиции, состояния, события риска и отчёты. |
| `DB_MAX_OPEN_CONNS` | целое число | `20` | валидации нет; `<= 0` для `database/sql` означает без лимита | Максимум открытых соединений с БД. Больше - выше параллелизм, но больше нагрузка на MariaDB. |
| `DB_MAX_IDLE_CONNS` | целое число | `5` | валидации нет; `<= 0` отключает idle pool | Размер пула простаивающих соединений. Больше - меньше переподключений, но больше удерживаемых соединений. |
| `DB_CONN_MAX_LIFETIME_MIN` | целое число минут | `30` | валидации нет; `<= 0` отключает лимит lifetime | Сколько живёт соединение до пересоздания. Меньше - чаще переподключения, больше - дольше используются старые соединения. |
| `DB_MIGRATIONS_AUTO_APPLY` | `true` или `false` | `true` | boolean | Автоматически применяет миграции при старте daemon-режима. `false` требует запускать миграции вручную через `cmd/migrate`. |
### TELEGRAM
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `TELEGRAM_BOT_TOKEN` | токен Telegram-бота | пусто | строка | Если токен или `TELEGRAM_CHAT_ID` пустые, уведомления отключены и используется noop notifier. |
| `TELEGRAM_CHAT_ID` | числовой chat id | `0` | `int64`; `0` отключает Telegram | Чат, куда отправляются уведомления. |
| `TELEGRAM_NOTIFY_INFO` | `true` или `false` | `true` | boolean | Включает информационные сообщения, например старт бота и события заявок. При переполнении очереди такие сообщения могут быть отброшены. |
| `TELEGRAM_NOTIFY_WARN` | `true` или `false` | `true` | boolean | Включает предупреждения. |
| `TELEGRAM_NOTIFY_ALERT` | `true` или `false` | `true` | boolean | Включает alert-сообщения. Они считаются критичными для доставки и ждут место в очереди. |
| `TELEGRAM_NOTIFY_REPORT` | `true` или `false` | `true` | boolean | Включает дневные отчёты. |
### STRATEGY
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `STRATEGY_ROLLING_SHORT` | количество торговых дней | `60` | рекомендуется `> 0` | Короткое окно статистики overnight-доходности. Больше - стабильнее оценка, но медленнее реакция; меньше - быстрее реакция, но больше шум. |
| `STRATEGY_ROLLING_LONG` | количество торговых дней | `252` | рекомендуется `>= STRATEGY_ROLLING_SHORT` и `> 0` | Длинное окно для проверки положительного долгосрочного edge и глубины backfill. Больше требует больше истории. |
| `STRATEGY_EWMA_LAMBDA` | дробь для EWMA | `0.08` | рабочий диапазон `(0, 1]`; вне диапазона EWMA-функция использует `0.08` | Вес новых наблюдений в EWMA. Больше - свежее движение влияет сильнее. |
| `STRATEGY_ALLOCATION_METHOD` | `equal_weight` | `equal_weight` | сейчас поддерживается только `equal_weight` | Метод распределения капитала между выбранными сигналами. Текущая реализация делит лимит экспозиции поровну между выбранными инструментами. |
| `STRATEGY_MIN_TSTAT_60` | decimal t-stat | `1.25` | валидации нет; обычно `>= 0` | Минимальная статистическая значимость короткого edge. Выше - меньше входов, ниже - больше входов. |
| `STRATEGY_MIN_WIN_RATE_60` | доля прибыльных overnight-дней | `0.55` | рекомендуется `0..1` | Минимальная доля положительных overnight-наблюдений. Выше - строже фильтр сигналов. |
| `STRATEGY_MIN_NET_EDGE_BPS` | bps | `10` | валидации нет; обычно `>= 0` | Минимальный ожидаемый edge после издержек. Выше - меньше, но потенциально качественнее сигналы. |
| `STRATEGY_RISK_BUFFER_BPS` | bps | `5` | валидации нет; обычно `>= 0` | Дополнительная надбавка к ожидаемым издержкам в расчёте `NetEdgeBps`. Больше - консервативнее отбор. |
| `STRATEGY_EXPECTED_ENTRY_SLIPPAGE_BPS` | bps | `8` | должно быть `>= 0` | Ожидаемое проскальзывание входа для расчёта издержек сигнала и backtest-параметров приложения. |
| `STRATEGY_EXPECTED_EXIT_SLIPPAGE_BPS` | bps | `8` | должно быть `>= 0` | Ожидаемое проскальзывание выхода для расчёта издержек сигнала и backtest-параметров приложения. |
| `STRATEGY_INTERVAL_VOLUME_LOOKBACK_DAYS` | количество торговых дней | `20` | `0` заменяется на `20`; отрицательные значения запрещены | Окно оценки объёма в вечернем и утреннем интервалах исполнения для participation sizing. |
| `STRATEGY_MAX_POSITIONS` | целое число позиций | `5` | `> 0` включает лимит; `<= 0` фактически отключает signal-level лимит | Максимум одновременно открытых позиций на уровне генерации сигналов. Больше - больше диверсификация и нагрузка на капитал. |
### EXEC
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `EXEC_ENTRY_SIGNAL_TIME` | `HH:MM:SS` | `18:10:00` | должно парситься как время | Время старта подготовки данных и генерации сигналов. |
| `EXEC_ENTRY_WINDOW_START` | `HH:MM:SS` | `18:20:00` | `ENTRY_WINDOW_START < ENTRY_WINDOW_END <= NO_NEW_ENTRY_AFTER` | Начало окна постановки заявок на вход. Позже - меньше времени на исполнение входа. |
| `EXEC_ENTRY_WINDOW_END` | `HH:MM:SS` | `18:38:30` | см. правило окна входа | Конец активной постановки заявок на вход и market close для pre-trade проверки входа. |
| `EXEC_NO_NEW_ENTRY_AFTER` | `HH:MM:SS` | `18:38:30` | не раньше `EXEC_ENTRY_WINDOW_END` | После этого времени новые входы не ставятся, бот переходит в overnight hold. |
| `EXEC_EXIT_WATCH_START` | `HH:MM:SS` | `09:50:00` | `EXIT_WATCH_START <= EXIT_NOT_BEFORE <= EXIT_WINDOW_START < EXIT_WINDOW_END <= HARD_EXIT_DEADLINE` | Начало утреннего наблюдения перед выходом. До `EXEC_EXIT_WINDOW_START` заявки на выход ещё не ставятся. |
| `EXEC_EXIT_NOT_BEFORE` | `HH:MM:SS` | `10:03:00` | см. правило окна выхода; сейчас используется только валидацией | Нижняя граница "не выходить раньше". На текущий scheduler напрямую не влияет, потому что заявки начинаются с `EXEC_EXIT_WINDOW_START`. |
| `EXEC_EXIT_WINDOW_START` | `HH:MM:SS` | `10:05:00` | см. правило окна выхода | Начало постановки заявок на выход. Раньше - больше шанс выйти быстрее, но ближе к открытию рынка. |
| `EXEC_EXIT_WINDOW_END` | `HH:MM:SS` | `10:25:00` | см. правило окна выхода | Конец постановки новых exit-заявок, после него идёт мониторинг до hard deadline. |
| `EXEC_HARD_EXIT_DEADLINE` | `HH:MM:SS` | `10:45:00` | не раньше `EXEC_EXIT_WINDOW_END` | Крайний срок выхода. После него запускаются reconciliation и report; незакрытая позиция ведёт к ручной обработке/HALT-сценарию. |
| `EXEC_MARKET_CLOSE` | `HH:MM:SS` | `18:50:00` | должно быть позже входного окна и hard deadline | Ориентир закрытия рынка для pre-trade проверки минимального времени до close. |
| `EXEC_MIN_TIME_TO_CLOSE_SEC` | целое число секунд | `90` | `> 0` включает проверку; `<= 0` отключает | Минимальный запас до конца торгового окна для pre-trade. Больше - меньше риск ставить заявку слишком поздно. |
| `EXEC_ALLOW_MARKET_ORDERS` | только `false` | `false` | жёстко должно быть `false` | Защитный флаг стратегии LIMIT-only. `true` запрещён валидацией. |
| `EXEC_MAX_ENTRY_ORDER_ATTEMPTS` | целое число | `3` | рекомендуется `>= 1` | Максимальное число постановок входной заявки в `MonitorUntil`: после polling/repost остаток отменяется к дедлайну окна входа. |
| `EXEC_MAX_EXIT_ORDER_ATTEMPTS` | целое число | `3` | рекомендуется `>= 1` | Максимальное число постановок выходной заявки в `MonitorUntil`: после polling/repost остаток отменяется к hard deadline. |
| `EXEC_PASSIVE_IMPROVE_TICKS` | целое число тиков | `1` | отрицательное значение в pricing приравнивается к `0` | Насколько улучшать passive limit price от лучшего bid/ask. Больше - цена агрессивнее, но код не пересекает spread. |
| `EXEC_QUOTE_DEPTH` | целое число уровней стакана | `20` | `1..50` | Глубина стакана для оценки bid/ask и spread. Больше - больше данных из API, но для цены используется лучший уровень. |
| `EXEC_MAX_QUOTE_AGE_SEC` | целое число секунд | `3` | `> 0` включает проверку; `<= 0` отключает | Максимальный возраст котировки. Меньше - строже к свежести данных, но больше отказов `quote age exceeds`. |
| `EXEC_ORDER_POLL_INTERVAL_MS` | целое число миллисекунд | `500` | рекомендуется `> 0` | Частота polling статусов заявок в `MonitorUntil`; также задаёт нижнюю границу интервала между repost-попытками. |
### RISK
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `RISK_USE_MARGIN` | только `false` | `false` | жёстко должно быть `false` | Защитный запрет маржинальной торговли. |
| `RISK_ALLOW_SHORT` | только `false` | `false` | жёстко должно быть `false` | Защитный запрет коротких позиций. |
| `RISK_MAX_TOTAL_EXPOSURE_PCT` | доля equity | `0.50` | рекомендуется `0..1`; `0` фактически запрещает новые позиции | Общий лимит экспозиции, делится на выбранные инструменты при sizing. Больше - больше капитал в рынке. |
| `RISK_MAX_POSITION_PCT` | доля equity | `0.10` | рекомендуется `0..1`; `0` запрещает размер позиции | Максимальный размер одной позиции от equity. |
| `RISK_MAX_DAILY_LOSS_PCT` | доля equity | `0.01` | `> 0` включает лимит; `<= 0` отключает | Дневной стоп по убытку. При достижении pre-trade отклоняет новые заявки. |
| `RISK_MAX_WEEKLY_LOSS_PCT` | доля equity | `0.03` | `> 0` включает лимит; `<= 0` отключает | Недельный стоп по убытку. |
| `RISK_MAX_MONTHLY_DRAWDOWN_PCT` | доля equity | `0.07` | `> 0` включает лимит; `<= 0` отключает | Месячный лимит просадки. |
| `RISK_MAX_OPEN_POSITIONS` | целое число | `5` | `> 0` включает лимит; `<= 0` отключает | Risk-level максимум открытых позиций перед постановкой заявки. |
| `RISK_MAX_AVG_SLIPPAGE_BPS_10_TRADES` | bps | `15` | `> 0` включает лимит; `<= 0` отключает | Блокирует новые заявки при слишком большом среднем slippage за 10 сделок. |
| `RISK_API_OUTAGE_HALT_SEC` | целое число секунд | `180` | должно быть `> 0` | Если инфраструктурный/API сбой длится дольше, бот переводится в HALT. Больше - терпимее к сбоям, меньше - быстрее останавливается. |
| `RISK_MAX_CLOCK_DRIFT_SEC` | целое число секунд | `2` | `> 0` включает проверку drift; `<= 0` отключает | Максимальный рассинхрон локального времени и серверного времени API в `/ready`. |
| `RISK_RECONCILIATION_WINDOW_HOURS` | целое число часов | `72` | должно быть `> 0` | Глубина сверки последних заявок и операций брокера. Больше - больше история сверки, но тяжелее запросы. |
| `RISK_RECONCILIATION_SKEW_SEC` | целое число секунд | `10` | `>= 0` | Grace-window для только что отправленных локальных заявок: свежие in-flight orders не считаются diff, пока брокерский active-list догоняет запись. |
| `RISK_COMMISSION_TOLERANCE_RUB` | сумма в рублях | `0.01` | `>= 0` | Допуск для reconciliation по расхождению локальной и брокерской комиссии. Ненулевая брокерская комиссия всё равно считается нарушением при `COMM_REQUIRE_ZERO_COMMISSION=true`. |
| `RISK_CASH_USAGE_BUFFER` | доля cash | `0.95` | рекомендуется `0..1`; `0` запрещает использование cash | Какая часть свободных денег может идти в sizing. Меньше - больше денежный буфер. |
| `RISK_RISK_BUDGET_PER_INSTRUMENT_PCT` | доля equity | `0.005` | рекомендуется `> 0` | Риск-бюджет на инструмент, используется вместе с оценкой неблагоприятного overnight-движения. Больше - крупнее позиции при прочих равных. |
| `RISK_MIN_ORDER_NOTIONAL_RUB` | сумма в рублях | `1000` | `> 0` включает минимум; `<= 0` фактически отключает | Минимальный notional заявки. Если рассчитанная позиция меньше, сигнал отклоняется по sizing. |
| `RISK_SIZE_REDUCTION_WINDOW_TRADES` | количество закрытых сделок | `20` | `0` заменяется на `20`; отрицательные значения запрещены | Окно контроля отклонения realized edge от expected edge. |
| `RISK_SIZE_REDUCTION_FACTOR` | множитель размера | `0.5` | `(0, 1]`; `0` заменяется на `0.5` | Во сколько раз уменьшать sizing при устойчивом ухудшении качества исполнения/edge. |
| `RISK_SIZE_REDUCTION_TRIGGER_BPS` | bps | `-10` | `0` заменяется на `-10` в scheduler fallback | Порог среднего `realized_edge_bps - expected_net_edge_bps`, ниже которого включается size reduction rule. |
Если средний `realized_edge_bps - expected_net_edge_bps` по последним 20 закрытым сделкам ниже `-10 bps`, scheduler пишет `risk_event(WARN, size_reduction_rule_triggered)` и до восстановления качества режет sizing до `0.5x`. Если два таких окна по 20 сделок идут подряд в `live_trade`, бот автоматически переключает persisted/runtime mode в `live_readonly` и блокирует новые брокерские заявки до ручного вмешательства.
### LIQ
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `LIQ_MIN_ADV_RUB` | сумма в рублях | `5000000` | рекомендуется `>= 0` | Минимальный средний дневной оборот за 20 дней. Выше - отсекает менее ликвидные фонды. |
| `LIQ_MAX_PARTICIPATION_RATE` | доля объёма | `0.01` | рекомендуется `0..1` | Максимальная доля объёма входного/выходного окна, которую может занять бот при sizing. Больше - крупнее позиции, но выше рыночное воздействие. |
| `LIQ_MAX_SPREAD_BPS_DEFAULT` | bps | `20` | рекомендуется `>= 0` | Максимальный spread для фондов без специальной категории. Ниже - строже фильтр ликвидности. |
| `LIQ_MAX_SPREAD_BPS_MONEY_MARKET` | bps | `5` | рекомендуется `>= 0` | Максимальный spread для money market фондов. |
| `LIQ_MAX_SPREAD_BPS_BOND_FUNDS` | bps | `10` | рекомендуется `>= 0` | Максимальный spread для bond/corporate bond фондов. |
| `LIQ_MAX_SPREAD_BPS_EQUITY_FUNDS` | bps | `25` | рекомендуется `>= 0` | Максимальный spread для equity фондов. |
| `LIQ_MAX_TICK_BPS` | bps | `10` | рекомендуется `>= 0` | Максимальный размер минимального шага цены относительно цены. Ниже - отсекает инструменты с грубым тиком. |
### COMM
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `COMM_REQUIRE_ZERO_COMMISSION` | `true` или `false` | `true` | boolean | При `true` сигналы по инструментам с ожидаемой комиссией `> 0` отклоняются. |
| `COMM_QUARANTINE_ON_NONZERO` | `true` или `false` | `true` | boolean | При фактической брокерской комиссии `> 0` инструмент переводится в quarantine, а система останавливается через HALT по zero-commission policy. |
| `COMM_FREE_ORDER_COUNT_POLICY` | `submitted` или `cancel_counts` | `submitted` | одно из двух значений | Политика учёта бесплатных заявок: `submitted` считает только отправку новой заявки, `cancel_counts` дополнительно считает успешные отмены перед repost. |
В справочнике инструментов `free_order_limit_per_day=0` означает, что политика бесплатных заявок не настроена и новые входы запрещены; `-1` означает явно подтверждённое отсутствие дневного лимита.
### BT
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `BT_DATE_FROM` | дата `YYYY-MM-DD` | пусто | сейчас не применяется | Зарезервировано под фильтр периода backtest. На текущий `cmd/bot` и `cmd/backtest` не влияет. |
| `BT_DATE_TO` | дата `YYYY-MM-DD` | пусто | сейчас не применяется | Зарезервировано под фильтр периода backtest. На текущий `cmd/bot` и `cmd/backtest` не влияет. |
| `BT_ENTRY_SLIPPAGE_BPS` | bps | `8` | рекомендуется `>= 0` | Модельная издержка входа. Используется в расчёте `ExpectedCostBps`/`NetEdgeBps`; больше - строже отбор сигналов. |
| `BT_EXIT_SLIPPAGE_BPS` | bps | `8` | рекомендуется `>= 0` | Модельная издержка выхода. Больше - снижает ожидаемый net edge. |
| `BT_COMMISSION_ROUNDTRIP_BPS` | bps | `0` | рекомендуется `>= 0` | Модельная комиссия за полный круг. Увеличение снижает `NetEdgeBps`; при zero-commission политике ненулевые комиссии могут отсеивать сделки в backtest engine. |
| `BT_USE_MINUTE_MODEL` | `true` или `false` | `false` | boolean | Включает консервативную minute-модель backtest: лимитная цена должна быть достижима внутри минутной свечи, размер ограничивается participation cap по минутному объёму. В CLI соответствует `-use-minute-model` и требует `-minute-candles`. |
| `BT_OUTPUT_DIR` | путь к директории | `./backtest_out` | сейчас не применяется в `cmd/backtest`, где используется флаг `-out` | Зарезервированный ENV-путь для результатов backtest. |
### LIVE
| Переменная | Что указывать | Дефолт | Границы/валидация | За что отвечает и что меняется |
| --- | --- | --- | --- | --- |
| `LIVE_TRADE_ACK` | ровно `I_ACCEPT_RISK` | пусто | обязателен только для `APP_MODE=live_trade` | Ручное подтверждение риска для режима реальной торговли. Без него `live_trade` не стартует. |
| `LIVE_READONLY_DAYS` | целое число торговых дней | `0` | для `live_trade` должно быть `>= 20` | Подтверждает накопленный период работы в `live_readonly` перед реальной торговлей. |
| `LIVE_PAPER_DAYS` | целое число торговых дней | `0` | для `live_trade` должно быть `>= 20` | Подтверждает период `paper`-прогона с bid/ask моделью. |
| `LIVE_SANDBOX_DAYS` | целое число торговых дней | `0` | для `live_trade` должно быть `>= 10` | Подтверждает период sandbox без критических ошибок. |
| `LIVE_COMMISSION_WHITELIST_CHECKED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Ручное подтверждение актуальных комиссий и whitelist инструментов. |
| `LIVE_TELEGRAM_TESTED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает тест доставки Telegram-уведомлений. |
| `LIVE_KILL_SWITCH_TESTED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает тест ручного halt/unhalt сценария. |
| `LIVE_SERVER_TIME_CHECKED` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает проверку server-time/drift в sandbox. |
| `LIVE_SMALL_CAPITAL` | `true` или `false` | `false` | для `live_trade` должно быть `true` | Подтверждает запуск реальной торговли с малым стартовым капиталом. |
## Commands
```sh
make fmt
make vet
make lint
make test
make race
make build
go run ./cmd/migrate -direction=up
go run ./cmd/migrate up
go run ./cmd/mode-days -check=true
go run ./cmd/backtest -candles candles.csv -out ./backtest_out
go run ./cmd/backtest -candles candles.csv -minute-candles minute.csv -use-minute-model -out ./backtest_out
go run ./cmd/bot -mode=paper
go run ./cmd/bot -halt -reason="manual kill switch"
go run ./cmd/bot -unhalt -reason="manual reconciliation complete"
go run ./cmd/bot -healthcheck
```
Backtest CSV columns:
```csv
instrument_uid,trade_date,open,high,low,close,volume_lots,lot,min_price_increment
TRUR,2024-01-09,100,101,99,100.5,10000,10,0.01
```
Для minute-модели используется тот же формат, но `trade_date` может быть timestamp (`2024-01-09T18:25:00Z` или `2024-01-09 18:25:00`). CLI backtest требует `lot` и `min_price_increment` для каждого `instrument_uid`; metadata можно дать в daily CSV или в minute CSV.
`cmd/mode-days` считает distinct-дни по `system_state_history` и проверяет пороги `live_readonly >= 20`, `paper >= 20`, `sandbox >= 10`. История пишется после миграции `0010`; дни до неё автоматически восстановить нельзя, потому что старая схема хранила только текущий `system_state`.
`ClientOrderID` детерминирован по `(date, instrument_uid, side, attempt)`, укладывается в лимит T-Invest `order_id <= 36` и содержит SHA-256 suffix. При ручных массовых перезапусках с теми же параметрами id остаётся тем же, что намеренно подавляет дубли.
## Deploy
`.gitea/workflows/deploy.yml` собирает статический бинарь и кладёт его на сервер. Никакого Docker/Podman ни в CI, ни на сервере не используется — служба запускается напрямую через `systemd` внутри LXC-контейнера. БД (MariaDB/MySQL) живёт на отдельном хосте и подключается через `DB_DSN`.
Pipeline на push в `master` (один job `deploy`):
1. `actions/setup-go@v5` (версия из `go.mod`), `go mod download`, `go vet ./...`, `go test ./...`.
2. Кросс-компиляция `linux/amd64` с `CGO_ENABLED=0` и `-trimpath -ldflags="-s -w"` для `cmd/bot`, `cmd/migrate`, `cmd/backtest`. Бинари именуются `overnight-trading-bot`, `overnight-trading-bot-migrate`, `overnight-trading-bot-backtest` — чтобы безопасно жить в `/usr/local/bin`.
3. Сборка `out/release.tar.gz` с бинарями и `deploy/systemd/overnight-trading-bot.service`.
4. SSH-ключ из base64 → `~/.ssh/deploy_key`, `ssh-keyscan` целевого хоста.
5. `scp` архива в `/var/tmp/overnight-trading-bot-deploy/release.tar.gz` на сервере.
6. На сервере: проверка наличия env-файла, идемпотентное создание системного пользователя `overnight-bot`, распаковка во временный staging-каталог `/var/tmp/overnight-trading-bot-deploy/stage`, атомарная замена `/usr/local/bin/overnight-trading-bot{,-migrate,-backtest}` через `*.new``mv -f`, копирование systemd unit в `/etc/systemd/system/`, `systemctl daemon-reload && enable && restart`. После рестарта workflow ждёт `is-active` (до 60 с), затем проверяет `overnight-trading-bot -healthcheck` (до 30 с); при провале печатает последние 100 строк `journalctl` и завершается с ошибкой.
Переменные Gitea:
- `secrets.DEPLOY_HOST` - IP сервера.
- `secrets.DEPLOY_SSH_PRIVATE_KEY_BASE64` - приватный SSH-ключ root в base64 (`base64 -w0 < id_ed25519`).
На сервере (debian 13 LXC) заранее должны быть установлены `systemd` (и стандартные утилиты `tar`, `useradd`, `journalctl` — все из coreutils/util-linux/systemd, в базовом образе debian 13 уже есть). Перед первым деплоем нужно создать production env-файл:
```sh
install -d -m 0750 /etc/overnight-trading-bot
install -m 0640 .env.example /etc/overnight-trading-bot/overnight-trading-bot.env
```
В `/etc/overnight-trading-bot/overnight-trading-bot.env` нужно заменить `DB_DSN` на адрес внешней БД и заполнить секреты T-Invest/Telegram. Workflow при каждом деплое перевыставляет владельцем группу `overnight-bot` и режим `0640`, чтобы env читался службой, но не был world-readable. Если файла нет, workflow падает до перезапуска службы.
Systemd unit (`deploy/systemd/overnight-trading-bot.service`) запускает `/usr/local/bin/overnight-trading-bot` под непривилегированным пользователем `overnight-bot` с базовым systemd hardening (`NoNewPrivileges`, `ProtectSystem=strict`, `ProtectHome`, пустой `CapabilityBoundingSet` и т.д.). Логи смотрятся через `journalctl -u overnight-trading-bot.service`.
## Runbook
API недоступен утром:
1. Бот повторяет запросы с retry/backoff.
2. Если сбой дольше `RISK_API_OUTAGE_HALT_SEC`, состояние переводится в `HALTED`.
3. После восстановления сначала запускается reconciliation.
4. Ручной вывод из HALT: `go run ./cmd/bot -unhalt -reason="..."`.
Позиция не закрыта до hard deadline:
1. Scheduler отменяет активные sell-заявки, помечает незакрытые `HOLDING_OVERNIGHT`/`EXIT_ORDER_SENT`/`EXIT_PARTIALLY_FILLED` как `EXIT_FAILED` и отправляет critical alert.
2. Новые входы блокируются через HALT (`hard_exit_deadline_missed`).
3. Требуется ручная сверка брокерского портфеля, активных заявок и локальной БД; `-unhalt` выполнит reconciliation перед снятием HALT и откажется продолжать при critical diff.
Ненулевая комиссия:
1. Reconciliation фиксирует критическое расхождение по комиссии.
2. Бот уходит в `HALTED` через событие риска `reconciliation_critical`.
3. Инструмент нужно вручную перевести в quarantine или выключить до выяснения причины. Автоматический quarantine по `COMM_QUARANTINE_ON_NONZERO` сейчас не подключён.
Превышен лимит риска при открытой позиции:
1. `HALTED` блокирует любые новые заявки, включая автоматический exit.
2. Оператор делает ручную сверку брокерского портфеля, активных заявок и локальных `positions`/`orders`.
3. Если позицию нужно закрывать ботом, сначала выполняется `go run ./cmd/bot -unhalt -reason="manual reconciliation before exit"` после успешной reconciliation; если расхождения остаются, закрытие выполняется вручную у брокера и затем синхронизируется в БД.
Полевой sandbox-чек времени сервера:
1. Перед `live_readonly` выполнить sandbox-запуск, в котором `GetServerTime` получает `Date` из gRPC metadata.
2. Если SDK/API не возвращает `Date`, `GetServerTime` отдаёт явную ошибку; в `paper` она глушится, в API-режимах учитывается через `RISK_API_OUTAGE_HALT_SEC`.
3. До исправления источника server-time не переводить этот аккаунт в `live_trade`.
## Live Preconditions
Перед `live_trade` должны быть выполнены условия из ТЗ: минимум 20 торговых дней `live_readonly`, 20 дней `paper`, 10 дней `sandbox`, ручная проверка комиссий и whitelist, Telegram-тест, kill-switch-тест, успешный server-time check в sandbox и малый стартовый капитал.