Implemented TP, SL, TSL

This commit is contained in:
Luke I. Wilson 2023-05-19 02:22:19 -05:00
parent 5a0a4d0c33
commit 56740b5a00
4 changed files with 256 additions and 69 deletions

View File

@ -20,7 +20,8 @@ import (
var ( 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 closed") ErrPositionClosed = errors.New("position already closed")
ErrZeroUnits = errors.New("no amount of units specifed")
) )
var _ Broker = (*TestBroker)(nil) // Compile-time interface check. var _ Broker = (*TestBroker)(nil) // Compile-time interface check.
@ -56,9 +57,11 @@ func Backtest(trader *Trader) {
{ {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
fmt.Fprintln(w) fmt.Fprintln(w)
fmt.Fprintf(w, "Timespan:\t%s\t\n", stats.Dated.Date(-1).Sub(stats.Dated.Date(0)).Round(time.Second))
fmt.Fprintf(w, "Net Profit:\t$%.2f (%.2f%%)\t\n", profit, 100*profit/stats.Dated.Float("Equity", 0)) fmt.Fprintf(w, "Net Profit:\t$%.2f (%.2f%%)\t\n", profit, 100*profit/stats.Dated.Float("Equity", 0))
fmt.Fprintf(w, "Profit Factor:\t%.2f\t\n", profitFactor) fmt.Fprintf(w, "Profit Factor:\t%.2f\t\n", profitFactor)
fmt.Fprintf(w, "Max Drawdown:\t$%.2f (%.2f%%)\t\n", maxDrawdown, maxDrawdownPct) fmt.Fprintf(w, "Max Drawdown:\t$%.2f (%.2f%%)\t\n", maxDrawdown, maxDrawdownPct)
fmt.Fprintf(w, "Spread collected:\t$%.2f\t\n", broker.spreadCollectedUSD)
fmt.Fprintln(w) fmt.Fprintln(w)
w.Flush() w.Flush()
} }
@ -328,9 +331,10 @@ type TestBroker struct {
Spread float64 // Number of pips to add to the price when buying and subtract when selling. (Forex) Spread float64 // Number of pips to add to the price when buying and subtract when selling. (Forex)
Slippage float64 // A percentage of the price to add when buying and subtract when selling. Slippage float64 // A percentage of the price to add when buying and subtract when selling.
candleCount int // The number of candles anyone outside this broker has seen. Also equal to the number of times Candles has been called. candleCount int // The number of candles anyone outside this broker has seen. Also equal to the number of times Candles has been called.
orders []Order orders []Order
positions []Position positions []Position
spreadCollectedUSD float64 // Total amount of spread collected from trades.
} }
func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread float64, startCandles int) *TestBroker { func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread float64, startCandles int) *TestBroker {
@ -340,25 +344,70 @@ func NewTestBroker(dataBroker Broker, data *DataFrame, cash, leverage, spread fl
Cash: cash, Cash: cash,
Leverage: Max(leverage, 1), Leverage: Max(leverage, 1),
Spread: spread, Spread: spread,
Slippage: 0.005, // Price +/- 0.5% Slippage: 0.005, // Price +/- up to 0.5% by a random amount.
candleCount: Max(startCandles, 1), candleCount: Max(startCandles, 1),
} }
} }
// SpreadCollected returns the total amount of spread collected from trades, in USD.
func (b *TestBroker) SpreadCollected() float64 {
return b.spreadCollectedUSD
}
// CandleIndex returns the index of the current candle. // CandleIndex returns the index of the current candle.
func (b *TestBroker) CandleIndex() int { func (b *TestBroker) CandleIndex() int {
return Max(b.candleCount-1, 0) return Max(b.candleCount-1, 0)
} }
// Advance advances the test broker to the next candle in the input data. This should be done at the end of the // Advance advances the test broker to the next candle in the input data. This should be done at the end of the
// strategy loop. // strategy loop. This will also call Tick() to update orders and positions.
func (b *TestBroker) Advance() { func (b *TestBroker) Advance() {
if b.candleCount < b.Data.Len() { if b.candleCount < b.Data.Len() {
b.candleCount++ b.candleCount++
} }
b.Tick()
} }
// Bid returns the price a seller pays for the current candle. func (b *TestBroker) Tick() {
// Check if the current candle's high and lows contain any take profits or stop losses.
high, low := b.Data.High(b.CandleIndex()), b.Data.Low(b.CandleIndex())
for _, any_p := range b.positions {
if any_p.Closed() {
continue
}
p := any_p.(*TestPosition)
price := b.Price("", p.units < 0) // We want to buy if we are short, and vice versa.
if p.trailingSLDist > 0 {
p.trailingSL = Max(p.trailingSL, price-p.trailingSLDist)
}
// Check if the position should be closed.
if p.takeProfit > 0 {
if (p.units > 0 && p.takeProfit <= high) || (p.units < 0 && p.takeProfit >= low) {
p.close(p.takeProfit, closeTypeTakeProfit)
}
} else if p.stopLoss > 0 {
if (p.units > 0 && p.stopLoss >= low) || (p.units < 0 && p.stopLoss <= high) {
p.close(p.stopLoss, closeTypeStopLoss)
}
} else if p.trailingSL > 0 {
if (p.units > 0 && p.trailingSL >= low) || (p.units < 0 && p.trailingSL <= high) {
p.close(p.trailingSL, closeTypeTrailingStop)
}
}
}
}
// Price returns the ask price if wantToBuy is true and the bid price if wantToBuy is false.
func (b *TestBroker) Price(symbol string, wantToBuy bool) float64 {
if wantToBuy {
return b.Ask(symbol)
}
return b.Bid(symbol)
}
// 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.Data.Close(b.CandleIndex())
} }
@ -390,6 +439,9 @@ func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataF
} }
func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) { func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) {
if units == 0 {
return nil, ErrZeroUnits
}
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 {
return nil, ErrNoData return nil, ErrNoData
@ -400,28 +452,32 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
} }
} }
var price float64 price := b.Price("", units > 0)
if units < 0 {
price = b.Bid("")
} else {
price = b.Ask("")
}
slippage := rand.Float64() * b.Slippage * price slippage := rand.Float64() * b.Slippage * price
price += slippage - slippage/2 // Get a slippage as +/- 50% of the slippage. price += slippage - slippage/2 // Get a slippage as +/- 50% of the slippage.
var trailingSL float64
if stopLoss < 0 {
trailingSL = -stopLoss
}
order := &TestOrder{ order := &TestOrder{
id: strconv.Itoa(rand.Int()), id: strconv.Itoa(rand.Int()),
leverage: b.Leverage, leverage: b.Leverage,
position: nil, position: nil,
price: price, price: price,
symbol: symbol, symbol: symbol,
stopLoss: stopLoss,
takeProfit: takeProfit, takeProfit: takeProfit,
time: time.Now(), time: time.Now(),
orderType: MarketOrder, orderType: MarketOrder,
units: units, units: units,
} }
if trailingSL > 0 {
order.trailingSL = trailingSL
} else {
order.stopLoss = stopLoss
}
// Instantly fulfill the order. // Instantly fulfill the order.
order.position = &TestPosition{ order.position = &TestPosition{
@ -431,11 +487,15 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
id: strconv.Itoa(rand.Int()), id: strconv.Itoa(rand.Int()),
leverage: b.Leverage, leverage: b.Leverage,
symbol: symbol, symbol: symbol,
stopLoss: stopLoss,
takeProfit: takeProfit, takeProfit: takeProfit,
time: time.Now(), time: time.Now(),
units: units, units: units,
} }
if trailingSL > 0 {
order.position.trailingSLDist = trailingSL
} else {
order.position.stopLoss = stopLoss
}
b.Cash -= order.position.EntryValue() b.Cash -= order.position.EntryValue()
b.orders = append(b.orders, order) b.orders = append(b.orders, order)
@ -493,38 +553,54 @@ func (b *TestBroker) Positions() []Position {
} }
type TestPosition struct { type TestPosition struct {
broker *TestBroker broker *TestBroker
closed bool closed bool
entryPrice float64 entryPrice float64
closePrice float64 // If zero, then position has not been closed. closePrice float64 // If zero, then position has not been closed.
id string closeType string // SL, TS, TP
leverage float64 id string
symbol string leverage float64
stopLoss float64 symbol string
takeProfit float64 trailingSL float64 // the price of the trailing stop loss as assigned by broker Tick().
time time.Time trailingSLDist float64 // serves to calculate the trailing stop loss at the broker.
units float64 stopLoss float64
takeProfit float64
time time.Time
units float64
} }
func (p *TestPosition) Close() error { func (p *TestPosition) Close() error {
p.close(p.broker.Price("", p.units < 0), closeTypeMarket)
return nil
}
const (
closeTypeMarket = "M"
closeTypeStopLoss = "SL"
closeTypeTrailingStop = "TS"
closeTypeTakeProfit = "TP"
)
func (p *TestPosition) close(atPrice float64, closeType string) {
if p.closed { if p.closed {
return ErrPositionClosed return
} }
p.closed = true p.closed = true
if p.units < 0 { p.closePrice = atPrice
p.closePrice = p.broker.Ask("") // Ask because we are short so we have to buy. p.closeType = closeType
} else {
p.closePrice = p.broker.Bid("") // Ask because we are long so we have to sell.
}
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 * p.units
p.broker.SignalEmit("PositionClosed", p) p.broker.SignalEmit("PositionClosed", p)
return nil
} }
func (p *TestPosition) Closed() bool { func (p *TestPosition) Closed() bool {
return p.closed return p.closed
} }
func (p *TestPosition) CloseType() string {
return p.closeType
}
func (p *TestPosition) EntryPrice() float64 { func (p *TestPosition) EntryPrice() float64 {
return p.entryPrice return p.entryPrice
} }
@ -553,6 +629,10 @@ func (p *TestPosition) Symbol() string {
return p.symbol return p.symbol
} }
func (p *TestPosition) TrailingStop() float64 {
return p.trailingSL
}
func (p *TestPosition) StopLoss() float64 { func (p *TestPosition) StopLoss() float64 {
return p.stopLoss return p.stopLoss
} }
@ -573,13 +653,7 @@ func (p *TestPosition) Value() float64 {
if p.closed { if p.closed {
return p.closePrice * p.units return p.closePrice * p.units
} }
var price float64 return p.broker.Price("", p.units > 0) * p.units
if p.units < 0 {
price = p.broker.Ask("")
} else {
price = p.broker.Bid("")
}
return price * p.units
} }
type TestOrder struct { type TestOrder struct {
@ -588,6 +662,7 @@ type TestOrder struct {
position *TestPosition position *TestPosition
price float64 price float64
symbol string symbol string
trailingSL float64
stopLoss float64 stopLoss float64
takeProfit float64 takeProfit float64
time time.Time time time.Time
@ -623,6 +698,10 @@ func (o *TestOrder) Symbol() string {
return o.symbol return o.symbol
} }
func (o *TestOrder) TrailingStop() float64 {
return o.trailingSL
}
func (o *TestOrder) StopLoss() float64 { func (o *TestOrder) StopLoss() float64 {
return o.stopLoss return o.stopLoss
} }

View File

@ -14,7 +14,7 @@ const testDataCSV = `date,open,high,low,close,volume
2022-01-05,1.1,1.2,1.0,1.15,110 2022-01-05,1.1,1.2,1.0,1.15,110
2022-01-06,1.15,1.2,1.1,1.2,120 2022-01-06,1.15,1.2,1.1,1.2,120
2022-01-07,1.2,1.3,1.15,1.25,140 2022-01-07,1.2,1.3,1.15,1.25,140
2022-01-08,1.25,1.3,1.2,1.1,150 2022-01-08,1.25,1.3,1.0,1.1,150
2022-01-09,1.1,1.4,1.0,1.3,220` 2022-01-09,1.1,1.4,1.0,1.3,220`
func newTestingDataframe() *DataFrame { func newTestingDataframe() *DataFrame {
@ -180,3 +180,98 @@ func TestBacktestingBrokerOrders(t *testing.T) {
t.Errorf("Expected broker PL to be 2500, got %f", broker.PL()) t.Errorf("Expected broker PL to be 2500, got %f", broker.PL())
} }
} }
func TestBacktestingBrokerStopLimitOrders(t *testing.T) {
data := newTestingDataframe()
broker := NewTestBroker(nil, data, 100_000, 50, 0, 0)
broker.Slippage = 0
order, err := broker.MarketOrder("", 10_000, 1.05, 1.25)
if err != nil {
t.Fatal(err)
}
if order == nil {
t.Fatal("Order is nil")
}
if order.StopLoss() != 1.05 {
t.Errorf("Expected stop loss to be 1.1, got %f", order.StopLoss())
}
if order.TakeProfit() != 1.25 {
t.Errorf("Expected take profit to be 1.25, got %f", order.TakeProfit())
}
position := order.Position()
if position == nil {
t.Fatal("Position is nil")
}
if position.StopLoss() != 1.05 {
t.Errorf("Expected stop loss to be 1.1, got %f", position.StopLoss())
}
if position.TakeProfit() != 1.25 {
t.Errorf("Expected take profit to be 1.25, got %f", position.TakeProfit())
}
broker.Advance()
broker.Advance() // Now we're at the third candle which hits our take profit
if position.Closed() != true {
t.Error("Expected position to be closed")
}
if position.ClosePrice() != 1.25 {
t.Errorf("Expected close price to be 1.25, got %f", position.ClosePrice())
}
if !EqualApprox(position.PL(), 1000) { // (1.25-1.15) * 10_000 = 1000
t.Errorf("Expected position PL to be 1000, got %f", position.PL())
}
broker.Advance() // 4th candle
order, err = broker.MarketOrder("", 10_000, -0.2, 1.4) // Long position with trailing stop loss of 0.2.
if err != nil {
t.Fatal(err)
}
if order == nil {
t.Fatal("Order is nil")
}
if order.StopLoss() != 0 {
t.Errorf("Expected stop loss to be 0, got %f", order.StopLoss())
}
if order.TakeProfit() != 1.4 {
t.Errorf("Expected take profit to be 1.4, got %f", order.TakeProfit())
}
if !EqualApprox(order.TrailingStop(), 0.2) { // Orders return the distance to the trailing stop loss.
t.Errorf("Expected trailing stop to be 0.2, got %f", order.TrailingStop())
}
broker.Advance() // Cause the position to get updated.
position = order.Position()
if position == nil {
t.Fatal("Position is nil")
}
if position.Closed() {
t.Error("Expected position to be open")
}
if position.StopLoss() != 0 {
t.Errorf("Expected stop loss to be 0, got %f", position.StopLoss())
}
if position.TakeProfit() != 1.4 {
t.Errorf("Expected take profit to be 1.4, got %f", position.TakeProfit())
}
if !EqualApprox(position.TrailingStop(), 0.95) { // Positions return the actual trailing stop loss price.
t.Errorf("Expected trailing stop to be 0.95, got %f", position.TrailingStop())
}
for !position.Closed() {
broker.Advance() // Advance until position is closed.
}
if !EqualApprox(position.ClosePrice(), 1.05) {
t.Errorf("Expected close price to be 1.05, got %f", position.ClosePrice())
}
if !EqualApprox(position.PL(), -500) { // (1.05-1.1) * 10_000 = -500
t.Errorf("Expected position PL to be 1000, got %f", position.PL())
}
if position.CloseType() != closeTypeTrailingStop {
t.Errorf("Expected close type to be %q, got %q", closeTypeTrailingStop, position.CloseType())
}
}

View File

@ -21,35 +21,38 @@ var (
) )
type Order interface { type Order interface {
Cancel() error // Cancel attempts to cancel the order and returns an error if it fails. If the error is nil, the order was canceled. Cancel() error // Cancel attempts to cancel the order and returns an error if it fails. If the error is nil, the order was canceled.
Fulfilled() bool // Fulfilled returns true if the order has been filled with the broker and a position is active. Fulfilled() bool // Fulfilled returns true if the order has been filled with the broker and a position is active.
Id() string // Id returns the unique identifier of the order by the broker. Id() string // Id returns the unique identifier of the order by the broker.
Leverage() float64 // Leverage returns the leverage of the order. Leverage() float64 // Leverage returns the leverage of the order.
Position() Position // Position returns the position of the order. If the order has not been filled, nil is returned. Position() Position // Position returns the position of the order. If the order has not been filled, nil is returned.
Price() float64 // Price returns the price of the symbol at the time the order was placed. Price() float64 // Price returns the price of the symbol at the time the order was placed.
Symbol() string // Symbol returns the symbol name of the order. Symbol() string // Symbol returns the symbol name of the order.
StopLoss() float64 // StopLoss returns the stop loss price of the order. TrailingStop() float64 // TrailingStop returns the trailing stop loss distance of the order.
TakeProfit() float64 // TakeProfit returns the take profit price of the order. StopLoss() float64 // StopLoss returns the stop loss price of the order.
Time() time.Time // Time returns the time the order was placed. TakeProfit() float64 // TakeProfit returns the take profit price of the order.
Type() OrderType // Type returns the type of order. Time() time.Time // Time returns the time the order was placed.
Units() float64 // Units returns the number of units purchased or sold by the order. Type() OrderType // Type returns the type of order.
Units() float64 // Units returns the number of units purchased or sold by the order.
} }
type Position interface { type Position interface {
Close() error // Close attempts to close the position and returns an error if it fails. If the error is nil, the position was closed. Close() error // Close attempts to close the position and returns an error if it fails. If the error is nil, the position was closed.
Closed() bool // Closed returns true if the position has been closed with the broker. Closed() bool // Closed returns true if the position has been closed with the broker.
ClosePrice() float64 // ClosePrice returns the price of the symbol at the time the position was closed. May be zero if the position is still open. CloseType() string // CloseType returns the type of order used to close the position.
EntryPrice() float64 // EntryPrice returns the price of the symbol at the time the position was opened. ClosePrice() float64 // ClosePrice returns the price of the symbol at the time the position was closed. May be zero if the position is still open.
EntryValue() float64 // EntryValue returns the value of the position at the time it was opened. EntryPrice() float64 // EntryPrice returns the price of the symbol at the time the position was opened.
Id() string // Id returns the unique identifier of the position by the broker. EntryValue() float64 // EntryValue returns the value of the position at the time it was opened.
Leverage() float64 // Leverage returns the leverage of the position. Id() string // Id returns the unique identifier of the position by the broker.
PL() float64 // PL returns the profit or loss of the position. Leverage() float64 // Leverage returns the leverage of the position.
Symbol() string // Symbol returns the symbol name of the position. PL() float64 // PL returns the profit or loss of the position.
StopLoss() float64 // StopLoss returns the stop loss price of the position. Symbol() string // Symbol returns the symbol name of the position.
TakeProfit() float64 // TakeProfit returns the take profit price of the position. TrailingStop() float64 // TrailingStop returns the trailing stop loss price of the position.
Time() time.Time // Time returns the time the position was opened. StopLoss() float64 // StopLoss returns the stop loss price of the position.
Units() float64 // Units returns the number of units purchased or sold by the position. TakeProfit() float64 // TakeProfit returns the take profit price of the position.
Value() float64 // Value returns the value of the position at the current price. Time() time.Time // Time returns the time the position was opened.
Units() float64 // Units returns the number of units purchased or sold by the position.
Value() float64 // Value returns the value of the position at the current price.
} }
// Broker is an interface that defines the methods that a broker must implement to report symbol data and place orders, etc. All Broker implementations must also implement the Signaler interface and emit the following functions when necessary: // Broker is an interface that defines the methods that a broker must implement to report symbol data and place orders, etc. All Broker implementations must also implement the Signaler interface and emit the following functions when necessary:
@ -57,10 +60,12 @@ type Position interface {
// - PositionClosed(Position) - Emitted after a position is closed either manually or automatically. // - PositionClosed(Position) - Emitted after a position is closed either manually or automatically.
type Broker interface { type Broker interface {
Signaler Signaler
Bid(symbol string) float64 // Bid returns the sell price of the symbol. Price(symbol string, wantToBuy bool) float64 // Price returns the ask price if wantToBuy is true and the bid price if wantToBuy is false.
Ask(symbol string) float64 // Ask returns the buy price of the symbol, which is typically higher than the sell price. Bid(symbol string) float64 // Bid returns the sell price of the symbol.
Ask(symbol string) float64 // Ask returns the buy price of the symbol, which is typically higher than the sell price.
// Candles returns a dataframe of candles for the given symbol, frequency, and count by querying the broker. // Candles returns a dataframe of candles for the given symbol, frequency, and count by querying the broker.
Candles(symbol, frequency string, count int) (*DataFrame, error) Candles(symbol, frequency string, count int) (*DataFrame, error)
// MarketOrder places a market order for the given symbol and returns an error if it fails. A short position has negative units. If stopLoss or takeProfit are zero, they will not be set. If stopLoss is greater than the current price for a long position or less than the current price for a short position, the order will fail. Likewise for takeProfit. If the stopLoss is a negative number, it is used as a trailing stop loss to represent how many price points away the stop loss should be from the current price.
MarketOrder(symbol string, units, stopLoss, takeProfit float64) (Order, error) MarketOrder(symbol string, units, stopLoss, takeProfit float64) (Order, error)
NAV() float64 // NAV returns the net asset value of the account. NAV() float64 // NAV returns the net asset value of the account.
PL() float64 // PL returns the profit or loss of the account. PL() float64 // PL returns the profit or loss of the account.

View File

@ -42,6 +42,14 @@ func NewOandaBroker(token, accountID string, practice bool) *OandaBroker {
} }
} }
// Price returns the ask price if wantToBuy is true and the bid price if wantToBuy is false.
func (b *OandaBroker) Price(symbol string, wantToBuy bool) float64 {
if wantToBuy {
return b.Ask(symbol)
}
return b.Bid(symbol)
}
func (b *OandaBroker) Bid(symbol string) float64 { func (b *OandaBroker) Bid(symbol string) float64 {
return 0 return 0
} }