Implemented stop and limit orders

This commit is contained in:
Luke I. Wilson 2023-05-19 13:17:08 -05:00
parent 56740b5a00
commit d851061d1f
5 changed files with 218 additions and 82 deletions

View File

@ -371,6 +371,28 @@ func (b *TestBroker) Advance() {
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())
// Update orders.
for _, any_o := range b.orders {
if any_o.Fulfilled() {
continue
}
o := any_o.(*TestOrder)
if o.orderType == Limit {
if o.price >= low && o.price <= high {
o.fulfill(o.price)
}
} else if o.orderType == Stop {
if o.price <= high && o.price >= low {
o.fulfill(o.price)
}
} else {
panic("the order type is either unknown or otherwise should not be market because those are fulfilled immediately")
}
}
// Update positions.
for _, any_p := range b.positions {
if any_p.Closed() {
continue
@ -385,15 +407,18 @@ func (b *TestBroker) Tick() {
// 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)
p.close(p.takeProfit, CloseTakeProfit)
continue
}
} else if p.stopLoss > 0 {
}
// stopLoss won't be set if trailingSL is set, and vice versa.
if p.stopLoss > 0 {
if (p.units > 0 && p.stopLoss >= low) || (p.units < 0 && p.stopLoss <= high) {
p.close(p.stopLoss, closeTypeStopLoss)
p.close(p.stopLoss, CloseStopLoss)
}
} else if p.trailingSL > 0 {
if (p.units > 0 && p.trailingSL >= low) || (p.units < 0 && p.trailingSL <= high) {
p.close(p.trailingSL, closeTypeTrailingStop)
p.close(p.trailingSL, CloseTrailingStop)
}
}
}
@ -438,7 +463,7 @@ func (b *TestBroker) Candles(symbol string, frequency string, count int) (*DataF
return b.Data.Copy(start, adjCount).(*DataFrame), nil
}
func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takeProfit float64) (Order, error) {
func (b *TestBroker) Order(orderType OrderType, symbol string, units, price, stopLoss, takeProfit float64) (Order, error) {
if units == 0 {
return nil, ErrZeroUnits
}
@ -452,17 +477,18 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
}
}
price := b.Price("", units > 0)
slippage := rand.Float64() * b.Slippage * price
price += slippage - slippage/2 // Get a slippage as +/- 50% of the slippage.
var trailingSL float64
if stopLoss < 0 {
trailingSL = -stopLoss
}
marketPrice := b.Price("", units > 0)
if orderType == Market {
price = marketPrice
}
order := &TestOrder{
broker: b,
id: strconv.Itoa(rand.Int()),
leverage: b.Leverage,
position: nil,
@ -470,7 +496,7 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
symbol: symbol,
takeProfit: takeProfit,
time: time.Now(),
orderType: MarketOrder,
orderType: orderType,
units: units,
}
if trailingSL > 0 {
@ -479,27 +505,18 @@ func (b *TestBroker) MarketOrder(symbol string, units float64, stopLoss, takePro
order.stopLoss = stopLoss
}
// Instantly fulfill the order.
order.position = &TestPosition{
broker: b,
closed: false,
entryPrice: price,
id: strconv.Itoa(rand.Int()),
leverage: b.Leverage,
symbol: symbol,
takeProfit: takeProfit,
time: time.Now(),
units: units,
// TODO: only instantly fulfill market orders or sometimes limit orders when requirements are met.
if orderType == Market {
order.fulfill(price)
} else if orderType == Limit {
if units > 0 && marketPrice <= order.price {
order.fulfill(price)
} else if units < 0 && marketPrice >= order.price {
order.fulfill(price)
}
if trailingSL > 0 {
order.position.trailingSLDist = trailingSL
} else {
order.position.stopLoss = stopLoss
}
b.Cash -= order.position.EntryValue()
b.orders = append(b.orders, order)
b.positions = append(b.positions, order.position)
b.SignalEmit("OrderPlaced", order)
return order, nil
@ -557,7 +574,7 @@ type TestPosition struct {
closed bool
entryPrice float64
closePrice float64 // If zero, then position has not been closed.
closeType string // SL, TS, TP
closeType OrderCloseType // SL, TS, TP
id string
leverage float64
symbol string
@ -570,18 +587,11 @@ type TestPosition struct {
}
func (p *TestPosition) Close() error {
p.close(p.broker.Price("", p.units < 0), closeTypeMarket)
p.close(p.broker.Price("", p.units < 0), CloseMarket)
return nil
}
const (
closeTypeMarket = "M"
closeTypeStopLoss = "SL"
closeTypeTrailingStop = "TS"
closeTypeTakeProfit = "TP"
)
func (p *TestPosition) close(atPrice float64, closeType string) {
func (p *TestPosition) close(atPrice float64, closeType OrderCloseType) {
if p.closed {
return
}
@ -597,7 +607,7 @@ func (p *TestPosition) Closed() bool {
return p.closed
}
func (p *TestPosition) CloseType() string {
func (p *TestPosition) CloseType() OrderCloseType {
return p.closeType
}
@ -657,6 +667,7 @@ func (p *TestPosition) Value() float64 {
}
type TestOrder struct {
broker *TestBroker
id string
leverage float64
position *TestPosition
@ -674,6 +685,31 @@ func (o *TestOrder) Cancel() error {
return ErrCancelFailed
}
func (o *TestOrder) fulfill(atPrice float64) {
slippage := rand.Float64() * o.broker.Slippage * atPrice
atPrice += slippage - slippage/2 // Adjust price as +/- 50% of the slippage.
o.position = &TestPosition{
broker: o.broker,
closed: false,
entryPrice: atPrice,
id: strconv.Itoa(rand.Int()),
leverage: o.leverage,
symbol: o.symbol,
takeProfit: o.takeProfit,
time: time.Now(),
units: o.units,
}
if o.trailingSL > 0 {
o.position.trailingSLDist = o.trailingSL
} else {
o.position.stopLoss = o.stopLoss
}
o.broker.Cash -= o.position.EntryValue()
o.broker.positions = append(o.broker.positions, o.position)
}
func (o *TestOrder) Fulfilled() bool {
return o.position != nil
}

View File

@ -10,7 +10,7 @@ const testDataCSV = `date,open,high,low,close,volume
2022-01-01,1.1,1.2,1.0,1.15,100
2022-01-02,1.15,1.2,1.1,1.2,110
2022-01-03,1.2,1.3,1.15,1.25,120
2022-01-04,1.25,1.3,1.2,1.1,130
2022-01-04,1.25,1.3,1.0,1.1,130
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-07,1.2,1.3,1.15,1.25,140
@ -79,21 +79,13 @@ func TestBacktestingBrokerCandles(t *testing.T) {
}
}
func TestBacktestingBrokerFunctions(t *testing.T) {
broker := NewTestBroker(nil, nil, 100_000, 20, 0, 0)
if !EqualApprox(broker.NAV(), 100_000) {
t.Errorf("Expected NAV to be 100_000, got %f", broker.NAV())
}
}
func TestBacktestingBrokerOrders(t *testing.T) {
func TestBacktestingBrokerMarketOrders(t *testing.T) {
data := newTestingDataframe()
broker := NewTestBroker(nil, data, 100_000, 50, 0, 0)
broker.Slippage = 0
timeBeforeOrder := time.Now()
order, err := broker.MarketOrder("EUR_USD", 50_000, 0, 0) // Buy 50,000 USD for 1000 EUR with no stop loss or take profit
order, err := broker.Order(Market, "EUR_USD", 50_000, 0, 0, 0) // Buy 50,000 USD for 1000 EUR with no stop loss or take profit
if err != nil {
t.Fatal(err)
}
@ -125,7 +117,7 @@ func TestBacktestingBrokerOrders(t *testing.T) {
if order.TakeProfit() != 0 {
t.Errorf("Expected take profit to be 0, got %f", order.TakeProfit())
}
if order.Type() != MarketOrder {
if order.Type() != Market {
t.Errorf("Expected order type to be MarketOrder, got %s", order.Type())
}
@ -181,12 +173,111 @@ func TestBacktestingBrokerOrders(t *testing.T) {
}
}
func TestBacktestingBrokerStopLimitOrders(t *testing.T) {
func TestBacktestingBrokerLimitOrders(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)
order, err := broker.Order(Limit, "EUR_USD", -50_000, 1.3, 1.35, 1.1) // Sell limit 50,000 USD for 1000 EUR
if err != nil {
t.Fatal(err)
}
if order == nil {
t.Fatal("Order is nil")
}
if order.Price() != 1.3 {
t.Errorf("Expected order price to be 1.3, got %f", order.Price())
}
if order.Fulfilled() != false {
t.Error("Expected order to not be fulfilled")
}
broker.Advance()
broker.Advance() // Advance to the third candle where the order should be fulfilled
if order.Fulfilled() != true {
t.Error("Expected order to be fulfilled")
}
position := order.Position()
if position == nil {
t.Fatal("Position is nil")
}
if position.Closed() != false {
t.Fatal("Expected position to not be closed")
}
broker.Advance() // Advance to the fourth candle which should hit our take profit
if position.Closed() != true {
t.Fatal("Expected position to be closed")
}
if position.ClosePrice() != 1.1 {
t.Errorf("Expected position close price to be 1.1, got %f", position.ClosePrice())
}
if position.CloseType() != CloseTakeProfit {
t.Errorf("Expected position close type to be TP, got %s", position.CloseType())
}
if !EqualApprox(position.PL(), 10_000) { // abs(1.1-1.3) * 50_000 = 10,000
t.Errorf("Expected position PL to be 10000, got %f", position.PL())
}
}
func TestBacktestingBrokerStopOrders(t *testing.T) {
data := newTestingDataframe()
broker := NewTestBroker(nil, data, 100_000, 50, 0, 0)
broker.Slippage = 0
order, err := broker.Order(Stop, "EUR_USD", 50_000, 1.2, 1, 1.3) // Buy stop 50,000 EUR for 1000 USD
if err != nil {
t.Fatal(err)
}
if order == nil {
t.Fatal("Order is nil")
}
if order.Price() != 1.2 {
t.Errorf("Expected order price to be 1.2, got %f", order.Price())
}
if order.Fulfilled() != false {
t.Error("Expected order to not be fulfilled")
}
broker.Advance() // Advance to the second candle where the order should be fulfilled
if order.Fulfilled() != true {
t.Error("Expected order to be fulfilled")
}
position := order.Position()
if position == nil {
t.Fatal("Position is nil")
}
if position.Closed() != false {
t.Fatal("Expected position to not be closed")
}
broker.Advance() // Advance to the third candle which should hit our take profit
if position.Closed() != true {
t.Fatal("Expected position to be closed")
}
if position.ClosePrice() != 1.3 {
t.Errorf("Expected position close price to be 1.3, got %f", position.ClosePrice())
}
if position.CloseType() != CloseTakeProfit {
t.Errorf("Expected position close type to be TP, got %s", position.CloseType())
}
if !EqualApprox(position.PL(), 5000) { // (1.3-1.2) * 50_000 = 5000
t.Errorf("Expected position PL to be 5000, got %f", position.PL())
}
}
func TestBacktestingBrokerStopLossTakeProfit(t *testing.T) {
data := newTestingDataframe()
broker := NewTestBroker(nil, data, 100_000, 50, 0, 0)
broker.Slippage = 0
order, err := broker.Order(Market, "", 10_000, 0, 1.05, 1.25)
if err != nil {
t.Fatal(err)
}
@ -226,7 +317,7 @@ func TestBacktestingBrokerStopLimitOrders(t *testing.T) {
broker.Advance() // 4th candle
order, err = broker.MarketOrder("", 10_000, -0.2, 1.4) // Long position with trailing stop loss of 0.2.
order, err = broker.Order(Market, "", 10_000, 0, -0.2, 1.4) // Long position with trailing stop loss of 0.2.
if err != nil {
t.Fatal(err)
}
@ -271,7 +362,7 @@ func TestBacktestingBrokerStopLimitOrders(t *testing.T) {
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())
if position.CloseType() != CloseTrailingStop {
t.Errorf("Expected close type to be %q, got %q", CloseTrailingStop, position.CloseType())
}
}

View File

@ -5,12 +5,21 @@ import (
"time"
)
type OrderCloseType string
const (
CloseMarket OrderCloseType = "M"
CloseStopLoss OrderCloseType = "SL"
CloseTrailingStop OrderCloseType = "TS"
CloseTakeProfit OrderCloseType = "TP"
)
type OrderType string
const (
MarketOrder OrderType = "MARKET"
LimitOrder OrderType = "LIMIT"
StopOrder OrderType = "STOP"
Market OrderType = "MARKET" // Market means to buy or sell at the current market price, which may not be what you ask for.
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.
)
var (
@ -39,7 +48,7 @@ type Order 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.
Closed() bool // Closed returns true if the position has been closed with the broker.
CloseType() string // CloseType returns the type of order used to close the position.
CloseType() OrderCloseType // CloseType returns the type of order used to close the position.
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.
EntryPrice() float64 // EntryPrice returns the price of the symbol at the time the position was opened.
EntryValue() float64 // EntryValue returns the value of the position at the time it was opened.
@ -65,8 +74,8 @@ type Broker interface {
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(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)
// Order places an order with orderType for the given symbol and returns an error if it fails. A short position has negative units. If the orderType is Market, the price argument will be ignored and the order will be fulfilled at current price. Otherwise, price is used to set the target price for Stop and Limit orders. If stopLoss or takeProfit are zero, they will not be set. If the 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.
Order(orderType OrderType, symbol string, units, price, stopLoss, takeProfit float64) (Order, error)
NAV() float64 // NAV returns the net asset value of the account.
PL() float64 // PL returns the profit or loss of the account.
OpenOrders() []Order

View File

@ -82,7 +82,7 @@ func (b *OandaBroker) Candles(symbol, frequency string, count int) (*auto.DataFr
return newDataframe(candlestickResponse)
}
func (b *OandaBroker) MarketOrder(symbol string, units, stopLoss, takeProfit float64) (auto.Order, error) {
func (b *OandaBroker) Order(orderType auto.OrderType, symbol string, units, price, stopLoss, takeProfit float64) (auto.Order, error) {
return nil, nil
}

View File

@ -165,14 +165,14 @@ func (t *Trader) fetchData() {
func (t *Trader) Buy(units float64) {
t.closeOrdersAndPositions()
t.Log.Printf("Buy %f units", units)
t.Broker.MarketOrder(t.Symbol, units, 0.0, 0.0)
t.Broker.Order(Market, t.Symbol, units, 0, 0, 0)
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{units, false})
}
func (t *Trader) Sell(units float64) {
t.closeOrdersAndPositions()
t.Log.Printf("Sell %f units", units)
t.Broker.MarketOrder(t.Symbol, -units, 0.0, 0.0)
t.Broker.Order(Market, t.Symbol, -units, 0, 0, 0)
t.stats.tradesThisCandle = append(t.stats.tradesThisCandle, TradeStat{-units, false})
}