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 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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
49 changes: 25 additions & 24 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 @@ -73,7 +73,7 @@ def __init__(self, parent: QMainWindow, logger: Logger = None):
}

self.lossTypes = ("Trailing", "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 leverage the actual enum values? let's not use integers but actual strings? what do you think? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could work - trying to think of how would be best to handle semantics vs presentation

Copy link
Owner

Choose a reason for hiding this comment

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

Well, as long as the enums stay the same, we can just replace the value.
So TRAILING can change from 1 to "Trailing", then we don't even need a mapper ;p

self.takeProfitTypes = ('Stop',)
self.takeOrderTypes = ('Stop',)
inverse marked this conversation as resolved.
Show resolved Hide resolved
self.lossOptimizerTypes = ('lossPercentage', 'stopLossCounter')
self.takeProfitOptimizerTypes = ('takeProfitPercentage',)

Expand Down Expand Up @@ -199,7 +199,7 @@ def get_optimizer_settings(self) -> dict:
settings = {}

self.helper_get_optimizer(tab, self.lossDict, 'lossType', self.lossOptimizerTypes, settings)
self.helper_get_optimizer(tab, self.takeProfitDict, 'takeProfitType', self.takeProfitOptimizerTypes, settings)
self.helper_get_optimizer(tab, self.takeProfitDict, 'takeOrderType', self.takeProfitOptimizerTypes, settings)
self.get_strategy_intervals_for_optimizer(settings)

settings['strategies'] = {}
Expand Down Expand Up @@ -282,26 +282,26 @@ def create_take_profit_inputs(self, tab: QTabWidget, innerLayout: QLayout, isOpt
if isOptimizer:
self.takeProfitDict['optimizerTypes'] = []
innerLayout.addRow(QLabel("Take Profit Types"))
for takeProfitType in self.takeProfitTypes:
checkbox = QCheckBox(f'Enable {takeProfitType} take profit?')
for takeOrderType in self.takeOrderTypes:
checkbox = QCheckBox(f'Enable {takeOrderType} take profit?')
inverse marked this conversation as resolved.
Show resolved Hide resolved
innerLayout.addRow(checkbox)
self.takeProfitDict['optimizerTypes'].append((takeProfitType, checkbox))
self.takeProfitDict['optimizerTypes'].append((takeOrderType, checkbox))

for optimizerType in self.takeProfitOptimizerTypes:
self.takeProfitDict[optimizerType, 'start'] = start = get_default_widget(QSpinBox, 1, 0)
self.takeProfitDict[optimizerType, 'end'] = end = get_default_widget(QSpinBox, 1, 0)
self.takeProfitDict[optimizerType, 'step'] = step = get_default_widget(QSpinBox, 1)
add_start_end_step_to_layout(innerLayout, optimizerType, start, end, step)
else:
self.takeProfitDict[tab, 'takeProfitType'] = takeProfitTypeComboBox = QComboBox()
self.takeProfitDict[tab, 'takeOrderType'] = takeOrderTypeComboBox = QComboBox()
self.takeProfitDict[tab, 'takeProfitPercentage'] = takeProfitPercentage = QDoubleSpinBox()

takeProfitTypeComboBox.addItems(self.takeProfitTypes)
takeProfitTypeComboBox.currentIndexChanged.connect(lambda: self.update_take_profit_settings(tab))
takeOrderTypeComboBox.addItems(self.takeOrderTypes)
takeOrderTypeComboBox.currentIndexChanged.connect(lambda: self.update_take_profit_settings(tab))
takeProfitPercentage.setValue(5)
takeProfitPercentage.valueChanged.connect(lambda: self.update_take_profit_settings(tab))

innerLayout.addRow(QLabel("Take Profit Type"), takeProfitTypeComboBox)
innerLayout.addRow(QLabel("Take Profit Type"), takeOrderTypeComboBox)
innerLayout.addRow(QLabel('Take Profit Percentage'), takeProfitPercentage)

def set_loss_settings(self, caller: int, config: dict):
Expand All @@ -327,11 +327,11 @@ def set_take_profit_settings(self, caller: int, config: dict):
:param caller: This caller's tab's GUI will be modified by this function.
:param config: Configuration dictionary from which to get take profit settings.
"""
if "takeProfitTypeIndex" not in config: # We don't have this data in config, so just return.
if "takeOrderTypeIndex" not in config: # We don't have this data in config, so just return.
return

tab = self.get_category_tab(caller)
self.takeProfitDict[tab, 'takeProfitType'].setCurrentIndex(config["takeProfitTypeIndex"])
self.takeProfitDict[tab, 'takeOrderType'].setCurrentIndex(config["takeOrderTypeIndex"])
self.takeProfitDict[tab, 'takeProfitPercentage'].setValue(config["takeProfitPercentage"])

def get_take_profit_settings(self, caller) -> dict:
Expand All @@ -343,16 +343,16 @@ def get_take_profit_settings(self, caller) -> dict:
tab = self.get_category_tab(caller)
dictionary = self.takeProfitDict
if dictionary[tab, 'groupBox'].isChecked():
if dictionary[tab, 'takeProfitType'].currentText() == "Trailing":
takeProfitType = TRAILING
if dictionary[tab, 'takeOrderType'].currentText() == "Trailing":
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 use the to_str method()

takeOrderType = OrderType.TRAILING
else:
takeProfitType = STOP
takeOrderType = OrderType.STOP
else:
takeProfitType = None
takeOrderType = None

return {
'takeProfitType': takeProfitType,
'takeProfitTypeIndex': dictionary[tab, 'takeProfitType'].currentIndex(),
'takeOrderType': takeOrderType,
'takeOrderTypeIndex': dictionary[tab, 'takeOrderType'].currentIndex(),
'takeProfitPercentage': dictionary[tab, 'takeProfitPercentage'].value()
}

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
17 changes: 9 additions & 8 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.takeOrderType),
self.get_safe_rounded_string(self.takeProfitPercentageDecimal, multiplier=100, symbol='%'),
self.symbol,
self.interval,
Expand Down Expand Up @@ -478,12 +479,12 @@ def apply_general_settings(self, settings: Dict[str, Union[float, str, dict]]):
Apples settings provided from the settings argument to the backtester object.
:param settings: Dictionary with keys and values to set.
"""
if 'takeProfitType' in settings:
self.takeProfitType = self.get_enum_from_str(settings['takeProfitType'])
if 'takeOrderType' in settings:
self.takeOrderType = OrderType.from_str(settings['takeOrderType'])
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 Expand Up @@ -629,7 +630,7 @@ def main_logic(self):
if self.currentPosition == SHORT:
if self.lossStrategy is not None and self.currentPrice > self.get_stop_loss():
self.buy_short('Exited short because a stop loss was triggered.', stopLossExit=True)
elif self.takeProfitType is not None and self.currentPrice <= self.get_take_profit():
elif self.takeOrderType is not None and self.currentPrice <= self.get_take_profit():
self.buy_short("Exited short because of take profit.")
elif trend == BULLISH:
self.buy_short('Exited short because a bullish trend was detected.')
Expand All @@ -639,7 +640,7 @@ def main_logic(self):
elif self.currentPosition == LONG:
if self.lossStrategy is not None and self.currentPrice < self.get_stop_loss():
self.sell_long('Exited long because a stop loss was triggered.', stopLossExit=True)
elif self.takeProfitType is not None and self.currentPrice >= self.get_take_profit():
elif self.takeOrderType is not None and self.currentPrice >= self.get_take_profit():
self.sell_long("Exited long because of take profit.")
elif trend == BEARISH:
self.sell_long('Exited long because a bearish trend was detected.')
Expand Down Expand Up @@ -688,7 +689,7 @@ def print_configuration_parameters(self, stdout=None):
print(f'\tMargin Enabled: {self.marginEnabled}')
print(f"\tStarting Balance: ${self.startingBalance}")

if self.takeProfitType is not None:
if self.takeOrderType is not None:
print(f'\tTake Profit Percentage: {round(self.takeProfitPercentageDecimal * 100, 2)}%')

if self.lossStrategy is not None:
Expand Down
6 changes: 3 additions & 3 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 @@ -105,9 +105,9 @@ def get_grouped_statistics(self) -> dict:
'scheduledTimerRemaining': self.get_remaining_safety_timer(),
}

if self.takeProfitType is not None:
if self.takeOrderType is not None:
groupedDict['takeProfit'] = {
'takeProfitType': self.get_trailing_or_stop_type_string(self.takeProfitType),
'takeOrderType': OrderType.to_str(self.takeOrderType),
'takeProfitPercentage': self.get_safe_rounded_percentage(self.takeProfitPercentageDecimal),
'trailingTakeProfitActivated': str(self.trailingTakeProfitActivated),
'takeProfitPoint': self.get_safe_rounded_string(self.takeProfitPoint),
Expand Down
46 changes: 12 additions & 34 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 @@ -36,7 +36,7 @@ def __init__(self, symbol, precision, startingBalance, marginEnabled: bool = Tru

self.takeProfitPoint = None # Price at which bot will exit trade to secure profits.
self.trailingTakeProfitActivated = False # Boolean that'll turn true if a stop order is activated.
self.takeProfitType = None # Type of take profit: trailing or stop.
self.takeOrderType = None # Type of take profit: trailing or stop.
self.takeProfitPercentageDecimal = None # Percentage of profit to exit trade at.

# Prices information.
Expand Down Expand Up @@ -172,7 +172,7 @@ def apply_take_profit_settings(self, takeProfitDict: Dict[str, int]):
:return: None
"""
self.takeProfitPercentageDecimal = takeProfitDict["takeProfitPercentage"] / 100
self.takeProfitType = takeProfitDict["takeProfitType"]
self.takeOrderType = takeProfitDict["takeOrderType"]

def apply_loss_settings(self, lossDict: Dict[str, int]):
"""
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 @@ -432,16 +410,16 @@ def get_take_profit(self) -> Union[float, None]:
Returns price at which position will be exited to secure profits.
:return: Price at which to exit position.
"""
if self.takeProfitType is None:
if self.takeOrderType is None:
return None

if self.currentPosition == SHORT:
if self.takeProfitType == STOP:
if self.takeOrderType == 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.takeOrderType == OrderType.STOP:
self.takeProfitPoint = self.buyLongPrice * (1 + self.takeProfitPercentageDecimal)
else:
raise ValueError("Invalid type of take profit type provided.")
Expand Down
Loading