mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-14 07:53:51 +00:00
262 lines
7.4 KiB
Go
262 lines
7.4 KiB
Go
package autotrader
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-co-op/gocron"
|
|
)
|
|
|
|
// Trader acts as the primary interface to the broker and strategy. To the strategy, it provides all the information
|
|
// about the current state of the market and the portfolio. To the broker, it provides the orders to be executed and
|
|
// requests for the current state of the portfolio.
|
|
type Trader struct {
|
|
Broker Broker
|
|
Strategy Strategy
|
|
Symbol string
|
|
Frequency string
|
|
CandlesToKeep int
|
|
Log *log.Logger
|
|
EOF bool
|
|
|
|
data *IndexedFrame[UnixTime]
|
|
sched *gocron.Scheduler
|
|
stats *TraderStats
|
|
}
|
|
|
|
func (t *Trader) Data() *IndexedFrame[UnixTime] {
|
|
return t.data
|
|
}
|
|
|
|
type TradeStat struct {
|
|
Price float64 // Price is the price at which the trade was executed. If Exit is true, this is the exit price. Otherwise, this is the entry price.
|
|
Units float64 // Units is the signed number of units bought or sold.
|
|
Exit bool // Exit is true if the trade was to exit a previous position.
|
|
}
|
|
|
|
// Financial performance reporting and statistics.
|
|
type TraderStats struct {
|
|
Dated *Frame
|
|
returnsThisCandle float64
|
|
tradesThisCandle []TradeStat
|
|
}
|
|
|
|
func (t *Trader) Stats() *TraderStats {
|
|
return t.stats
|
|
}
|
|
|
|
// Run starts the trader. This is a blocking call.
|
|
func (t *Trader) Run() {
|
|
t.sched = gocron.NewScheduler(time.UTC)
|
|
capitalizedFreq := strings.ToUpper(t.Frequency)
|
|
if strings.HasPrefix(capitalizedFreq, "S") {
|
|
seconds, err := strconv.Atoi(t.Frequency[1:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
t.sched.Every(seconds).Seconds()
|
|
} else if strings.HasPrefix(capitalizedFreq, "M") {
|
|
minutes, err := strconv.Atoi(t.Frequency[1:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
t.sched.Every(minutes).Minutes()
|
|
} else if strings.HasPrefix(capitalizedFreq, "H") {
|
|
hours, err := strconv.Atoi(t.Frequency[1:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
t.sched.Every(hours).Hours()
|
|
} else {
|
|
switch capitalizedFreq {
|
|
case "D":
|
|
t.sched.Every(1).Day()
|
|
case "W":
|
|
t.sched.Every(1).Day()
|
|
case "M":
|
|
t.sched.Every(1).Day()
|
|
default:
|
|
panic(fmt.Sprintf("invalid frequency: %s", t.Frequency))
|
|
}
|
|
}
|
|
t.sched.Do(t.Tick) // Set the function to be run when the interval repeats.
|
|
|
|
t.Init()
|
|
t.sched.StartBlocking()
|
|
}
|
|
|
|
func (t *Trader) Init() {
|
|
t.Strategy.Init(t)
|
|
t.stats.Dated = NewFrame(
|
|
NewSeries("Date"),
|
|
NewSeries("Equity"),
|
|
NewSeries("Profit"),
|
|
NewSeries("Drawdown"),
|
|
NewSeries("Returns"),
|
|
NewSeries("Trades"), // []float64 representing the number of units traded positive for buy, negative for sell.
|
|
)
|
|
t.stats.tradesThisCandle = make([]TradeStat, 0, 2)
|
|
t.Broker.SignalConnect(OrderFulfilled, t, func(a ...any) {
|
|
order := a[0].(Order)
|
|
tradeStat := TradeStat{order.Position().EntryPrice(), order.Units(), false}
|
|
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
|
})
|
|
t.Broker.SignalConnect("PositionClosed", t, func(args ...any) {
|
|
position := args[0].(Position)
|
|
tradeStat := TradeStat{position.ClosePrice(), position.Units(), true}
|
|
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
|
t.stats.returnsThisCandle += position.PL()
|
|
})
|
|
}
|
|
|
|
// Tick updates the current state of the market and runs the strategy.
|
|
func (t *Trader) Tick() {
|
|
t.fetchData() // Fetch the latest candlesticks from the broker.
|
|
t.Strategy.Next(t) // Run the strategy.
|
|
|
|
// Update the stats.
|
|
err := t.stats.Dated.PushValues(map[string]any{
|
|
"Date": t.data.Date(-1).Time(),
|
|
"Equity": t.Broker.NAV(),
|
|
"Profit": t.Broker.PL(),
|
|
"Drawdown": func() float64 {
|
|
var bal float64
|
|
if t.stats.Dated.Len() > 0 {
|
|
bal = t.stats.Dated.Float("Equity", 0) // Take starting balance
|
|
} else {
|
|
bal = t.Broker.NAV() // Take current balance for first value
|
|
}
|
|
return Max(bal-t.Broker.NAV(), 0)
|
|
}(),
|
|
"Returns": func() any {
|
|
if t.stats.returnsThisCandle != 0 {
|
|
return t.stats.returnsThisCandle
|
|
} else {
|
|
return nil
|
|
}
|
|
}(),
|
|
"Trades": func() any {
|
|
if len(t.stats.tradesThisCandle) == 0 {
|
|
return nil
|
|
}
|
|
trades := make([]TradeStat, len(t.stats.tradesThisCandle))
|
|
copy(trades, t.stats.tradesThisCandle)
|
|
t.stats.tradesThisCandle = t.stats.tradesThisCandle[:0]
|
|
return trades
|
|
}(),
|
|
})
|
|
if err != nil {
|
|
log.Printf("error pushing values to stats dataframe: %v\n", err.Error())
|
|
}
|
|
t.stats.returnsThisCandle = 0
|
|
}
|
|
|
|
func (t *Trader) fetchData() {
|
|
var err error
|
|
t.data, err = t.Broker.Candles(t.Symbol, t.Frequency, t.CandlesToKeep)
|
|
if err == ErrEOF {
|
|
t.EOF = true
|
|
t.Log.Println("End of data")
|
|
if t.sched != nil && t.sched.IsRunning() {
|
|
t.sched.Clear()
|
|
}
|
|
} else if err != nil {
|
|
panic(err) // TODO: implement safe shutdown procedure
|
|
}
|
|
}
|
|
|
|
func (t *Trader) Order(orderType OrderType, units, price, stopLoss, takeProfit float64) (Order, error) {
|
|
var priceStr string
|
|
if orderType != Market { // Price is ignored on market orders.
|
|
priceStr = fmt.Sprintf(" @ $%.2f", price)
|
|
} else {
|
|
priceStr = fmt.Sprintf(" @ ~$%.2f", t.Broker.Price(t.Symbol, units > 0))
|
|
}
|
|
t.Log.Printf("%v %v units%v, stopLoss: %v, takeProfit: %v", orderType, units, priceStr, stopLoss, takeProfit)
|
|
|
|
order, err := t.Broker.Order(orderType, t.Symbol, units, price, stopLoss, takeProfit)
|
|
if err != nil {
|
|
return order, err
|
|
}
|
|
|
|
// NOTE: Trade stats get added by handling an event by the broker
|
|
return order, nil
|
|
}
|
|
|
|
// Buy creates a buy market order. Units must be greater than zero or ErrInvalidUnits is returned.
|
|
func (t *Trader) Buy(units, stopLoss, takeProfit float64) (Order, error) {
|
|
if units <= 0 {
|
|
return nil, ErrInvalidUnits
|
|
}
|
|
return t.Order(Market, units, 0, stopLoss, takeProfit)
|
|
}
|
|
|
|
// Sell creates a sell market order. Units must be greater than zero or ErrInvalidUnits is returned.
|
|
func (t *Trader) Sell(units, stopLoss, takeProfit float64) (Order, error) {
|
|
if units <= 0 {
|
|
return nil, ErrInvalidUnits
|
|
}
|
|
return t.Order(Market, -units, 0, stopLoss, takeProfit)
|
|
}
|
|
|
|
func (t *Trader) CloseOrdersAndPositions() {
|
|
for _, order := range t.Broker.OpenOrders() {
|
|
if order.Symbol() == t.Symbol {
|
|
t.Log.Printf("Cancelling order: %v units", order.Units())
|
|
order.Cancel()
|
|
}
|
|
}
|
|
for _, position := range t.Broker.OpenPositions() {
|
|
if position.Symbol() == t.Symbol {
|
|
t.Log.Printf("Closing position: %v units, $%.2f PL, ($%.2f -> $%.2f)", position.Units(), position.PL(), position.EntryPrice(), position.ClosePrice())
|
|
position.Close() // Event gets handled in the Init function
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Trader) IsLong() bool {
|
|
positions := t.Broker.OpenPositions()
|
|
if len(positions) < 1 {
|
|
return false
|
|
} else if len(positions) > 1 {
|
|
panic("cannot call IsLong with hedging enabled")
|
|
}
|
|
return positions[0].Units() > 0
|
|
}
|
|
|
|
func (t *Trader) IsShort() bool {
|
|
positions := t.Broker.OpenPositions()
|
|
if len(positions) < 1 {
|
|
return false
|
|
} else if len(positions) > 1 {
|
|
panic("cannot call IsShort with hedging enabled")
|
|
}
|
|
return positions[0].Units() < 0
|
|
}
|
|
|
|
type TraderConfig struct {
|
|
Broker Broker
|
|
Strategy Strategy
|
|
Symbol string
|
|
Frequency string
|
|
CandlesToKeep int
|
|
}
|
|
|
|
// NewTrader initializes a new Trader which can be used for live trading or backtesting.
|
|
func NewTrader(config TraderConfig) *Trader {
|
|
logger := log.New(os.Stdout, "autotrader: ", log.LstdFlags|log.Lshortfile)
|
|
return &Trader{
|
|
Broker: config.Broker,
|
|
Strategy: config.Strategy,
|
|
Symbol: config.Symbol,
|
|
Frequency: config.Frequency,
|
|
CandlesToKeep: config.CandlesToKeep,
|
|
Log: logger,
|
|
stats: &TraderStats{},
|
|
}
|
|
}
|