feat: init go project

This commit is contained in:
2026-06-06 22:19:21 +00:00
parent 3b6e443483
commit 7809552d05
8 changed files with 235 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
.DS_Store
# Go build artifacts
/bin/
/dist/
*.test
*.out
# Local configuration and secrets
.env
config.yaml
# Editor folders
.idea/
.vscode/
+13
View File
@@ -0,0 +1,13 @@
.PHONY: fmt test run tidy
fmt:
go fmt ./...
test:
go test ./...
run:
go run ./cmd/bot
tidy:
go mod tidy
+21
View File
@@ -0,0 +1,21 @@
# Overnight Trading Bot
Go-проект для overnight-бота по фондам T-Капитала через T-Invest API.
## Quick Start
```sh
cp config.example.yaml config.yaml
go test ./...
go run ./cmd/bot -config config.yaml
```
## Development
```sh
make fmt
make test
make run
```
`config.yaml` не коммитится: в нем будут локальные настройки, account id и ссылки на переменные окружения с токенами.
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"overnight-trading-bot/internal/app"
)
func main() {
configPath := flag.String("config", "config.yaml", "path to YAML configuration file")
flag.Parse()
if err := app.Run(context.Background(), app.Options{
ConfigPath: *configPath,
Stdout: os.Stdout,
}); err != nil {
fmt.Fprintf(os.Stderr, "bot failed: %v\n", err)
os.Exit(1)
}
}
+70
View File
@@ -0,0 +1,70 @@
app:
mode: paper
timezone: Europe/Moscow
tinkoff:
token_env: T_INVEST_TOKEN
account_id_env: T_INVEST_ACCOUNT_ID
risk:
max_position_rub: 10000
min_net_edge_bps: 10
risk_buffer_bps: 5
max_spread_bps: 20
max_tick_bps: 5
min_adv_rub: 10000000
signal:
overnight_window_short: 60
overnight_window_long: 252
min_tstat_short: 1.25
min_win_rate_short: 0.55
ewma_lambda: 0.08
execution:
entry_window: "18:30-18:45"
exit_window: "10:15-10:45"
limit_order_only: true
instruments:
- ticker: TRUR
enabled: true
expected_commission_bps_per_side: 0
free_order_limit_per_day: 15
fund_type: mixed
- ticker: TGLD
enabled: true
expected_commission_bps_per_side: 0
free_order_limit_per_day: 15
fund_type: commodity
- ticker: TBRU
enabled: true
expected_commission_bps_per_side: 0
fund_type: bonds
- ticker: TDIV
enabled: true
expected_commission_bps_per_side: 0
fund_type: equity_income
- ticker: TMON
enabled: true
expected_commission_bps_per_side: 0
fund_type: money_market
- ticker: TOFZ
enabled: true
expected_commission_bps_per_side: 0
fund_type: bonds
- ticker: TLCB
enabled: true
expected_commission_bps_per_side: 0
fund_type: corporate_bonds
- ticker: TITR
enabled: true
expected_commission_bps_per_side: 0
fund_type: equity
- ticker: TRND
enabled: true
expected_commission_bps_per_side: 0
fund_type: equity
- ticker: TMOS
enabled: false
exclude_reason: "Excluded by default due to possible non-zero sell-side fee"
+3
View File
@@ -0,0 +1,3 @@
module overnight-trading-bot
go 1.26.2
+39
View File
@@ -0,0 +1,39 @@
package app
import (
"context"
"errors"
"fmt"
"io"
"os"
)
type Options struct {
ConfigPath string
Stdout io.Writer
}
func Run(ctx context.Context, opts Options) error {
if err := ctx.Err(); err != nil {
return err
}
if opts.ConfigPath == "" {
return errors.New("config path is required")
}
if opts.Stdout == nil {
opts.Stdout = io.Discard
}
if _, err := os.Stat(opts.ConfigPath); err != nil {
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("config file %q does not exist; copy config.example.yaml to config.yaml and fill credentials", opts.ConfigPath)
}
return fmt.Errorf("check config file %q: %w", opts.ConfigPath, err)
}
fmt.Fprintf(opts.Stdout, "overnight trading bot initialized with config %q\n", opts.ConfigPath)
return nil
}
+51
View File
@@ -0,0 +1,51 @@
package app
import (
"bytes"
"context"
"os"
"strings"
"testing"
)
func TestRunRequiresConfigPath(t *testing.T) {
err := Run(context.Background(), Options{})
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "config path is required") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunReportsMissingConfig(t *testing.T) {
err := Run(context.Background(), Options{ConfigPath: "missing.yaml"})
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "does not exist") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestRunUsesExistingConfig(t *testing.T) {
touch := t.TempDir() + "/config.yaml"
if err := os.WriteFile(touch, []byte("instruments: []\n"), 0o600); err != nil {
t.Fatal(err)
}
var stdout bytes.Buffer
err := Run(context.Background(), Options{
ConfigPath: touch,
Stdout: &stdout,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(stdout.String(), "initialized") {
t.Fatalf("unexpected stdout: %q", stdout.String())
}
}