Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulate Trailing and Stop #43

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 26 additions & 2 deletions algobot/enums.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# TODO: Add unit tests.
from typing import Optional

BULLISH = 1
BEARISH = -1
Expand All @@ -17,8 +18,31 @@ class GraphType:
LONG = 1
SHORT = -1

TRAILING = 2
STOP = 1

class OrderType:
Copy link
Contributor Author

@inverse inverse Jul 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still could be improved but it's a step in the right direction. by improvements we could use real enums and get read of the whole from_str but that would require a lot more mapping ;)

Like mapping the UI element to string -> enum value :)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

TRAILING = 2
STOP = 1

@staticmethod
def from_str(value: str) -> int:
if value.lower() == "trailing":
return OrderType.TRAILING
elif value.lower() == "stop":
return OrderType.STOP
else:
raise ValueError(f"{value} is unsupported")

@staticmethod
def to_str(order_type: Optional[int]) -> str:
if order_type == OrderType.STOP:
return "Stop"
elif order_type == OrderType.TRAILING:
return "Trailing"
elif order_type is None:
return "None"
else:
raise ValueError(f"Unknown OrderType with value {order_type}")


BACKTEST = 2
SIMULATION = 3
Expand Down
19 changes: 10 additions & 9 deletions algobot/interface/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
QLabel, QLayout, QMainWindow, QSpinBox,
QTabWidget)

from algobot.enums import BACKTEST, LIVE, OPTIMIZER, SIMULATION, STOP, TRAILING
from algobot.enums import BACKTEST, LIVE, OPTIMIZER, SIMULATION, OrderType
from algobot.graph_helpers import create_infinite_line
from algobot.helpers import ROOT_DIR
from algobot.interface.config_utils.credential_utils import load_credentials
Expand Down Expand Up @@ -344,9 +344,9 @@ def get_take_profit_settings(self, caller) -> dict:
dictionary = self.takeProfitDict
if dictionary[tab, 'groupBox'].isChecked():
if dictionary[tab, 'takeProfitType'].currentText() == "Trailing":
takeProfitType = TRAILING
takeProfitType = OrderType.TRAILING
else:
takeProfitType = STOP
takeProfitType = OrderType.STOP
else:
takeProfitType = None

Expand Down Expand Up @@ -381,21 +381,22 @@ def get_loss_settings(self, caller: int) -> dict:
tab = self.get_category_tab(caller)
dictionary = self.lossDict
if dictionary[tab, 'groupBox'].isChecked():
lossType = TRAILING if dictionary[tab, "lossType"].currentText() == "Trailing" else STOP
loss_type = dictionary[tab, "lossType"].currentText()
loss_strategy = OrderType.TRAILING if loss_type == "Trailing" else OrderType.STOP
else:
lossType = None
loss_strategy = None

lossSettings = {
'lossType': lossType,
loss_settings = {
'lossType': loss_strategy,
'lossTypeIndex': dictionary[tab, "lossType"].currentIndex(),
'lossPercentage': dictionary[tab, 'lossPercentage'].value(),
'smartStopLossCounter': dictionary[tab, 'smartStopLossCounter'].value()
}

if tab != self.backtestConfigurationTabWidget:
lossSettings['safetyTimer'] = dictionary[tab, 'safetyTimer'].value()
loss_settings['safetyTimer'] = dictionary[tab, 'safetyTimer'].value()

return lossSettings
return loss_settings

def add_strategy_to_config(self, caller: int, strategyName: str, config: dict):
"""
Expand Down
9 changes: 5 additions & 4 deletions algobot/traders/backtester.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from dateutil import parser

from algobot.enums import (BACKTEST, BEARISH, BULLISH, ENTER_LONG, ENTER_SHORT,
EXIT_LONG, EXIT_SHORT, LONG, OPTIMIZER, SHORT)
EXIT_LONG, EXIT_SHORT, LONG, OPTIMIZER, SHORT,
OrderType)
from algobot.helpers import (LOG_FOLDER, ROOT_DIR,
convert_all_dates_to_datetime,
convert_small_interval, get_interval_minutes,
Expand Down Expand Up @@ -442,7 +443,7 @@ def get_basic_optimize_info(self, run: int, totalRuns: int, result: str = 'PASSE
round(self.get_net() / self.startingBalance * 100 - 100, 2),
self.get_stop_loss_strategy_string(),
self.get_safe_rounded_string(self.lossPercentageDecimal, multiplier=100, symbol='%'),
self.get_trailing_or_stop_type_string(self.takeProfitType),
OrderType.to_str(self.takeProfitType),
self.get_safe_rounded_string(self.takeProfitPercentageDecimal, multiplier=100, symbol='%'),
self.symbol,
self.interval,
Expand Down Expand Up @@ -479,11 +480,11 @@ def apply_general_settings(self, settings: Dict[str, Union[float, str, dict]]):
:param settings: Dictionary with keys and values to set.
"""
if 'takeProfitType' in settings:
self.takeProfitType = self.get_enum_from_str(settings['takeProfitType'])
self.takeProfitType = OrderType.from_str(settings['takeProfitType'])
self.takeProfitPercentageDecimal = settings['takeProfitPercentage'] / 100

if 'lossType' in settings:
self.lossStrategy = self.get_enum_from_str(settings['lossType'])
self.lossStrategy = OrderType.from_str(settings['lossType'])
self.lossPercentageDecimal = settings['lossPercentage'] / 100

if 'stopLossCounter' in settings:
Expand Down
4 changes: 2 additions & 2 deletions algobot/traders/simulationtrader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from algobot.data import Data
from algobot.enums import (BEARISH, BULLISH, ENTER_LONG, ENTER_SHORT,
EXIT_LONG, EXIT_SHORT, LONG, SHORT)
EXIT_LONG, EXIT_SHORT, LONG, SHORT, OrderType)
from algobot.helpers import convert_small_interval, get_logger
from algobot.traders.trader import Trader

