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