thirteenth version

This commit is contained in:
2026-06-09 21:04:01 +00:00
parent f877907b20
commit 4dec14f57c
19 changed files with 602 additions and 110 deletions
+51 -17
View File
@@ -46,14 +46,15 @@ type AppConfig struct {
}
type TInvestConfig struct {
Token string `env:"TOKEN"`
AccountID string `env:"ACCOUNT_ID"`
Endpoint string `env:"ENDPOINT" envDefault:"invest-public-api.tinkoff.ru:443"`
AppName string `env:"APP_NAME" envDefault:"overnight-trading-bot"`
RequestTimeoutSec int `env:"REQUEST_TIMEOUT_SEC" envDefault:"10"`
RetryCount int `env:"RETRY_COUNT" envDefault:"3"`
RetryBackoffSec int `env:"RETRY_BACKOFF_SEC" envDefault:"2"`
UseSandbox bool `env:"USE_SANDBOX" envDefault:"false"`
Token string `env:"TOKEN"`
AccountID string `env:"ACCOUNT_ID"`
Endpoint string `env:"ENDPOINT" envDefault:"invest-public-api.tinkoff.ru:443"`
AppName string `env:"APP_NAME" envDefault:"overnight-trading-bot"`
RequestTimeoutSec int `env:"REQUEST_TIMEOUT_SEC" envDefault:"10"`
RetryCount int `env:"RETRY_COUNT" envDefault:"3"`
RetryBackoffSec int `env:"RETRY_BACKOFF_SEC" envDefault:"2"`
UseSandbox bool `env:"USE_SANDBOX" envDefault:"false"`
TradingCalendarExchange string `env:"TRADING_CALENDAR_EXCHANGE" envDefault:"MOEX"`
}
type DBConfig struct {
@@ -74,15 +75,18 @@ type TelegramConfig struct {
}
type StrategyConfig struct {
RollingShort int `env:"ROLLING_SHORT" envDefault:"60"`
RollingLong int `env:"ROLLING_LONG" envDefault:"252"`
EWMALambda float64 `env:"EWMA_LAMBDA" envDefault:"0.08"`
AllocationMethod string `env:"ALLOCATION_METHOD" envDefault:"equal_weight"`
MinTStat60 decimal.Decimal `env:"MIN_TSTAT_60" envDefault:"1.25"`
MinWinRate60 decimal.Decimal `env:"MIN_WIN_RATE_60" envDefault:"0.55"`
MinNetEdgeBps decimal.Decimal `env:"MIN_NET_EDGE_BPS" envDefault:"10"`
RiskBufferBps decimal.Decimal `env:"RISK_BUFFER_BPS" envDefault:"5"`
MaxPositions int `env:"MAX_POSITIONS" envDefault:"5"`
RollingShort int `env:"ROLLING_SHORT" envDefault:"60"`
RollingLong int `env:"ROLLING_LONG" envDefault:"252"`
EWMALambda float64 `env:"EWMA_LAMBDA" envDefault:"0.08"`
AllocationMethod string `env:"ALLOCATION_METHOD" envDefault:"equal_weight"`
MinTStat60 decimal.Decimal `env:"MIN_TSTAT_60" envDefault:"1.25"`
MinWinRate60 decimal.Decimal `env:"MIN_WIN_RATE_60" envDefault:"0.55"`
MinNetEdgeBps decimal.Decimal `env:"MIN_NET_EDGE_BPS" envDefault:"10"`
RiskBufferBps decimal.Decimal `env:"RISK_BUFFER_BPS" envDefault:"5"`
ExpectedEntrySlippageBps decimal.Decimal `env:"EXPECTED_ENTRY_SLIPPAGE_BPS" envDefault:"8"`
ExpectedExitSlippageBps decimal.Decimal `env:"EXPECTED_EXIT_SLIPPAGE_BPS" envDefault:"8"`
IntervalVolumeLookbackDays int `env:"INTERVAL_VOLUME_LOOKBACK_DAYS" envDefault:"20"`
MaxPositions int `env:"MAX_POSITIONS" envDefault:"5"`
}
type ExecutionConfig struct {
@@ -124,6 +128,9 @@ type RiskConfig struct {
CashUsageBuffer decimal.Decimal `env:"CASH_USAGE_BUFFER" envDefault:"0.95"`
RiskBudgetPerInstrumentPct decimal.Decimal `env:"RISK_BUDGET_PER_INSTRUMENT_PCT" envDefault:"0.005"`
MinOrderNotionalRUB decimal.Decimal `env:"MIN_ORDER_NOTIONAL_RUB" envDefault:"1000"`
SizeReductionWindowTrades int `env:"SIZE_REDUCTION_WINDOW_TRADES" envDefault:"20"`
SizeReductionFactor decimal.Decimal `env:"SIZE_REDUCTION_FACTOR" envDefault:"0.5"`
SizeReductionTriggerBps decimal.Decimal `env:"SIZE_REDUCTION_TRIGGER_BPS" envDefault:"-10"`
}
type LiquidityConfig struct {
@@ -194,6 +201,9 @@ func (c *Config) Validate() error {
if c.TInvest.RequestTimeoutSec <= 0 {
return errors.New("TINVEST_REQUEST_TIMEOUT_SEC must be positive")
}
if c.TInvest.TradingCalendarExchange == "" {
c.TInvest.TradingCalendarExchange = "MOEX"
}
if c.Execution.AllowMarketOrders {
return errors.New("EXEC_ALLOW_MARKET_ORDERS must remain false: strategy is LIMIT-only")
}
@@ -221,6 +231,18 @@ func (c *Config) Validate() error {
if c.Risk.CommissionToleranceRUB.IsNegative() {
return errors.New("RISK_COMMISSION_TOLERANCE_RUB must be non-negative")
}
if c.Risk.SizeReductionWindowTrades == 0 {
c.Risk.SizeReductionWindowTrades = 20
}
if c.Risk.SizeReductionWindowTrades < 0 {
return errors.New("RISK_SIZE_REDUCTION_WINDOW_TRADES must be positive")
}
if c.Risk.SizeReductionFactor.IsZero() {
c.Risk.SizeReductionFactor = decimal.RequireFromString("0.5")
}
if !c.Risk.SizeReductionFactor.IsPositive() || c.Risk.SizeReductionFactor.GreaterThan(decimal.NewFromInt(1)) {
return errors.New("RISK_SIZE_REDUCTION_FACTOR must be in (0, 1]")
}
if c.Commission.FreeOrderCountPolicy == "" {
c.Commission.FreeOrderCountPolicy = "submitted"
}
@@ -235,6 +257,18 @@ func (c *Config) Validate() error {
if c.Strategy.AllocationMethod != "equal_weight" {
return fmt.Errorf("STRATEGY_ALLOCATION_METHOD must be equal_weight, got %q", c.Strategy.AllocationMethod)
}
if c.Strategy.ExpectedEntrySlippageBps.IsNegative() {
return errors.New("STRATEGY_EXPECTED_ENTRY_SLIPPAGE_BPS must be non-negative")
}
if c.Strategy.ExpectedExitSlippageBps.IsNegative() {
return errors.New("STRATEGY_EXPECTED_EXIT_SLIPPAGE_BPS must be non-negative")
}
if c.Strategy.IntervalVolumeLookbackDays == 0 {
c.Strategy.IntervalVolumeLookbackDays = 20
}
if c.Strategy.IntervalVolumeLookbackDays < 0 {
return errors.New("STRATEGY_INTERVAL_VOLUME_LOOKBACK_DAYS must be positive")
}
if err := c.validateWindows(); err != nil {
return err
}
+42
View File
@@ -44,6 +44,48 @@ func TestValidateLiveTradeAcceptsAllPreconditions(t *testing.T) {
}
}
func TestLoadKeepsStrategyExpectedSlippageSeparateFromBacktest(t *testing.T) {
t.Setenv("APP_MODE", "backtest")
t.Setenv("STRATEGY_EXPECTED_ENTRY_SLIPPAGE_BPS", "2")
t.Setenv("STRATEGY_EXPECTED_EXIT_SLIPPAGE_BPS", "3")
t.Setenv("BT_ENTRY_SLIPPAGE_BPS", "11")
t.Setenv("BT_EXIT_SLIPPAGE_BPS", "13")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if !cfg.Strategy.ExpectedEntrySlippageBps.Equal(decimal.NewFromInt(2)) || !cfg.Strategy.ExpectedExitSlippageBps.Equal(decimal.NewFromInt(3)) {
t.Fatalf("strategy slippage entry=%s exit=%s, want 2/3", cfg.Strategy.ExpectedEntrySlippageBps, cfg.Strategy.ExpectedExitSlippageBps)
}
if !cfg.Backtest.EntrySlippageBps.Equal(decimal.NewFromInt(11)) || !cfg.Backtest.ExitSlippageBps.Equal(decimal.NewFromInt(13)) {
t.Fatalf("backtest slippage entry=%s exit=%s, want 11/13", cfg.Backtest.EntrySlippageBps, cfg.Backtest.ExitSlippageBps)
}
}
func TestLoadSchedulerKnobsFromEnv(t *testing.T) {
t.Setenv("APP_MODE", "backtest")
t.Setenv("STRATEGY_INTERVAL_VOLUME_LOOKBACK_DAYS", "12")
t.Setenv("RISK_SIZE_REDUCTION_WINDOW_TRADES", "7")
t.Setenv("RISK_SIZE_REDUCTION_FACTOR", "0.25")
t.Setenv("RISK_SIZE_REDUCTION_TRIGGER_BPS", "-5")
t.Setenv("TINVEST_TRADING_CALENDAR_EXCHANGE", "MOEX_FOND")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if cfg.Strategy.IntervalVolumeLookbackDays != 12 || cfg.Risk.SizeReductionWindowTrades != 7 {
t.Fatalf("window config strategy=%d risk=%d, want 12/7", cfg.Strategy.IntervalVolumeLookbackDays, cfg.Risk.SizeReductionWindowTrades)
}
if !cfg.Risk.SizeReductionFactor.Equal(decimal.RequireFromString("0.25")) || !cfg.Risk.SizeReductionTriggerBps.Equal(decimal.NewFromInt(-5)) {
t.Fatalf("size reduction factor=%s trigger=%s, want 0.25/-5", cfg.Risk.SizeReductionFactor, cfg.Risk.SizeReductionTriggerBps)
}
if cfg.TInvest.TradingCalendarExchange != "MOEX_FOND" {
t.Fatalf("calendar exchange=%q, want MOEX_FOND", cfg.TInvest.TradingCalendarExchange)
}
}
func minimalBrokerConfig(mode domain.Mode) Config {
return Config{
App: AppConfig{