Skip to content

Commit fc06f57

Browse files
committed
Add initial stop loss and take profit support
1 parent cd17db7 commit fc06f57

24 files changed

+1155
-58
lines changed

investing_algorithm_framework/app/algorithm.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,51 @@ def add_trailing_stop_loss(self, trade, percentage: int) -> None:
14131413
"""
14141414
self.trade_service.add_trailing_stop_loss(trade, percentage=percentage)
14151415

1416+
def add_take_profit(self, trade, percentage: int) -> None:
1417+
"""
1418+
Function to add a take profit to a trade. This function will add a
1419+
take profit to the specified trade. If the take profit is triggered,
1420+
the trade will be closed.
1421+
1422+
Example of take profit:
1423+
* You buy BTC at $40,000.
1424+
* You set a TP of 5% → TP level at $42,000 (40,000 + 5%).
1425+
* BTC rises to $42,000 → TP level reached, trade
1426+
closes, securing profit.
1427+
1428+
Args:
1429+
trade: Trade - The trade to add the take profit to
1430+
percentage: int - The take profit of the trade
1431+
1432+
Returns:
1433+
None
1434+
"""
1435+
self.trade_service.add_take_profit(trade, percentage=percentage)
1436+
1437+
def add_trailing_take_profit(self, trade, percentage: int) -> None:
1438+
"""
1439+
Function to add a trailing take profit to a trade. This function will
1440+
add a trailing take profit to the specified trade. If the trailing
1441+
take profit is triggered, the trade will be closed.
1442+
1443+
Example of trailing take profit:
1444+
* You buy BTC at $40,000.
1445+
* You set a TTP of 5%.
1446+
* BTC rises to $42,000 → New TTP level at $39,900 (42,000 - 5%).
1447+
* BTC rises to $45,000 → New TTP level at $42,750.
1448+
* BTC drops to $42,750 → Trade closes, securing profit.
1449+
1450+
Args:
1451+
trade: Trade - The trade to add the trailing take profit to
1452+
percentage: int - The trailing take profit of the trade
1453+
1454+
Returns:
1455+
None
1456+
"""
1457+
self.trade_service.add_trailing_take_profit(
1458+
trade, percentage=percentage
1459+
)
1460+
14161461
def close_trade(self, trade, precision=None) -> None:
14171462
"""
14181463
Function to close a trade. This function will close a trade by

investing_algorithm_framework/dependency_container.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from investing_algorithm_framework.infrastructure import SQLOrderRepository, \
55
SQLPositionRepository, SQLPortfolioRepository, \
66
SQLPortfolioSnapshotRepository, SQLTradeRepository, \
7-
SQLPositionSnapshotRepository, PerformanceService, CCXTMarketService
7+
SQLPositionSnapshotRepository, PerformanceService, CCXTMarketService, \
8+
SQLTradeStopLossRepository, SQLTradeTakeProfitRepository
89
from investing_algorithm_framework.services import OrderService, \
910
PositionService, PortfolioService, StrategyOrchestratorService, \
1011
PortfolioConfigurationService, MarketDataSourceService, BacktestService, \
@@ -41,6 +42,10 @@ class DependencyContainer(containers.DeclarativeContainer):
4142
SQLPortfolioSnapshotRepository
4243
)
4344
trade_repository = providers.Factory(SQLTradeRepository)
45+
trade_take_profit_repository = providers\
46+
.Factory(SQLTradeTakeProfitRepository)
47+
trade_stop_loss_repository = providers.Factory(SQLTradeStopLossRepository)
48+
4449
market_service = providers.Factory(
4550
CCXTMarketService,
4651
market_credential_service=market_credential_service,
@@ -75,6 +80,8 @@ class DependencyContainer(containers.DeclarativeContainer):
7580
)
7681
trade_service = providers.Factory(
7782
TradeService,
83+
trade_take_profit_repository=trade_take_profit_repository,
84+
trade_stop_loss_repository=trade_stop_loss_repository,
7885
configuration_service=configuration_service,
7986
trade_repository=trade_repository,
8087
portfolio_repository=portfolio_repository,

investing_algorithm_framework/domain/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
BacktestReport, PortfolioSnapshot, StrategyProfile, \
2020
BacktestPosition, Trade, MarketCredential, PositionSnapshot, \
2121
BacktestReportsEvaluation, AppMode, BacktestDateRange, DateRange, \
22-
MarketDataType
22+
MarketDataType, TradeRiskType, TradeTakeProfit, TradeStopLoss
2323
from .services import TickerMarketDataSource, OrderBookMarketDataSource, \
2424
OHLCVMarketDataSource, BacktestMarketDataSource, MarketDataSource, \
2525
MarketService, MarketCredentialService, AbstractPortfolioSyncService, \
@@ -124,5 +124,8 @@
124124
"DEFAULT_LOGGING_CONFIG",
125125
"DATABASE_DIRECTORY_NAME",
126126
"BACKTESTING_INITIAL_AMOUNT",
127-
"MarketDataType"
127+
"MarketDataType",
128+
"TradeRiskType",
129+
"TradeTakeProfit",
130+
"TradeStopLoss",
128131
]

