mirror of
https://github.com/lukewilson2002/autotrader.git
synced 2025-06-14 16:03: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",
|
||||
"mode": "test",
|
||||
"program": "${fileDirname}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
@ -18,6 +19,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}/cmd/sma_crossover.go",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
@ -26,6 +28,16 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"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}"
|
||||
}
|
||||
]
|
||||
|
@ -22,7 +22,7 @@ var (
|
||||
ErrEOF = errors.New("end of the input data")
|
||||
ErrNoData = errors.New("no data")
|
||||
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.
|
||||
@ -452,14 +452,22 @@ func (b *TestBroker) Price(symbol string, wantToBuy bool) float64 {
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
@ -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) {
|
||||
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.DataBroker == nil {
|
||||
@ -537,7 +545,7 @@ func (b *TestBroker) Order(orderType OrderType, symbol string, units, price, sto
|
||||
}
|
||||
|
||||
b.orders = append(b.orders, order)
|
||||
b.SignalEmit("OrderPlaced", order)
|
||||
b.SignalEmit(OrderPlaced, order)
|
||||
|
||||
return order, nil
|
||||
}
|
||||
@ -598,16 +606,16 @@ type TestPosition struct {
|
||||
id string
|
||||
leverage float64
|
||||
symbol string
|
||||
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.
|
||||
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.
|
||||
stopLoss float64
|
||||
takeProfit float64
|
||||
time time.Time
|
||||
units float64
|
||||
units float64 // Is negative if this is a short position or positive for long.
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -620,7 +628,7 @@ func (p *TestPosition) close(atPrice float64, closeType OrderCloseType) {
|
||||
p.closeType = closeType
|
||||
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.SignalEmit("PositionClosed", p)
|
||||
p.broker.SignalEmit(PositionClosed, p)
|
||||
}
|
||||
|
||||
func (p *TestPosition) Closed() bool {
|
||||
@ -707,7 +715,7 @@ func (o *TestOrder) Cancel() error {
|
||||
|
||||
func (o *TestOrder) fulfill(atPrice float64) {
|
||||
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{
|
||||
broker: o.broker,
|
||||
@ -725,9 +733,11 @@ func (o *TestOrder) fulfill(atPrice float64) {
|
||||
} else {
|
||||
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.positions = append(o.broker.positions, o.position)
|
||||
o.broker.SignalEmit(OrderFulfilled, o)
|
||||
}
|
||||
|
||||
func (o *TestOrder) Fulfilled() bool {
|
||||
|
10
broker.go
10
broker.go
@ -12,14 +12,20 @@ const (
|
||||
CloseStopLoss OrderCloseType = "SL"
|
||||
CloseTrailingStop OrderCloseType = "TS"
|
||||
CloseTakeProfit OrderCloseType = "TP"
|
||||
|
||||
OrderPlaced = "OrderPlaced"
|
||||
OrderCancelled = "OrderCancelled"
|
||||
OrderFulfilled = "OrderFulfilled"
|
||||
|
||||
PositionClosed = "PositionClosed"
|
||||
)
|
||||
|
||||
type OrderType string
|
||||
|
||||
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.
|
||||
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 (
|
||||
|
@ -20,10 +20,13 @@ func (s *SMAStrategy) Init(_ *auto.Trader) {
|
||||
func (s *SMAStrategy) Next(t *auto.Trader) {
|
||||
sma1 := t.Data().Closes().Copy().Rolling(s.period1).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) {
|
||||
t.CloseOrdersAndPositions()
|
||||
t.Buy(1000, 0, 0)
|
||||
} else if auto.CrossoverIndex(*t.Data().Date(-1), sma2, sma1) {
|
||||
t.CloseOrdersAndPositions()
|
||||
t.Sell(1000, 0, 0)
|
||||
}
|
||||
}
|
||||
@ -41,8 +44,8 @@ func main() {
|
||||
}
|
||||
|
||||
auto.Backtest(auto.NewTrader(auto.TraderConfig{
|
||||
Broker: auto.NewTestBroker(broker /* data, */, nil, 10000, 50, 0.0002, 0),
|
||||
Strategy: &SMAStrategy{period1: 10, period2: 25},
|
||||
Broker: auto.NewTestBroker(broker, nil, 10000, 50, 0.0002, 0),
|
||||
Strategy: &SMAStrategy{period1: 7, period2: 20},
|
||||
Symbol: "EUR_USD",
|
||||
Frequency: "M15",
|
||||
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.
|
||||
)
|
||||
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()
|
||||
})
|
||||
}
|
||||
@ -162,22 +169,38 @@ func (t *Trader) fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Trader) Buy(units, stopLoss, takeProfit float64) {
|
||||
t.CloseOrdersAndPositions()
|
||||
t.Log.Printf("Buy %v units", units)
|
||||
t.Broker.Order(Market, t.Symbol, units, 0, stopLoss, takeProfit)
|
||||
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)
|
||||
|
||||
tradeStat := TradeStat{t.Broker.Ask(t.Symbol), units, false}
|
||||
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
||||
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
|
||||
}
|
||||
|
||||
func (t *Trader) Sell(units, stopLoss, takeProfit float64) {
|
||||
t.CloseOrdersAndPositions()
|
||||
t.Log.Printf("Sell %v units", units)
|
||||
t.Broker.Order(Market, t.Symbol, -units, 0, stopLoss, takeProfit)
|
||||
// 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)
|
||||
}
|
||||
|
||||
tradeStat := TradeStat{t.Broker.Bid(t.Symbol), units, false}
|
||||
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
||||
// 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() {
|
||||
@ -189,18 +212,15 @@ func (t *Trader) CloseOrdersAndPositions() {
|
||||
}
|
||||
for _, position := range t.Broker.OpenPositions() {
|
||||
if position.Symbol() == t.Symbol {
|
||||
t.Log.Printf("Closing position: %v units, $%.2f PL", position.Units(), position.PL())
|
||||
position.Close()
|
||||
|
||||
tradeStat := TradeStat{t.Broker.Price(t.Symbol, position.Units() < 0), position.Units(), true}
|
||||
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, tradeStat)
|
||||
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) <= 0 {
|
||||
if len(positions) < 1 {
|
||||
return false
|
||||
} else if len(positions) > 1 {
|
||||
panic("cannot call IsLong with hedging enabled")
|
||||
@ -210,7 +230,7 @@ func (t *Trader) IsLong() bool {
|
||||
|
||||
func (t *Trader) IsShort() bool {
|
||||
positions := t.Broker.OpenPositions()
|
||||
if len(positions) <= 0 {
|
||||
if len(positions) < 1 {
|
||||
return false
|
||||
} else if len(positions) > 1 {
|
||||
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)
|
||||
}
|
||||
|
||||
// CrossoverIndex is similar to Crossover, except that it works for IndexedSeries.
|
||||
func CrossoverIndex[I Index](index I, a, b *IndexedSeries[I]) bool {
|
||||
aRow, bRow := a.Row(index), b.Row(index)
|
||||
if aRow < 1 || bRow < 1 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user