commit 2b892123e1290e8ff2323dc92ddad9a70612564b Author: hwinkel Date: Wed Jan 14 22:44:12 2026 +0100 ADD: added first version 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 0000000..d70386c Binary files /dev/null and b/go.mod:Zone.Identifier differ diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..66024a8 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ= +cloud.google.com/go v0.118.0/go.mod h1:zIt2pkedt/mo+DQjcT4/L3NDxzHPR29j5HcclNH+9PM= +github.com/alpacahq/alpaca-trade-api-go/v3 v3.9.0 h1:UqrbAa9gncu6GeCxf6vs09jw/n/o+pd6nziRjk3Twjg= +github.com/alpacahq/alpaca-trade-api-go/v3 v3.9.0/go.mod h1:BM5f01Jh+mmcEK/Y5kS6XsQojVSuUM8HL4MQgrRtyis= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markcheno/go-talib v0.0.0-20250114000313-ec55a20c902f h1:iKq//xEUUaeRoXNcAshpK4W8eSm7HtgI0aNznWtX7lk= +github.com/markcheno/go-talib v0.0.0-20250114000313-ec55a20c902f/go.mod h1:3YUtoVrKWu2ql+iAeRyepSz3fy6a+19hJzGS88+u4u0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= +github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= +github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/alpaca/client.go b/internal/alpaca/client.go new file mode 100644 index 0000000..137b7bc --- /dev/null +++ b/internal/alpaca/client.go @@ -0,0 +1,82 @@ +package alpaca + +import ( + "time" + + "alpaca-bot/internal/config" + "alpaca-bot/internal/model" + + "github.com/alpacahq/alpaca-trade-api-go/v3/alpaca" + "github.com/alpacahq/alpaca-trade-api-go/v3/marketdata" + "github.com/shopspring/decimal" +) + +type Client struct { + alpacaClient *alpaca.Client + mdClient *marketdata.Client +} + +// Create new Alpaca client +func NewClient(cfg config.AlpacaConfig) *Client { + c := alpaca.NewClient(alpaca.ClientOpts{ + APIKey: cfg.ApiKey, + APISecret: cfg.ApiSecret, + BaseURL: cfg.TradeURL, + }) + + md := marketdata.NewClient(marketdata.ClientOpts{ + APIKey: cfg.ApiKey, + APISecret: cfg.ApiSecret, + }) + + return &Client{ + alpacaClient: c, + mdClient: md, + } +} + +// Fetch historical bars +func (c *Client) GetHistoricalBars(symbol string, timeframe string, start, end time.Time) ([]model.Bar, error) { + req := marketdata.GetBarsRequest{ + TimeFrame: marketdata.OneHour, // e.g. "1Day", "1Min" + Start: start, + End: end, + } + + resp, err := c.mdClient.GetBars(symbol, req) + if err != nil { + return nil, err + } + + bars := []model.Bar{} + for _, b := range resp { + bars = append(bars, model.Bar{ + Symbol: symbol, + Time: b.Timestamp, + Open: b.Open, + High: b.High, + Low: b.Low, + Close: b.Close, + // Volume: b.Volume, + }) + } + + return bars, nil +} + +// Example: submit an order +func (c *Client) SubmitOrder(symbol string, qty *decimal.Decimal, side alpaca.Side, orderType alpaca.OrderType, timeInForce alpaca.TimeInForce) (*alpaca.Order, error) { + orderReq := alpaca.PlaceOrderRequest{ + Symbol: symbol, + Qty: qty, + Side: side, + Type: orderType, + TimeInForce: timeInForce, + } + + order, err := c.alpacaClient.PlaceOrder(orderReq) + if err != nil { + return nil, err + } + return order, nil +} diff --git a/internal/backtest/engine.go b/internal/backtest/engine.go new file mode 100644 index 0000000..750da8d --- /dev/null +++ b/internal/backtest/engine.go @@ -0,0 +1,121 @@ +package backtest + +import ( + "alpaca-bot/internal/model" + "alpaca-bot/internal/strategy" + "time" + + talib "github.com/markcheno/go-talib" +) + +type Engine struct { + Strategy strategy.Strategy + Cash float64 + Position int + Trades []model.Trade + Symbol string + RiskPercent float64 // fraction of cash to risk per trade + ATRPeriod int + ATRMultiplier float64 + TakeProfitMultiplier float64 // e.g., 2 for 2:1 reward/risk + highs, lows, closes []float64 + currentStop float64 + currentTakeProfit float64 + entryPrice float64 +} + +func NewEngine(symbol string, strat strategy.Strategy, cash float64, risk float64, atrPeriod int, atrMult float64, tpMult float64) *Engine { + return &Engine{ + Symbol: symbol, + Strategy: strat, + Cash: cash, + RiskPercent: risk, + ATRPeriod: atrPeriod, + ATRMultiplier: atrMult, + TakeProfitMultiplier: tpMult, + } +} + +// Run executes the backtest with ATR stops and take profit +func (e *Engine) Run(bars []model.Bar) { + for _, bar := range bars { + // collect prices for ATR + e.highs = append(e.highs, bar.High) + e.lows = append(e.lows, bar.Low) + e.closes = append(e.closes, bar.Close) + + // skip until enough bars for ATR + if len(e.closes) <= e.ATRPeriod { + continue + } + + // calculate ATR safely + highsSlice := e.highs[len(e.highs)-e.ATRPeriod-1:] + lowsSlice := e.lows[len(e.lows)-e.ATRPeriod-1:] + closesSlice := e.closes[len(e.closes)-e.ATRPeriod-1:] + + atrValues := talib.Atr(highsSlice, lowsSlice, closesSlice, e.ATRPeriod) + if len(atrValues) == 0 { + continue + } + atr := atrValues[len(atrValues)-1] + if atr <= 0 { + continue + } + + stopDistance := atr * e.ATRMultiplier + + // check existing position for stop loss / take profit + if e.Position > 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 0000000..e87ccb7 Binary files /dev/null and b/output/backtest.xlsx differ