mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-15 00:13:51 +00:00
Making trade charting more reliable and informative
This commit is contained in:
parent
d755f07d38
commit
f1c45dbb74
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@ -10,6 +10,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "test",
|
"mode": "test",
|
||||||
"program": "${fileDirname}",
|
"program": "${fileDirname}",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/cmd/sma_crossover.go",
|
"program": "${workspaceFolder}/cmd/sma_crossover.go",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -26,6 +28,16 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}/cmd/ichimoku.go",
|
"program": "${workspaceFolder}/cmd/ichimoku.go",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run Spread Example",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/spread_example.go",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -22,7 +22,7 @@ var (
|
|||||||
ErrEOF = errors.New("end of the input data")
|
ErrEOF = errors.New("end of the input data")
|
||||||
ErrNoData = errors.New("no data")
|
ErrNoData = errors.New("no data")
|
||||||
ErrPositionClosed = errors.New("position already closed")
|
ErrPositionClosed = errors.New("position already closed")
|
||||||
ErrZeroUnits = errors.New("no amount of units specifed")
|
ErrInvalidUnits = errors.New("the units provided failed to meet the criteria")
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Broker = (*TestBroker)(nil) // Compile-time interface check.
|
var _ Broker = (*TestBroker)(nil) // Compile-time interface check.
|
||||||
@ -452,14 +452,22 @@ func (b *TestBroker) Price(symbol string, wantToBuy bool) float64 {
|
|||||||
return b.Bid(symbol)
|
return b.Bid(symbol)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *TestBroker) lastClose() float64 {
|
||||||
|
if b.CandleIndex() < b.Data.Len() {
|
||||||
|
return b.Data.Close(b.CandleIndex())
|
||||||
|
} else {
|
||||||
|
return b.Data.Close(-1) // If we are at end of data, then grab the last candlestick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bid returns the price a seller receives for the current candle.
|
// Bid returns the price a seller receives for the current candle.
|
||||||
func (b *TestBroker) Bid(_ string) float64 {
|
func (b *TestBroker) Bid(_ string) float64 {
|
||||||
return b.Data.Close(b.CandleIndex())
|
return b.lastClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask returns the price a buyer pays for the current candle.
|
// Ask returns the price a buyer pays for the current candle.
|
||||||
func (b *TestBroker) Ask(_ string) float64 {
|
func (b *TestBroker) Ask(_ string) float64 {
|
||||||
return b.Data.Close(b.CandleIndex()) + b.Spread
|
return b.lastClose() + b.Spread
|
||||||
}
|
}
|
||||||
|
|
||||||
// Candles returns the last count candles for the given symbol and frequency. If count is greater than the number of candles, then a dataframe with zero rows is returned.
|
// Candles returns the last count candles for the given symbol and frequency. If count is greater than the number of candles, then a dataframe with zero rows is returned.
|
||||||
@ -485,7 +493,7 @@ func (b *TestBroker) Candles(symbol string, frequency string, count int) (*Index
|
|||||||
|
|
||||||
func (b *TestBroker) Order(orderType OrderType, symbol string, units, price, stopLoss, takeProfit float64) (Order, error) {
|
func (b *TestBroker) Order(orderType OrderType, symbol string, units, price, stopLoss, takeProfit float64) (Order, error) {
|
||||||
if units == 0 {
|
if units == 0 {
|
||||||
return nil, ErrZeroUnits
|
return nil, ErrInvalidUnits
|
||||||
}
|
}
|
||||||
if b.Data == nil { // The DataBroker could have data but nobody has fetched it, yet.
|
if b.Data == nil { // The DataBroker could have data but nobody has fetched it, yet.
|
||||||
if b.DataBroker == nil {
|
if b.DataBroker == nil {
|
||||||
@ -537,7 +545,7 @@ func (b *TestBroker) Order(orderType OrderType, symbol string, units, price, sto
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.orders = append(b.orders, order)
|
b.orders = append(b.orders, order)
|
||||||
b.SignalEmit("OrderPlaced", order)
|
b.SignalEmit(OrderPlaced, order)
|
||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
@ -598,16 +606,16 @@ type TestPosition struct {
|
|||||||
id string
|
id string
|
||||||
leverage float64
|
leverage float64
|
||||||
symbol string
|
symbol string
|
||||||
trailingSL float64 // the price of the trailing stop loss as assigned by broker Tick().
|
trailingSL float64 // The price of the trailing stop loss as assigned by broker Tick().
|
||||||
trailingSLDist float64 // serves to calculate the trailing stop loss at the broker.
|
trailingSLDist float64 // Serves to calculate the trailing stop loss at the broker.
|
||||||
stopLoss float64
|
stopLoss float64
|
||||||
takeProfit float64
|
takeProfit float64
|
||||||
time time.Time
|
time time.Time
|
||||||
units float64
|
units float64 // Is negative if this is a short position or positive for long.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TestPosition) Close() error {
|
func (p *TestPosition) Close() error {
|
||||||
p.close(p.broker.Price("", p.units < 0), CloseMarket)
|
p.close(p.broker.Price(p.symbol, p.units < 0), CloseMarket)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,7 +628,7 @@ func (p *TestPosition) close(atPrice float64, closeType OrderCloseType) {
|
|||||||
p.closeType = closeType
|
p.closeType = closeType
|
||||||
p.broker.Cash += p.Value() // Return the value of the position to the broker.
|
p.broker.Cash += p.Value() // Return the value of the position to the broker.
|
||||||
p.broker.spreadCollectedUSD += p.broker.Spread * math.Abs(p.units) * p.closePrice
|
p.broker.spreadCollectedUSD += p.broker.Spread * math.Abs(p.units) * p.closePrice
|
||||||
p.broker.SignalEmit("PositionClosed", p)
|
p.broker.SignalEmit(PositionClosed, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *TestPosition) Closed() bool {
|
func (p *TestPosition) Closed() bool {
|
||||||
@ -707,7 +715,7 @@ func (o *TestOrder) Cancel() error {
|
|||||||
|
|
||||||
func (o *TestOrder) fulfill(atPrice float64) {
|
func (o *TestOrder) fulfill(atPrice float64) {
|
||||||
slippage := rand.Float64() * o.broker.Slippage * atPrice
|
slippage := rand.Float64() * o.broker.Slippage * atPrice
|
||||||
atPrice += slippage - slippage/2 // Adjust price as +/- 50% of the slippage.
|
atPrice += slippage / 2 // Adjust price as +/- 50% of the slippage.
|
||||||
|
|
||||||
o.position = &TestPosition{
|
o.position = &TestPosition{
|
||||||
broker: o.broker,
|
broker: o.broker,
|
||||||
@ -725,9 +733,11 @@ func (o *TestOrder) fulfill(atPrice float64) {
|
|||||||
} else {
|
} else {
|
||||||
o.position.stopLoss = o.stopLoss
|
o.position.stopLoss = o.stopLoss
|
||||||
}
|
}
|
||||||
|
// TODO: cash should be a function because position values change over time and you will pay for losses in realtime
|
||||||
o.broker.Cash -= o.position.EntryValue()
|
o.broker.Cash -= o.position.EntryValue()
|
||||||
|
|
||||||
o.broker.positions = append(o.broker.positions, o.position)
|
o.broker.positions = append(o.broker.positions, o.position)
|
||||||
|
o.broker.SignalEmit(OrderFulfilled, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *TestOrder) Fulfilled() bool {
|
func (o *TestOrder) Fulfilled() bool {
|
||||||
|
10
broker.go
10
broker.go
@ -12,14 +12,20 @@ const (
|
|||||||
CloseStopLoss OrderCloseType = "SL"
|
CloseStopLoss OrderCloseType = "SL"
|
||||||
CloseTrailingStop OrderCloseType = "TS"
|
CloseTrailingStop OrderCloseType = "TS"
|
||||||
CloseTakeProfit OrderCloseType = "TP"
|
CloseTakeProfit OrderCloseType = "TP"
|
||||||
|
|
||||||
|
OrderPlaced = "OrderPlaced"
|
||||||
|
OrderCancelled = "OrderCancelled"
|
||||||
|
OrderFulfilled = "OrderFulfilled"
|
||||||
|
|
||||||
|
PositionClosed = "PositionClosed"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrderType string
|
type OrderType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Market OrderType = "MARKET" // Market means to buy or sell at the current market price, which may not be what you ask for.
|
Market OrderType = "MARKET" // Market means to buy or sell at the current market price, which may not always be what you expect.
|
||||||
Limit OrderType = "LIMIT" // Limit means to buy or sell at a specific price or better.
|
Limit OrderType = "LIMIT" // Limit means to buy or sell at a specific price or better.
|
||||||
Stop OrderType = "STOP" // Stop means to buy or sell when the price reaches a specific price or worse.
|
Stop OrderType = "STOP" // Stop means to buy or sell when the price reaches a specific price or ASAP.
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -20,10 +20,13 @@ func (s *SMAStrategy) Init(_ *auto.Trader) {
|
|||||||
func (s *SMAStrategy) Next(t *auto.Trader) {
|
func (s *SMAStrategy) Next(t *auto.Trader) {
|
||||||
sma1 := t.Data().Closes().Copy().Rolling(s.period1).Mean()
|
sma1 := t.Data().Closes().Copy().Rolling(s.period1).Mean()
|
||||||
sma2 := t.Data().Closes().Copy().Rolling(s.period2).Mean()
|
sma2 := t.Data().Closes().Copy().Rolling(s.period2).Mean()
|
||||||
// If the shorter SMA crosses above the longer SMA, buy.
|
|
||||||
|
// If the shorter SMA (sma1) crosses above the longer SMA (sma2), buy.
|
||||||
if auto.CrossoverIndex(*t.Data().Date(-1), sma1, sma2) {
|
if auto.CrossoverIndex(*t.Data().Date(-1), sma1, sma2) {
|
||||||
|
t.CloseOrdersAndPositions()
|
||||||
t.Buy(1000, 0, 0)
|
t.Buy(1000, 0, 0)
|
||||||
} else if auto.CrossoverIndex(*t.Data().Date(-1), sma2, sma1) {
|
} else if auto.CrossoverIndex(*t.Data().Date(-1), sma2, sma1) {
|
||||||
|
t.CloseOrdersAndPositions()
|
||||||
t.Sell(1000, 0, 0)
|
t.Sell(1000, 0, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,8 +44,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto.Backtest(auto.NewTrader(auto.TraderConfig{
|
auto.Backtest(auto.NewTrader(auto.TraderConfig{
|
||||||
Broker: auto.NewTestBroker(broker /* data, */, nil, 10000, 50, 0.0002, 0),
|
Broker: auto.NewTestBroker(broker, nil, 10000, 50, 0.0002, 0),
|
||||||
Strategy: &SMAStrategy{period1: 10, period2: 25},
|
Strategy: &SMAStrategy{period1: 7, period2: 20},
|
||||||
Symbol: "EUR_USD",
|
Symbol: "EUR_USD",
|
||||||
Frequency: "M15",
|
Frequency: "M15",
|
||||||
CandlesToKeep: 2500,
|
CandlesToKeep: 2500,
|
||||||
|
49
cmd/spread_example.go
Normal file
49
cmd/spread_example.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//go:build ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
auto "github.com/fivemoreminix/autotrader"
|
||||||
|
"github.com/fivemoreminix/autotrader/oanda"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SpreadPayupStrategy struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpreadPayupStrategy) Init(t *auto.Trader) {
|
||||||
|
t.Broker.SignalConnect(auto.OrderFulfilled, s, func(a ...any) {
|
||||||
|
order := a[0].(auto.Order)
|
||||||
|
order.Position().Close() // Immediately close the position so we only pay spread.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SpreadPayupStrategy) Next(t *auto.Trader) {
|
||||||
|
_, err := t.Sell(1000, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
/* data, err := auto.EURUSD()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
broker, err := oanda.NewOandaBroker(os.Getenv("OANDA_TOKEN"), os.Getenv("OANDA_ACCOUNT_ID"), true)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auto.Backtest(auto.NewTrader(auto.TraderConfig{
|
||||||
|
Broker: auto.NewTestBroker(broker, nil, 10000, 50, 0.0002, 0),
|
||||||
|
Strategy: &SpreadPayupStrategy{},
|
||||||
|
Symbol: "EUR_USD",
|
||||||
|
Frequency: "M15",
|
||||||
|
CandlesToKeep: 1000,
|
||||||
|
}))
|
||||||
|
}
|
58
trader.go
58
trader.go
@ -100,8 +100,15 @@ func (t *Trader) Init() {
|
|||||||
NewSeries("Trades"), // []float64 representing the number of units traded positive for buy, negative for sell.
|
NewSeries("Trades"), // []float64 representing the number of units traded positive for buy, negative for sell.
|
||||||
)
|
)
|
||||||
t.stats.tradesThisCandle = make([]TradeStat, 0, 2)
|
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) {
|
t.Broker.SignalConnect("PositionClosed", t, func(args ...any) {
|
||||||
position := args[0].(Position)
|
position := args[0].(Position)
|
||||||
|
tradeStat := TradeStat{position.ClosePrice(), position.Units(), true}
|
||||||
|
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
||||||
t.stats.returnsThisCandle += position.PL()
|
t.stats.returnsThisCandle += position.PL()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -162,22 +169,38 @@ func (t *Trader) fetchData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) Buy(units, stopLoss, takeProfit float64) {
|
func (t *Trader) Order(orderType OrderType, units, price, stopLoss, takeProfit float64) (Order, error) {
|
||||||
t.CloseOrdersAndPositions()
|
var priceStr string
|
||||||
t.Log.Printf("Buy %v units", units)
|
if orderType != Market { // Price is ignored on market orders.
|
||||||
t.Broker.Order(Market, t.Symbol, units, 0, stopLoss, takeProfit)
|
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)
|
||||||
|
|
||||||
tradeStat := TradeStat{t.Broker.Ask(t.Symbol), units, false}
|
order, err := t.Broker.Order(orderType, t.Symbol, units, price, stopLoss, takeProfit)
|
||||||
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
if err != nil {
|
||||||
|
return order, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Trade stats get added by handling an event by the broker
|
||||||
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) Sell(units, stopLoss, takeProfit float64) {
|
// Buy creates a buy market order. Units must be greater than zero or ErrInvalidUnits is returned.
|
||||||
t.CloseOrdersAndPositions()
|
func (t *Trader) Buy(units, stopLoss, takeProfit float64) (Order, error) {
|
||||||
t.Log.Printf("Sell %v units", units)
|
if units <= 0 {
|
||||||
t.Broker.Order(Market, t.Symbol, -units, 0, stopLoss, takeProfit)
|
return nil, ErrInvalidUnits
|
||||||
|
}
|
||||||
|
return t.Order(Market, units, 0, stopLoss, takeProfit)
|
||||||
|
}
|
||||||
|
|
||||||
tradeStat := TradeStat{t.Broker.Bid(t.Symbol), units, false}
|
// Sell creates a sell market order. Units must be greater than zero or ErrInvalidUnits is returned.
|
||||||
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
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() {
|
func (t *Trader) CloseOrdersAndPositions() {
|
||||||
@ -189,18 +212,15 @@ func (t *Trader) CloseOrdersAndPositions() {
|
|||||||
}
|
}
|
||||||
for _, position := range t.Broker.OpenPositions() {
|
for _, position := range t.Broker.OpenPositions() {
|
||||||
if position.Symbol() == t.Symbol {
|
if position.Symbol() == t.Symbol {
|
||||||
t.Log.Printf("Closing position: %v units, $%.2f PL", position.Units(), position.PL())
|
t.Log.Printf("Closing position: %v units, $%.2f PL, ($%.2f -> $%.2f)", position.Units(), position.PL(), position.EntryPrice(), position.ClosePrice())
|
||||||
position.Close()
|
position.Close() // Event gets handled in the Init function
|
||||||
|
|
||||||
tradeStat := TradeStat{t.Broker.Price(t.Symbol, position.Units() < 0), position.Units(), true}
|
|
||||||
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Trader) IsLong() bool {
|
func (t *Trader) IsLong() bool {
|
||||||
positions := t.Broker.OpenPositions()
|
positions := t.Broker.OpenPositions()
|
||||||
if len(positions) <= 0 {
|
if len(positions) < 1 {
|
||||||
return false
|
return false
|
||||||
} else if len(positions) > 1 {
|
} else if len(positions) > 1 {
|
||||||
panic("cannot call IsLong with hedging enabled")
|
panic("cannot call IsLong with hedging enabled")
|
||||||
@ -210,7 +230,7 @@ func (t *Trader) IsLong() bool {
|
|||||||
|
|
||||||
func (t *Trader) IsShort() bool {
|
func (t *Trader) IsShort() bool {
|
||||||
positions := t.Broker.OpenPositions()
|
positions := t.Broker.OpenPositions()
|
||||||
if len(positions) <= 0 {
|
if len(positions) < 1 {
|
||||||
return false
|
return false
|
||||||
} else if len(positions) > 1 {
|
} else if len(positions) > 1 {
|
||||||
panic("cannot call IsShort with hedging enabled")
|
panic("cannot call IsShort with hedging enabled")
|
||||||
|
1
utils.go
1
utils.go
@ -18,6 +18,7 @@ func Crossover(a, b *Series) bool {
|
|||||||
return a.Float(-1) > b.Float(-1) && a.Float(-2) <= b.Float(-2)
|
return a.Float(-1) > b.Float(-1) && a.Float(-2) <= b.Float(-2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CrossoverIndex is similar to Crossover, except that it works for IndexedSeries.
|
||||||
func CrossoverIndex[I Index](index I, a, b *IndexedSeries[I]) bool {
|
func CrossoverIndex[I Index](index I, a, b *IndexedSeries[I]) bool {
|
||||||
aRow, bRow := a.Row(index), b.Row(index)
|
aRow, bRow := a.Row(index), b.Row(index)
|
||||||
if aRow < 1 || bRow < 1 {
|
if aRow < 1 || bRow < 1 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user