Expand Down Expand Up @@ -107,7 +107,7 @@ def get_grouped_statistics(self) -> dict:

if self.takeProfitType is not None:
groupedDict['takeProfit'] = {
'takeProfitType': self.get_trailing_or_stop_type_string(self.takeProfitType),
'takeProfitType': OrderType.to_str(self.takeProfitType),
'takeProfitPercentage': self.get_safe_rounded_percentage(self.takeProfitPercentageDecimal),
'trailingTakeProfitActivated': str(self.trailingTakeProfitActivated),
'takeProfitPoint': self.get_safe_rounded_string(self.takeProfitPoint),
Expand Down
40 changes: 9 additions & 31 deletions algobot/traders/trader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Dict, List, Union

from algobot.enums import (BEARISH, BULLISH, ENTER_LONG, ENTER_SHORT,
EXIT_LONG, EXIT_SHORT, LONG, SHORT, STOP, TRAILING)
EXIT_LONG, EXIT_SHORT, LONG, SHORT, OrderType)
from algobot.helpers import get_label_string, parse_strategy_name
from algobot.strategies.strategy import Strategy

Expand Down Expand Up @@ -224,16 +224,16 @@ def get_stop_loss(self):
if self.currentPosition == SHORT:
if self.smartStopLossEnter and self.previousStopLoss > self.currentPrice:
self.stopLoss = self.previousStopLoss
elif self.lossStrategy == TRAILING:
elif self.lossStrategy == OrderType.TRAILING:
self.stopLoss = self.shortTrailingPrice * (1 + self.lossPercentageDecimal)
elif self.lossStrategy == STOP:
elif self.lossStrategy == OrderType.STOP:
self.stopLoss = self.sellShortPrice * (1 + self.lossPercentageDecimal)
elif self.currentPosition == LONG:
if self.smartStopLossEnter and self.previousStopLoss < self.currentPrice:
self.stopLoss = self.previousStopLoss
elif self.lossStrategy == TRAILING:
elif self.lossStrategy == OrderType.TRAILING:
self.stopLoss = self.longTrailingPrice * (1 - self.lossPercentageDecimal)
elif self.lossStrategy == STOP:
elif self.lossStrategy == OrderType.STOP:
self.stopLoss = self.buyLongPrice * (1 - self.lossPercentageDecimal)

