From 2b892123e1290e8ff2323dc92ddad9a70612564b Mon Sep 17 00:00:00 2001 From: hwinkel Date: Wed, 14 Jan 2026 22:44:12 +0100 Subject: [PATCH] ADD: added first version --- .gitignore | 29 ++++++++ README.md | 2 + cmd/backtest/main.go | 96 ++++++++++++++++++++++++++ go.mod | 28 ++++++++ go.mod:Zone.Identifier | Bin 0 -> 89 bytes go.sum | 43 ++++++++++++ internal/alpaca/client.go | 82 ++++++++++++++++++++++ internal/backtest/engine.go | 121 +++++++++++++++++++++++++++++++++ internal/backtest/result.go | 32 +++++++++ internal/config/alpaca.go | 19 ++++++ internal/data/bars.go | 14 ++++ internal/logger/logger.go | 10 +++ internal/model/bar.go | 13 ++++ internal/model/trade.go | 14 ++++ internal/model/types.go | 10 +++ internal/strategy/ema_cross.go | 60 ++++++++++++++++ internal/strategy/strategy.go | 7 ++ output/backtest.xlsx | Bin 0 -> 8015 bytes 18 files changed, 580 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/backtest/main.go create mode 100644 go.mod create mode 100644 go.mod:Zone.Identifier create mode 100644 go.sum create mode 100644 internal/alpaca/client.go create mode 100644 internal/backtest/engine.go create mode 100644 internal/backtest/result.go create mode 100644 internal/config/alpaca.go create mode 100644 internal/data/bars.go create mode 100644 internal/logger/logger.go create mode 100644 internal/model/bar.go create mode 100644 internal/model/trade.go create mode 100644 internal/model/types.go create mode 100644 internal/strategy/ema_cross.go create mode 100644 internal/strategy/strategy.go create mode 100755 output/backtest.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d7629d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +Results/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd44c46 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# TradingBot + diff --git a/cmd/backtest/main.go b/cmd/backtest/main.go new file mode 100644 index 0000000..7466287 --- /dev/null +++ b/cmd/backtest/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + "time" + + "alpaca-bot/internal/alpaca" + "alpaca-bot/internal/backtest" + "alpaca-bot/internal/config" + "alpaca-bot/internal/logger" + "alpaca-bot/internal/strategy" + + "github.com/joho/godotenv" + "github.com/xuri/excelize/v2" +) + +func main() { + _ = godotenv.Load() + log := logger.New("[BACKTEST]") + + cfg := config.LoadAlpaca() + + // CLI flags + symbol := flag.String("symbol", "AAPL", "Stock symbol") + tf := flag.String("tf", "1Day", "Timeframe") + startStr := flag.String("start", "2023-01-01", "Start date") + endStr := flag.String("end", "2026-01-01", "End date") + cash := flag.Float64("cash", 10000, "Start capital") + risk := flag.Float64("risk", 0.02, "Risk per trade (fraction)") + atrPeriod := flag.Int("atr", 14, "ATR period") + atrMult := flag.Float64("atrMult", 1.5, "ATR multiplier for stop") + tpMult := flag.Float64("tpMult", 2.0, "Take profit multiplier") + fast := flag.Int("fast", 12, "Fast EMA") + slow := flag.Int("slow", 26, "Slow EMA") + out := flag.String("out", "output/backtest.xlsx", "Excel output path") + flag.Parse() + + start, err := time.Parse("2006-01-02", *startStr) + if err != nil { + log.Fatal("invalid start date:", err) + } + end, err := time.Parse("2006-01-02", *endStr) + if err != nil { + log.Fatal("invalid end date:", err) + } + + // Create output folder + if err := os.MkdirAll(filepath.Dir(*out), 0755); err != nil { + log.Fatal(err) + } + + // Load historical bars from Alpaca + client := alpaca.NewClient(cfg) + bars, err := client.GetHistoricalBars(*symbol, *tf, start, end) + if err != nil { + log.Fatal(err) + } + log.Printf("Loaded %d bars for %s", len(bars), *symbol) + + // Initialize EMA crossover strategy + strat := strategy.NewEMACross(*fast, *slow) + + // Initialize backtest engine with ATR stops and TakeProfit + engine := backtest.NewEngine(*symbol, strat, *cash, *risk, *atrPeriod, *atrMult, *tpMult) + engine.Run(bars) + + log.Printf("Backtest finished | Trades: %d | End Cash: %.2f", len(engine.Trades), engine.Cash) + + // Export trades to Excel + f := excelize.NewFile() + sheet := "Backtest" + f.NewSheet(sheet) + + headers := []string{"Time", "Symbol", "Side", "Price", "Qty", "Cash", "StopPrice", "TakeProfit"} + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheet, cell, h) + } + + for i, t := range engine.Trades { + row := i + 2 + values := []interface{}{t.Time.Format("2006-01-02 15:04:05"), t.Symbol, t.Side, t.Price, t.Qty, t.Cash, t.StopPrice, t.TakeProfit} + for colIdx, val := range values { + cell, _ := excelize.CoordinatesToCellName(colIdx+1, row) + f.SetCellValue(sheet, cell, val) + } + } + + if err := f.SaveAs(*out); err != nil { + log.Fatal("failed to save Excel:", err) + } + + log.Printf("Backtest Excel saved to %s", *out) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e78702e --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module alpaca-bot + +go 1.24.0 + +require ( + github.com/markcheno/go-talib v0.0.0-20250114000313-ec55a20c902f + github.com/xuri/excelize/v2 v2.10.0 +) + +require ( + cloud.google.com/go v0.118.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/shopspring/decimal v1.3.1 // indirect +) + +require ( + github.com/alpacahq/alpaca-trade-api-go/v3 v3.9.0 + github.com/joho/godotenv v1.5.1 + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/tiendc/go-deepcopy v1.7.1 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/text v0.30.0 // indirect +) diff --git a/go.mod:Zone.Identifier b/go.mod:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d70386c7502f8b2266c17c42f3bd0d724f88187b GIT binary patch literal 89 zcma!!%Fjy;DN4*MPD?F{<>dl#JyUFrdAWj8fg(kzMWIDGw$4^Dp~b01#W5bKc}0~m o{&}e`MVV!(F)pda*(Lb}F^M?^iOGq&N% 0 { + // stop loss hit + if bar.Low <= e.currentStop { + e.Cash += float64(e.Position) * e.currentStop + e.record("SELL_STOP", e.currentStop, bar.Time, e.Position, e.currentStop, 0) + e.Position = 0 + } else if bar.High >= e.currentTakeProfit { + e.Cash += float64(e.Position) * e.currentTakeProfit + e.record("SELL_TP", e.currentTakeProfit, bar.Time, e.Position, e.currentStop, e.currentTakeProfit) + e.Position = 0 + } + } + + // get new signal + signal := e.Strategy.OnBar(bar) + switch signal { + case model.Buy: + if e.Position == 0 { // only enter new position if flat + riskCash := e.Cash * e.RiskPercent + qty := int(riskCash / stopDistance) + if qty <= 0 || e.Cash < float64(qty)*bar.Close { + continue + } + e.Position = qty + e.Cash -= float64(qty) * bar.Close + e.entryPrice = bar.Close + e.currentStop = bar.Close - stopDistance + e.currentTakeProfit = bar.Close + stopDistance*e.TakeProfitMultiplier + e.record("BUY", bar.Close, bar.Time, qty, e.currentStop, e.currentTakeProfit) + } + case model.Sell: + if e.Position > 0 { // manual exit signal + e.Cash += float64(e.Position) * bar.Close + e.record("SELL_SIGNAL", bar.Close, bar.Time, e.Position, e.currentStop, e.currentTakeProfit) + e.Position = 0 + } + } + } +} + +// record logs each trade +func (e *Engine) record(side string, price float64, t time.Time, qty int, stopPrice float64, takeProfit float64) { + e.Trades = append(e.Trades, model.Trade{ + Time: t, + Symbol: e.Symbol, + Side: side, + Price: price, + Qty: qty, + Cash: e.Cash, + StopPrice: stopPrice, + TakeProfit: takeProfit, + }) +} diff --git a/internal/backtest/result.go b/internal/backtest/result.go new file mode 100644 index 0000000..096f2cc --- /dev/null +++ b/internal/backtest/result.go @@ -0,0 +1,32 @@ +package backtest + +import ( + "alpaca-bot/internal/model" + "fmt" + + "github.com/xuri/excelize/v2" +) + +func ExportToExcel(trades []model.Trade, path string) error { + f := excelize.NewFile() + sheet := "Backtest" + f.NewSheet(sheet) + + headers := []string{"Time", "Symbol", "Side", "Price", "Qty", "Cash"} + for i, h := range headers { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + f.SetCellValue(sheet, cell, h) + } + + for i, t := range trades { + row := i + 2 + f.SetCellValue(sheet, fmt.Sprintf("A%d", row), t.Time) + f.SetCellValue(sheet, fmt.Sprintf("B%d", row), t.Symbol) + f.SetCellValue(sheet, fmt.Sprintf("C%d", row), t.Side) + f.SetCellValue(sheet, fmt.Sprintf("D%d", row), t.Price) + f.SetCellValue(sheet, fmt.Sprintf("E%d", row), t.Qty) + f.SetCellValue(sheet, fmt.Sprintf("F%d", row), t.Cash) + } + + return f.SaveAs(path) +} diff --git a/internal/config/alpaca.go b/internal/config/alpaca.go new file mode 100644 index 0000000..01442d7 --- /dev/null +++ b/internal/config/alpaca.go @@ -0,0 +1,19 @@ +package config + +import "os" + +type AlpacaConfig struct { + ApiKey string + ApiSecret string + DataURL string + TradeURL string +} + +func LoadAlpaca() AlpacaConfig { + return AlpacaConfig{ + ApiKey: os.Getenv("ALPACA_API_KEY"), + ApiSecret: os.Getenv("ALPACA_API_SECRET"), + DataURL: os.Getenv("ALPACA_DATA_URL"), + TradeURL: os.Getenv("ALPACA_TRADE_URL"), + } +} diff --git a/internal/data/bars.go b/internal/data/bars.go new file mode 100644 index 0000000..6ca656d --- /dev/null +++ b/internal/data/bars.go @@ -0,0 +1,14 @@ +package data + +import "alpaca-bot/internal/model" + +func LoadBars(symbol string) []model.Bar { + return []model.Bar{ + {Symbol: symbol, Close: 150}, + {Symbol: symbol, Close: 152}, + {Symbol: symbol, Close: 149}, + {Symbol: symbol, Close: 155}, + {Symbol: symbol, Close: 160}, + {Symbol: symbol, Close: 158}, + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..f844127 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,10 @@ +package logger + +import ( + "log" + "os" +) + +func New(prefix string) *log.Logger { + return log.New(os.Stdout, prefix+" ", log.LstdFlags|log.Lshortfile) +} diff --git a/internal/model/bar.go b/internal/model/bar.go new file mode 100644 index 0000000..3a2f1f9 --- /dev/null +++ b/internal/model/bar.go @@ -0,0 +1,13 @@ +package model + +import "time" + +type Bar struct { + Symbol string + Time time.Time + Open float64 + High float64 + Low float64 + Close float64 + Volume int64 +} diff --git a/internal/model/trade.go b/internal/model/trade.go new file mode 100644 index 0000000..85598ea --- /dev/null +++ b/internal/model/trade.go @@ -0,0 +1,14 @@ +package model + +import "time" + +type Trade struct { + Time time.Time + Symbol string + Side string + Price float64 + Qty int + Cash float64 + StopPrice float64 // ATR-based stop + TakeProfit float64 // ATR-based take profit +} diff --git a/internal/model/types.go b/internal/model/types.go new file mode 100644 index 0000000..e728589 --- /dev/null +++ b/internal/model/types.go @@ -0,0 +1,10 @@ +package model + +type Signal int + +const ( + Hold Signal = iota + Buy + Sell +) + diff --git a/internal/strategy/ema_cross.go b/internal/strategy/ema_cross.go new file mode 100644 index 0000000..6e24014 --- /dev/null +++ b/internal/strategy/ema_cross.go @@ -0,0 +1,60 @@ +package strategy + +import ( + "alpaca-bot/internal/model" + + talib "github.com/markcheno/go-talib" +) + +type EMACross struct { + FastPeriod int + SlowPeriod int + prices []float64 + lastFast float64 + lastSlow float64 + init bool +} + +func NewEMACross(fast, slow int) *EMACross { + return &EMACross{ + FastPeriod: fast, + SlowPeriod: slow, + } +} + +func (s *EMACross) OnBar(bar model.Bar) model.Signal { + s.prices = append(s.prices, bar.Close) + + if len(s.prices) < s.SlowPeriod { + return model.Hold + } + + fast := talib.Ema(s.prices, s.FastPeriod) + slow := talib.Ema(s.prices, s.SlowPeriod) + + curFast := fast[len(fast)-1] + curSlow := slow[len(slow)-1] + + if !s.init { + s.lastFast = curFast + s.lastSlow = curSlow + s.init = true + return model.Hold + } + + if s.lastFast <= s.lastSlow && curFast > curSlow { + s.lastFast = curFast + s.lastSlow = curSlow + return model.Buy + } + + if s.lastFast >= s.lastSlow && curFast < curSlow { + s.lastFast = curFast + s.lastSlow = curSlow + return model.Sell + } + + s.lastFast = curFast + s.lastSlow = curSlow + return model.Hold +} diff --git a/internal/strategy/strategy.go b/internal/strategy/strategy.go new file mode 100644 index 0000000..5026a7c --- /dev/null +++ b/internal/strategy/strategy.go @@ -0,0 +1,7 @@ +package strategy + +import "alpaca-bot/internal/model" + +type Strategy interface { + OnBar(bar model.Bar) model.Signal +} diff --git a/output/backtest.xlsx b/output/backtest.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..e87ccb76ecb2c811d7474284fc4f7c696c913adb GIT binary patch literal 8015 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 G0QesVBiY9Q literal 0 HcmV?d00001