Skip to content

Commit

Permalink
Merge branch '57-add-trigger-stop-loss-feature' into 'develop'
Browse files Browse the repository at this point in the history
Resolve "Add Trigger Stop Loss feature"

Closes #57

See merge request aoterocom/AOCryptobot!44
  • Loading branch information
aoterolorenzo committed Nov 18, 2021
2 parents 318ddad + 4b2fd9e commit 5b0f335
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 89 deletions.
9 changes: 7 additions & 2 deletions bot_signal-trader/conf.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ maxOpenPositions : 3

interval: 1h

# Stop Loss
stopLoss : true
stopLossPct : 0.018
stopLossPct : 1.8

# Database Settings
# Trailing Stop Loss
trailingStopLoss: true
trailingStopLossTriggerPct: 0.75
trailingStopLossPct: 0.5

# Database Settings
enableDatabaseRecording: true

databaseName: AOCryptoBot
Expand Down
143 changes: 110 additions & 33 deletions bot_signal-trader/services/signal-trader_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"gitlab.com/aoterocom/AOCryptobot/interfaces"
"gitlab.com/aoterocom/AOCryptobot/models"
"gitlab.com/aoterocom/AOCryptobot/services"
"log"
"os"
"reflect"
"strconv"
Expand All @@ -22,11 +21,19 @@ type SignalTraderService struct {
multiMarketService *services.MultiMarketService
tradingRecordService *services.TradingRecordService
databaseService *database.DBService
maxOpenPositions int
targetCoin string

// Stop Loss
stopLoss bool
stopLossPct float64

// Trailing Stop Loss
trailingStopLoss bool
trailingStopLossTriggerPct float64
trailingStopLossPct float64
trailingStopLossArmedAt map[string]float64

maxOpenPositions int
targetCoin string
stopLoss bool
stopLossPct float64
tradePctPerPosition float64
balancePctToTrade float64
databaseIsEnabled bool
Expand All @@ -35,7 +42,6 @@ type SignalTraderService struct {
initialBalance float64
tradeQuantityPerPosition float64
firstExitTriggered map[string]bool
//pairDirection map[string]models.MarketDirection
}

func NewSignalTrader(databaseService *database.DBService, marketAnalysisService *services.MarketAnalysisService, multiMarketService *services.MultiMarketService) SignalTraderService {
Expand All @@ -46,12 +52,35 @@ func NewSignalTrader(databaseService *database.DBService, marketAnalysisService
}
}

func NewSignalTraderFullFilled(marketAnalysisService *services.MarketAnalysisService, multiMarketService *services.MultiMarketService, tradingRecordService *services.TradingRecordService, databaseService *database.DBService,
maxOpenPositions int, targetCoin string, stopLoss bool, stopLossPct float64, trailingStopLoss bool, trailingStopLossTriggerPct float64, trailingStopLossPct float64, trailingStopLossArmedPct map[string]float64,
tradePctPerPosition float64, balancePctToTrade float64, databaseIsEnabled bool, currentBalance float64, initialBalance float64, tradeQuantityPerPosition float64, firstExitTriggered map[string]bool) SignalTraderService {
return SignalTraderService{
marketAnalysisService: marketAnalysisService,
multiMarketService: multiMarketService,
tradingRecordService: tradingRecordService,
databaseService: databaseService,
maxOpenPositions: maxOpenPositions,
targetCoin: targetCoin,
stopLoss: stopLoss,
stopLossPct: stopLossPct,
trailingStopLoss: trailingStopLoss,
trailingStopLossTriggerPct: trailingStopLossTriggerPct,
trailingStopLossPct: trailingStopLossPct,
trailingStopLossArmedAt: trailingStopLossArmedPct,
tradePctPerPosition: tradePctPerPosition,
balancePctToTrade: balancePctToTrade,
databaseIsEnabled: databaseIsEnabled,
currentBalance: currentBalance,
initialBalance: initialBalance,
tradeQuantityPerPosition: tradeQuantityPerPosition,
firstExitTriggered: firstExitTriggered,
}
}

func init() {
cwd, _ := os.Getwd()
err := godotenv.Load(cwd + "/bot_signal-trader/conf.env")
if err != nil {
log.Fatalln("Error loading go.env file", err)
}
_ = godotenv.Load(cwd + "/bot_signal-trader/conf.env")
}

func (t *SignalTraderService) Start() {
Expand All @@ -66,6 +95,10 @@ func (t *SignalTraderService) Start() {
t.balancePctToTrade, _ = strconv.ParseFloat(os.Getenv("balancePctToTrade"), 64)
t.databaseIsEnabled, _ = strconv.ParseBool(os.Getenv("enableDatabaseRecording"))
t.firstExitTriggered = make(map[string]bool)
t.trailingStopLoss, _ = strconv.ParseBool(os.Getenv("trailingStopLoss"))
t.trailingStopLossTriggerPct, _ = strconv.ParseFloat(os.Getenv("trailingStopLossTriggerPct"), 64)
t.trailingStopLossPct, _ = strconv.ParseFloat(os.Getenv("trailingStopLossPct"), 64)
t.trailingStopLossArmedAt = make(map[string]float64)
initialBalance, err := t.marketAnalysisService.ExchangeService.GetAvailableBalance(t.targetCoin)
if err != nil {
helpers.Logger.Fatalln(fmt.Sprintf("Couldn't get the initial currentBalance: %s", err.Error()))
Expand Down Expand Up @@ -140,28 +173,64 @@ func (t *SignalTraderService) EnterIfDelayedEntryCheck(pair string, strategy int
func (t *SignalTraderService) ExitIfDelayedExitCheck(pair string, strategy interfaces.Strategy,
timeSeries *techan.TimeSeries, constants []float64, delay int) {

if t.StopLossCheck(pair, strategy,
timeSeries, constants, true) {
t.PerformExit(pair, strategy, timeSeries, constants)
t.UnLockPair(pair)
return
}

// If there's no stop-loss signal, wait delay and exit if recheck
// Wait delay and exit if recheck
time.Sleep(time.Duration(delay) * time.Second)
if t.ExitCheck(pair, strategy, timeSeries, constants) {
t.PerformExit(pair, strategy, timeSeries, constants)
t.PerformExit(pair, strategy, timeSeries, constants, models.ExitTriggerStrategy)
t.UnLockPair(pair)
}
}

func (t *SignalTraderService) StopLossCheck(pair string, strategy interfaces.Strategy, timeSeries *techan.TimeSeries, constants []float64, silent bool) bool {
if t.tradingRecordService.HasOpenPositions(pair) && t.stopLoss {
entryPrice, _ := strconv.ParseFloat(t.tradingRecordService.OpenPositions[pair][0].EntranceOrder().Price, 64)
if entryPrice*(1-t.stopLossPct) > timeSeries.LastCandle().ClosePrice.Float() {
if !silent {
helpers.Logger.Debugln(fmt.Sprintf("Stop-Loss signal for %s. Exiting position", pair))
}
func (t *SignalTraderService) MiddleChecks(pair string, timeSeries *techan.TimeSeries) (bool, models.ExitTrigger) {
entryPrice, _ := strconv.ParseFloat(t.tradingRecordService.OpenPositions[pair][0].EntranceOrder().Price, 64)

// STOP - LOSS CHECK
if t.stopLoss {
if t.StopLossCheck(pair, entryPrice, timeSeries) {
return true, models.ExitTriggerStopLoss
}
}

// TRIGGER STOP - LOSS CHECK
if t.trailingStopLoss {
if t.TrailingStopLossCheck(pair, entryPrice, timeSeries) {
return true, models.ExitTriggerTrailingStopLoss
}
}
return false, models.ExitTriggerNone
}

func (t *SignalTraderService) StopLossCheck(pair string, entryPrice float64, timeSeries *techan.TimeSeries) bool {
currentPrice := timeSeries.LastCandle().ClosePrice.Float()

if t.tradingRecordService.HasOpenPositions(pair) {

if entryPrice*(1-(t.stopLossPct/100)) > currentPrice {
helpers.Logger.Debugln(fmt.Sprintf("Stop-Loss signal for %s. Exiting position", pair))
return true
}
}
return false
}

func (t *SignalTraderService) TrailingStopLossCheck(pair string, entryPrice float64, timeSeries *techan.TimeSeries) bool {
currentPrice := timeSeries.LastCandle().ClosePrice.Float()

// Firstly, if price overpass triggerPct, we activate triggerStopLoss
if currentPrice >= entryPrice*(1+(t.trailingStopLossTriggerPct/100)) && currentPrice > t.trailingStopLossArmedAt[pair] {
if t.trailingStopLossArmedAt[pair] == 0.0 {
helpers.Logger.Debugln(fmt.Sprintf("Trailing stop-Loss armed for %s. Current price %f", pair, currentPrice))
}
t.trailingStopLossArmedAt[pair] = currentPrice
}

// If already triggered
if t.trailingStopLossArmedAt[pair] != 0.0 {
targetPrice := t.trailingStopLossArmedAt[pair] * (1 - (t.trailingStopLossPct / 100))
if targetPrice > currentPrice {
helpers.Logger.Debugln(fmt.Sprintf("Trailing stop-Loss signal for %s. Target Price %f. Current price %f. Exiting position", pair, targetPrice, currentPrice))
t.trailingStopLossArmedAt[pair] = 0.0
t.firstExitTriggered[pair] = false
return true
}
}
Expand All @@ -177,12 +246,19 @@ func (t *SignalTraderService) EntryCheck(pair string, strategy interfaces.Strate

func (t *SignalTraderService) ExitCheck(pair string, strategy interfaces.Strategy,
timeSeries *techan.TimeSeries, constants []float64) bool {
if t.StopLossCheck(pair, strategy,
timeSeries, constants, false) {
return true

if t.tradingRecordService.HasOpenPositions(pair) {
shouldExit, exitTrigger := t.MiddleChecks(pair, timeSeries)
if shouldExit {
t.PerformExit(pair, strategy, timeSeries, constants, exitTrigger)
t.UnLockPair(pair)
return false
}

return strategy.ParametrizedShouldExit(timeSeries, constants)
}

return t.tradingRecordService.HasOpenPositions(pair) && strategy.ParametrizedShouldExit(timeSeries, constants)
return false
}

func (t *SignalTraderService) PerformEntry(pair string, strategy interfaces.Strategy,
Expand All @@ -200,12 +276,12 @@ func (t *SignalTraderService) PerformEntry(pair string, strategy interfaces.Stra
lastPosition := t.tradingRecordService.LastOpenPosition(pair)

if t.databaseIsEnabled {
lastPosition.Id = t.databaseService.AddPosition(*lastPosition, strings.Replace(reflect.TypeOf(strategy).String(), "*strategies.", "", 1), constants, -1000, 0.0, 0.0)
lastPosition.Id = t.databaseService.AddPosition(*lastPosition, strings.Replace(reflect.TypeOf(strategy).String(), "*strategies.", "", 1), constants, -1000, 0.0, models.ExitTriggerNone)
}
}

func (t *SignalTraderService) PerformExit(pair string, strategy interfaces.Strategy,
timeSeries *techan.TimeSeries, constants []float64) {
timeSeries *techan.TimeSeries, constants []float64, exitTrigger models.ExitTrigger) {

_ = t.tradingRecordService.ExitPositions(pair, t.marketAnalysisService.GetPairAnalysisResult(pair).MarketDirection)

Expand All @@ -232,12 +308,13 @@ func (t *SignalTraderService) PerformExit(pair string, strategy interfaces.Strat
}

if t.databaseIsEnabled {
t.databaseService.UpdatePosition(lastPosition.Id, *lastPosition, strings.Replace(reflect.TypeOf(strategy).String(), "*strategies.", "", 1), constants, profitPct, transactionBenefit, t.currentBalance-t.initialBalance)
t.databaseService.UpdatePosition(lastPosition.Id, *lastPosition, strings.Replace(reflect.TypeOf(strategy).String(), "*strategies.", "", 1), constants, profitPct, transactionBenefit, exitTrigger)
}

helpers.Logger.Infoln(
fmt.Sprintf("📉 **%s: ❕ Exit signal**\n", pair) +
fmt.Sprintf("Strategy: %s\n", strings.Replace(reflect.TypeOf(strategy).String(), "*strategies.", "", 1)) +
fmt.Sprintf("Trigger: %s\n", exitTrigger) +
fmt.Sprintf("Constants: %v\n", constants) +
fmt.Sprintf("Sell Price: %f\n", timeSeries.Candles[len(timeSeries.Candles)-1].ClosePrice.Float()) +
fmt.Sprintf("Updated Balance: %.2f€\n", t.currentBalance) +
Expand Down
68 changes: 34 additions & 34 deletions database/db_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func init() {
}

func (dbs *DBService) AddPosition(position models.Position, strategy string, constants []float64,
profitPct float64, benefits float64, cumulatedGain float64) uint {
profitPct float64, benefits float64, exitTrigger models.ExitTrigger) uint {

var dbConstants []database.Constant
for _, constant := range constants {
Expand All @@ -48,14 +48,14 @@ func (dbs *DBService) AddPosition(position models.Position, strategy string, con
var dbPosition database.Position
if position.IsClosed() {
dbPosition = database.Position{
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: position.ExitOrder().Time,
Gain: benefits,
CumulatedGain: cumulatedGain,
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: position.ExitOrder().Time,
Gain: benefits,
ExitTrigger: exitTrigger,
Orders: []database.Order{{
OrderID: position.EntranceOrder().OrderID,
ClientOrderID: position.EntranceOrder().ClientOrderID,
Expand Down Expand Up @@ -92,14 +92,14 @@ func (dbs *DBService) AddPosition(position models.Position, strategy string, con
}
} else {
dbPosition = database.Position{
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: 0,
Gain: benefits,
CumulatedGain: cumulatedGain,
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: position.ExitOrder().Time,
Gain: benefits,
ExitTrigger: exitTrigger,
Orders: []database.Order{{
OrderID: position.EntranceOrder().OrderID,
ClientOrderID: position.EntranceOrder().ClientOrderID,
Expand All @@ -125,7 +125,7 @@ func (dbs *DBService) AddPosition(position models.Position, strategy string, con
}

func (dbs *DBService) UpdatePosition(positionID uint, position models.Position, strategy string, constants []float64,
profitPct float64, benefits float64, cumulatedGain float64) {
profitPct float64, benefits float64, exitTrigger models.ExitTrigger) {

var dbConstants []database.Constant
for _, constant := range constants {
Expand All @@ -135,14 +135,14 @@ func (dbs *DBService) UpdatePosition(positionID uint, position models.Position,
var dbPosition database.Position
if position.IsClosed() {
dbPosition = database.Position{
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: position.ExitOrder().Time,
Gain: benefits,
CumulatedGain: cumulatedGain,
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: position.ExitOrder().Time,
Gain: benefits,
ExitTrigger: exitTrigger,
Orders: []database.Order{{
OrderID: position.EntranceOrder().OrderID,
ClientOrderID: position.EntranceOrder().ClientOrderID,
Expand Down Expand Up @@ -179,14 +179,14 @@ func (dbs *DBService) UpdatePosition(positionID uint, position models.Position,
}
} else {
dbPosition = database.Position{
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: 0,
Gain: benefits,
CumulatedGain: cumulatedGain,
Symbol: position.EntranceOrder().Symbol,
Strategy: strategy,
Constants: dbConstants,
Profit: profitPct,
EntryTime: position.EntranceOrder().Time,
ExitTime: 0,
Gain: benefits,
ExitTrigger: exitTrigger,
Orders: []database.Order{{
OrderID: position.EntranceOrder().OrderID,
ClientOrderID: position.EntranceOrder().ClientOrderID,
Expand Down
23 changes: 13 additions & 10 deletions database/models/db_position.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package database

import "gorm.io/gorm"
import (
"gitlab.com/aoterocom/AOCryptobot/models"
"gorm.io/gorm"
)

// Position is a pair of two Order objects
type Position struct {
gorm.Model
Symbol string `json:"symbol"`
EntryTime int64 `json:"entryTime"`
ExitTime int64 `json:"exitTime"`
Orders []Order `gorm:"foreignKey:PositionID"`
Strategy string
Constants []Constant `gorm:"foreignKey:PositionID"`
Profit float64
Gain float64
CumulatedGain float64
Symbol string `json:"symbol"`
EntryTime int64 `json:"entryTime"`
ExitTime int64 `json:"exitTime"`
Orders []Order `gorm:"foreignKey:PositionID"`
Strategy string
Constants []Constant `gorm:"foreignKey:PositionID"`
Profit float64
Gain float64
ExitTrigger models.ExitTrigger
}

type Constant struct {
Expand Down
2 changes: 1 addition & 1 deletion helpers/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (l *FileLogger) Infoln(args ...interface{}) {
if l.telegramOutput {
err := sendOnTelegramChannel(fmt.Sprintf("%s", args[0]), l.telegramToken, l.telegramChatId)
if err != nil {
log.Fatal(err)
log.Errorln(err)
}
}

Expand Down
Loading

0 comments on commit 5b0f335

Please sign in to comment.