if self.stopLoss is not None: # This is for the smart stop loss to reenter position.
Expand All @@ -246,9 +246,9 @@ def get_stop_loss_strategy_string(self) -> str:
Returns stop loss strategy in string format, instead of integer enum.
:return: Stop loss strategy in string format.
"""
if self.lossStrategy == STOP:
if self.lossStrategy == OrderType.STOP:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably deprecate this function and add a kwarg for suffix in the to_str method

return 'Stop Loss'
elif self.lossStrategy == TRAILING:
elif self.lossStrategy == OrderType.TRAILING:
return 'Trailing Loss'
elif self.lossStrategy is None:
return 'None'
Expand Down Expand Up @@ -320,28 +320,6 @@ def get_profit_percentage(initialNet: float, finalNet: float) -> float:
else:
return -1 * (100 - finalNet / initialNet * 100)

@staticmethod
def get_trailing_or_stop_type_string(stopType: Union[int, None]) -> str:
"""
Returns stop type in string format instead of integer enum.
:return: Stop type in string format.
"""
if stopType == STOP:
return 'Stop'
elif stopType == TRAILING:
return 'Trailing'
elif stopType is None:
return 'None'
else:
raise ValueError("Unknown type of exit position type.")

@staticmethod
def get_enum_from_str(string):
if string.lower() == "trailing":
return TRAILING
elif string.lower() == 'stop':
return STOP

@staticmethod
def get_trend_string(trend) -> str:
"""
Expand Down Expand Up @@ -436,12 +414,12 @@ def get_take_profit(self) -> Union[float, None]:
return None

if self.currentPosition == SHORT:
if self.takeProfitType == STOP:
if self.takeProfitType == OrderType.STOP:
self.takeProfitPoint = self.sellShortPrice * (1 - self.takeProfitPercentageDecimal)
else:
raise ValueError("Invalid type of take profit type provided.")
elif self.currentPosition == LONG:
if self.takeProfitType == STOP:
if self.takeProfitType == OrderType.STOP:
self.takeProfitPoint = self.buyLongPrice * (1 + self.takeProfitPercentageDecimal)
else:
raise ValueError("Invalid type of take profit type provided.")
Expand Down
16 changes: 8 additions & 8 deletions tests/test_backtester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from algobot.enums import LONG, SHORT, STOP, TRAILING
from algobot.enums import LONG, SHORT, OrderType
from algobot.helpers import convert_all_dates_to_datetime, load_from_csv
from algobot.traders.backtester import Backtester

Expand All @@ -25,8 +25,8 @@ def setUp(self) -> None:
symbol="1INCHUSDT",
marginEnabled=True,
)
self.backtester.apply_take_profit_settings({'takeProfitType': TRAILING, 'takeProfitPercentage': 5})
self.backtester.apply_loss_settings({'lossType': TRAILING, 'lossPercentage': 5})
self.backtester.apply_take_profit_settings({'takeProfitType': OrderType.TRAILING, 'takeProfitPercentage': 5})
self.backtester.apply_loss_settings({'lossType': OrderType.TRAILING, 'lossPercentage': 5})

def test_initialization(self):
"""
Expand Down Expand Up @@ -281,39 +281,39 @@ def test_long_stop_loss(self):
Test backtester stop loss logic in a long position.
"""
backtester = self.backtester
backtester.lossStrategy = STOP
backtester.lossStrategy = OrderType.STOP
backtester.set_priced_current_price_and_period(5)
backtester.buy_long("Test purchase.")
self.assertEqual(backtester.get_stop_loss(), 5 * (1 - backtester.lossPercentageDecimal))

backtester.set_priced_current_price_and_period(10)
self.assertEqual(backtester.get_stop_loss(), 5 * (1 - backtester.lossPercentageDecimal))

backtester.lossStrategy = TRAILING
backtester.lossStrategy = OrderType.TRAILING
self.assertEqual(backtester.get_stop_loss(), 10 * (1 - backtester.lossPercentageDecimal))

def test_short_stop_loss(self):
"""
Test backtester stop loss logic in a short position.
"""
backtester = self.backtester
backtester.lossStrategy = STOP
backtester.lossStrategy = OrderType.STOP
backtester.set_priced_current_price_and_period(5)
backtester.sell_short("Test short.")
self.assertEqual(backtester.get_stop_loss(), 5 * (1 + backtester.lossPercentageDecimal))

backtester.set_priced_current_price_and_period(3)
self.assertEqual(backtester.get_stop_loss(), 5 * (1 + backtester.lossPercentageDecimal))

backtester.lossStrategy = TRAILING
backtester.lossStrategy = OrderType.TRAILING
self.assertEqual(backtester.get_stop_loss(), 3 * (1 + backtester.lossPercentageDecimal))

def test_stop_take_profit(self):
"""
Test backtester take profit logic.
"""
backtester = self.backtester
backtester.takeProfitType = STOP
backtester.takeProfitType = OrderType.STOP
backtester.set_priced_current_price_and_period(10)
backtester.buy_long("Test purchase.")
self.assertEqual(backtester.get_take_profit(), 10 * (1 + backtester.takeProfitPercentageDecimal))
Expand Down
23 changes: 9 additions & 14 deletions tests/test_base_trader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from algobot.enums import BEARISH, BULLISH, LONG, SHORT, STOP, TRAILING
from algobot.enums import BEARISH, BULLISH, LONG, SHORT, OrderType
from algobot.strategies.strategy import Strategy
from algobot.traders.trader import Trader