investing_algorithm_framework/domain/models/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from .time_frame import TimeFrame
1010
from .time_interval import TimeInterval
1111
from .time_unit import TimeUnit
12-
from .trade import Trade, TradeStatus
12+
from .trade import Trade, TradeStatus, TradeStopLoss, TradeTakeProfit, \
13+
TradeRiskType
1314
from .trading_data_types import TradingDataType
1415
from .trading_time_frame import TradingTimeFrame
1516
from .date_range import DateRange
@@ -40,5 +41,8 @@
4041
"AppMode",
4142
"BacktestDateRange",
4243
"DateRange",
43-
"MarketDataType"
44+
"MarketDataType",
45+
"TradeStopLoss",
46+
"TradeTakeProfit",
47+
"TradeRiskType",
4448
]
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
from .trade import Trade
22
from .trade_status import TradeStatus
3+
from .trade_stop_loss import TradeStopLoss
4+
from .trade_take_profit import TradeTakeProfit
5+
from .trade_risk_type import TradeRiskType
36

4-
__all__ = ["Trade", "TradeStatus"]
7+
__all__ = [
8+
"Trade",
9+
"TradeStatus",
10+
"TradeStopLoss",
11+
"TradeTakeProfit",
12+
"TradeRiskType",
13+
]

investing_algorithm_framework/domain/models/trade/trade.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ class Trade(BaseModel):
3636
created_at (datetime): the datetime when the trade was created
3737
updated_at (datetime): the datetime when the trade was last updated
3838
status (str): the status of the trade
39-
stop_loss_percentage (float): the stop loss percentage of
40-
the trade
41-
trailing_stop_loss_percentage (float): the trailing stop
42-
loss percentage
39+
stop_losses (List[TradeStopLoss]): the stop losses of the trade
40+
take_profits (List[TradeTakeProfit]): the take profits of the trade
4341
"""
4442

4543
def __init__(
@@ -59,9 +57,8 @@ def __init__(
5957
last_reported_price=None,
6058
high_water_mark=None,
6159
updated_at=None,
62-
stop_loss_percentage=None,
63-
trailing_stop_loss_percentage=None,
64-
stop_loss_triggered=False,
60+
stop_losses=None,
61+
take_profits=None,
6562
):
6663
self.id = id
6764
self.orders = orders
@@ -78,9 +75,8 @@ def __init__(
7875
self.high_water_mark = high_water_mark
7976
self.status = status
8077
self.updated_at = updated_at
81-
self.stop_loss_percentage = stop_loss_percentage
82-
self.trailing_stop_loss_percentage = trailing_stop_loss_percentage
83-
self.stop_loss_triggered = stop_loss_triggered
78+
self.stop_losses = stop_losses
79+
self.take_profits = take_profits
8480

8581
@property
8682
def closed_prices(self):
@@ -228,6 +224,49 @@ def is_trailing_stop_loss_triggered(self):
228224

229225
return self.last_reported_price <= stop_loss_price
230226

227+
def is_take_profit_triggered(self):
228+
229+
if self.take_profit_percentage is None:
230+
return False
231+
232+
if self.last_reported_price is None:
233+
return False
234+
235+
if self.open_price is None:
236+
return False
237+
238+
take_profit_price = self.open_price * \
239+
(1 + (self.take_profit_percentage / 100))
240+
241+
return self.last_reported_price >= take_profit_price
242+
243+
def is_trailing_take_profit_triggered(self):
244+
"""
245+
Function to check if the trailing take profit is triggered.
246+
The trailing take profit is triggered when the last reported price
247+
is greater than or equal to the high water mark times the trailing
248+
take profit percentage.
249+
"""
250+
251+
if self.trailing_take_profit_percentage is None:
252+
return False
253+
254+
if self.last_reported_price is None:
255+
return False
256+
257+
if self.high_water_mark is None:
258+
259+
if self.open_price is not None:
260+
self.high_water_mark = self.open_price
261+
else:
262+
return False
263+
264+
take_profit_price = self.high_water_mark * \
265+
(1 + (self.trailing_take_profit_percentage / 100))
266+
267+
return self.last_reported_price >= take_profit_price
268+
269+
231270
def to_dict(self, datetime_format=None):
232271

233272
if datetime_format is not None:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from enum import Enum
2+
3+
4+
class TradeRiskType(Enum):
5+
FIXED = "FIXED"
6+
TRAILING = "TRAILING"
7+
8+
@staticmethod
9+
def from_string(value: str):
10+
11+
if isinstance(value, str):
12+
for status in TradeRiskType:
13+
14+
if value.upper() == status.value:
15+
return status
16+
17+
raise ValueError("Could not convert value to TradeRiskType")
18+
19+
@staticmethod
20+
def from_value(value):
21+
22+
if isinstance(value, TradeRiskType):
23+
for risk_type in TradeRiskType:
24+
25+
if value == risk_type:
26+
return risk_type
27+
28+
elif isinstance(value, str):
29+
return TradeRiskType.from_string(value)
30+
31+
raise ValueError("Could not convert value to TradeRiskType")
32+
33+
def equals(self, other):
34+
return TradeRiskType.from_value(other) == self
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from investing_algorithm_framework.domain.models.base_model import BaseModel
2+
from investing_algorithm_framework.domain.models.trade.trade_risk_type import \
3+
TradeRiskType
4+
from investing_algorithm_framework.domain.models.trade.trade import Trade
5+
6+
7+
class TradeStopLoss(BaseModel):
8+
"""
9+
TradeStopLoss represents a stop loss strategy for a trade.
10+
11+
Attributes:
12+
trade: Trade - the trade that the take profit is for
13+
take_profit: float - the take profit percentage
14+
take_risk_type: TradeRiskType - the type of trade risk, either
15+
trailing or fixed
16+
percentage: float - the stop loss percentage
17+
sell_percentage: float - the percentage of the trade to sell when the
18+
take profit is hit. Default is 100% of the trade. If the take profit percentage is lower than 100% a check must be made that
19+
the combined sell percentage of all take profits is less or
20+
equal than 100%.
21+
sell_amount: float - the amount to sell when the stop loss triggers
22+
sold_amount: float - the amount that has been sold
23+
high_water_mark: float - the highest price of the trade
24+
stop_loss_price: float - the price at which the stop loss triggers
25+
26+
if trade_risk_type is fixed, the stop loss price is calculated as follows:
27+
You buy a stock at $100.
28+
You set a 5% stop loss, meaning you will sell if
29+
the price drops to $95.
30+
If the price rises to $120, the stop loss is not triggered.
31+
But if the price keeps falling to $95, the stop loss triggers,
32+
and you exit with a $5 loss.
33+
34+
if trade_risk_type is trailing, the stop loss price is calculated as follows:
35+
You buy a stock at $100.
36+
You set a 5% trailing stop loss, meaning you will sell if
37+
the price drops 5% from its peak at $96
38+
If the price rises to $120, the stop loss adjusts
39+
to $114 (5% below $120).
40+
If the price falls to $114, the position is
41+
closed, securing a $14 profit.
42+
But if the price keeps rising to $150, the stop
43+
loss moves up to $142.50.
44+
If the price drops from $150 to $142.50, the stop
45+
loss triggers, and you exit with a $42.50 profit.
46+
"""
47+
48+
def __init__(
49+
self,
50+
trade_id: int,
51+
take_risk_type: TradeRiskType,
52+
percentage: float,
53+
open_price: float,
54+
total_amount_trade: float,
55+
sell_percentage: float = 100,
56+
active: bool = True
57+
):
58+
self.trade_id = trade_id
59+
self.take_risk_type = take_risk_type
60+
self.percentage = percentage
61+
self.sell_percentage = sell_percentage
62+
self.high_water_mark = open_price
63+
self.stop_loss_price = self.high_water_mark * \
64+
(1 - (self.percentage / 100))
65+
self.sell_amount = total_amount_trade * (self.sell_percentage / 100)
66+
self.sold_amount = 0
67+
self.active = active
68+
69+
def has_triggered(self, current_price: float) -> bool:
70+
"""
71+
Function to check if the stop loss has triggered.
72+
Function always returns False if the stop loss is not active or
73+
the sold amount is equal to the sell amount.
74+
75+
Args:
76+
current_price: float - the current price of the trade
77+
78+
Returns:
79+
bool - True if the stop loss has triggered, False otherwise
80+
"""
81+
82+
if not self.active or self.sold_amount == self.sell_amount:
83+
return False
84+
85+
if TradeRiskType.FIXED.equals(self.take_risk_type):
86+
# Check if the current price is less than the high water mark
87+
return current_price <= self.stop_loss_price
88+
else:
89+
# Check if the current price is less than the stop loss price
90+
if current_price <= self.stop_loss_price:
91+
return True
92+
elif current_price > self.high_water_mark:
93+
self.high_water_mark = current_price
94+
self.stop_loss_price = self.high_water_mark * \
95+
(1 - (self.percentage / 100))
96+
97+
return False
98+
99+
def get_sell_amount(self, trade: Trade) -> float:
100+
"""
101+
Function to calculate the amount to sell based on the
102+
sell percentage and the remaining amount of the trade.
103+
Keep in mind the moment the take profit triggers, the remaining
104+
amount of the trade is used to calculate the sell amount.
105+
If the remaining amount is smaller then the trade amount, the
106+
trade stop loss stays active. The client that uses the
107+
trade stop loss is responsible for setting the trade stop
108+
loss to inactive.
109+
110+
Args:
111+
trade: Trade - the trade to calculate the sell amount for
112+
113+
"""
114+
115+
if not self.active:
116+
return 0
117+
118+
return trade.remaining * (self.sell_percentage / 100)
119+
120+
def __str__(self) -> str:
121+
return (
122+
f"TradeStopLoss(trade_id={self.trade_id}, "
123+
f"take_profit_type={self.take_profit_type}, "
124+
f"percentage={self.percentage}, "
125+
f"sell_percentage={self.sell_percentage})"
126+
f"high_water_mark={self.high_water_mark}, "
127+
f"stop_loss_price={self.stop_loss_price}"
128+
)

0 commit comments

Comments
 (0)