feat: init go project
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
.DS_Store
|
||||
|
||||
# Go build artifacts
|
||||
/bin/
|
||||
/dist/
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Local configuration and secrets
|
||||
.env
|
||||
config.yaml
|
||||
|
||||
# Editor folders
|
||||
.idea/
|
||||
.vscode/
|
||||
@@ -0,0 +1,13 @@
|
||||
.PHONY: fmt test run tidy
|
||||
|
||||
fmt:
|
||||
go fmt ./...
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
run:
|
||||
go run ./cmd/bot
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
@@ -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 и ссылки на переменные окружения с токенами.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user