Expand Down Expand Up @@ -149,30 +149,30 @@ def test_set_safety_timer(self):
def test_apply_take_profit_settings(self):
take_profit_settings = {
'takeProfitPercentage': 25,
'takeProfitType': STOP
'takeProfitType': OrderType.STOP
}
self.trader.apply_take_profit_settings(take_profit_settings)

self.assertEqual(self.trader.takeProfitPercentageDecimal, 0.25)
self.assertEqual(self.trader.takeProfitType, STOP)
self.assertEqual(self.trader.takeProfitType, OrderType.STOP)

def test_apply_loss_settings(self):
loss_settings = {
'lossType': STOP,
'lossType': OrderType.STOP,
'lossPercentage': 5.5,
'smartStopLossCounter': 15,
'safetyTimer': 45
}
self.trader.apply_loss_settings(loss_settings)

self.assertEqual(self.trader.lossStrategy, STOP)
self.assertEqual(self.trader.lossStrategy, OrderType.STOP)
self.assertEqual(self.trader.lossPercentageDecimal, 0.055)
self.assertEqual(self.trader.smartStopLossInitialCounter, 15)
self.assertEqual(self.trader.smartStopLossCounter, 15)
self.assertEqual(self.trader.safetyTimer, 45)

def test_get_stop_loss(self):
self.trader.lossStrategy = STOP
self.trader.lossStrategy = OrderType.STOP
self.trader.lossPercentageDecimal = 0.1
self.trader.currentPrice = 5

Expand All @@ -190,10 +190,10 @@ def test_get_stop_loss(self):
# TODO implement trailing stop loss test

def test_get_stop_loss_strategy_string(self):
self.trader.lossStrategy = STOP
self.trader.lossStrategy = OrderType.STOP
self.assertEqual(self.trader.get_stop_loss_strategy_string(), "Stop Loss")

self.trader.lossStrategy = TRAILING
self.trader.lossStrategy = OrderType.TRAILING
self.assertEqual(self.trader.get_stop_loss_strategy_string(), "Trailing Loss")

self.trader.lossStrategy = None
Expand Down Expand Up @@ -273,11 +273,6 @@ def test_get_profit_percentage(self):
self.assertEqual(self.trader.get_profit_percentage(100, 50), -50)
self.assertEqual(self.trader.get_profit_percentage(100, 130), 30)

def test_get_trailing_or_stop_loss_string(self):
self.assertEqual(self.trader.get_trailing_or_stop_type_string(STOP), 'Stop')
self.assertEqual(self.trader.get_trailing_or_stop_type_string(TRAILING), 'Trailing')
self.assertEqual(self.trader.get_trailing_or_stop_type_string(None), 'None')

def test_get_trend_string(self):
self.assertEqual(self.trader.get_trend_string(None), str(None))
self.assertEqual(self.trader.get_trend_string(BEARISH), "Bearish")
Expand Down Expand Up @@ -321,7 +316,7 @@ def test_get_safe_rounded_string(self):
multiplier=5), '6.15*')

def test_get_take_profit(self):
self.trader.takeProfitType = STOP
self.trader.takeProfitType = OrderType.STOP
self.trader.takeProfitPercentageDecimal = 0.05

self.trader.currentPosition = LONG
Expand Down
22 changes: 22 additions & 0 deletions tests/test_enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import unittest

from algobot.enums import OrderType


class OrderTypeTest(unittest.TestCase):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use pytest? We should soon start rewriting all unit test tests to leverage pytest :p

def test_from_str(self):
self.assertEqual(OrderType.from_str("Stop"), OrderType.STOP)
self.assertEqual(OrderType.from_str("Trailing"), OrderType.TRAILING)

def test_from_str_unsupported(self):
with self.assertRaises(ValueError):
OrderType.from_str("Random")

def test_to_str(self):
self.assertEqual(OrderType.to_str(OrderType.STOP), "Stop")
self.assertEqual(OrderType.to_str(OrderType.TRAILING), "Trailing")
self.assertEqual(OrderType.to_str(None), "None")

def test_to_str_unsupported(self):
with self.assertRaises(ValueError):
OrderType.to_str(100)