investing-algorithm-framework 6.9.1__py3-none-any.whl → 7.19.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of investing-algorithm-framework might be problematic. Click here for more details.
- investing_algorithm_framework/__init__.py +147 -44
- investing_algorithm_framework/app/__init__.py +23 -6
- investing_algorithm_framework/app/algorithm/algorithm.py +5 -41
- investing_algorithm_framework/app/algorithm/algorithm_factory.py +17 -10
- investing_algorithm_framework/app/analysis/__init__.py +15 -0
- investing_algorithm_framework/app/analysis/backtest_data_ranges.py +121 -0
- investing_algorithm_framework/app/analysis/backtest_utils.py +107 -0
- investing_algorithm_framework/app/analysis/permutation.py +116 -0
- investing_algorithm_framework/app/analysis/ranking.py +297 -0
- investing_algorithm_framework/app/app.py +1322 -707
- investing_algorithm_framework/app/context.py +196 -88
- investing_algorithm_framework/app/eventloop.py +590 -0
- investing_algorithm_framework/app/reporting/__init__.py +16 -5
- investing_algorithm_framework/app/reporting/ascii.py +57 -202
- investing_algorithm_framework/app/reporting/backtest_report.py +284 -170
- investing_algorithm_framework/app/reporting/charts/__init__.py +10 -2
- investing_algorithm_framework/app/reporting/charts/entry_exist_signals.py +66 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve.py +37 -0
- investing_algorithm_framework/app/reporting/charts/equity_curve_drawdown.py +11 -26
- investing_algorithm_framework/app/reporting/charts/line_chart.py +11 -0
- investing_algorithm_framework/app/reporting/charts/ohlcv_data_completeness.py +51 -0
- investing_algorithm_framework/app/reporting/charts/rolling_sharp_ratio.py +1 -1
- investing_algorithm_framework/app/reporting/generate.py +100 -114
- investing_algorithm_framework/app/reporting/tables/key_metrics_table.py +40 -32
- investing_algorithm_framework/app/reporting/tables/time_metrics_table.py +34 -27
- investing_algorithm_framework/app/reporting/tables/trade_metrics_table.py +23 -19
- investing_algorithm_framework/app/reporting/tables/trades_table.py +1 -1
- investing_algorithm_framework/app/reporting/tables/utils.py +1 -0
- investing_algorithm_framework/app/reporting/templates/report_template.html.j2 +10 -16
- investing_algorithm_framework/app/strategy.py +315 -175
- investing_algorithm_framework/app/task.py +5 -3
- investing_algorithm_framework/cli/cli.py +30 -12
- investing_algorithm_framework/cli/deploy_to_aws_lambda.py +131 -34
- investing_algorithm_framework/cli/initialize_app.py +20 -1
- investing_algorithm_framework/cli/templates/app_aws_lambda_function.py.template +18 -6
- investing_algorithm_framework/cli/templates/aws_lambda_dockerfile.template +22 -0
- investing_algorithm_framework/cli/templates/aws_lambda_dockerignore.template +92 -0
- investing_algorithm_framework/cli/templates/aws_lambda_requirements.txt.template +2 -2
- investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template +1 -1
- investing_algorithm_framework/create_app.py +3 -5
- investing_algorithm_framework/dependency_container.py +25 -39
- investing_algorithm_framework/domain/__init__.py +45 -38
- investing_algorithm_framework/domain/backtesting/__init__.py +21 -0
- investing_algorithm_framework/domain/backtesting/backtest.py +503 -0
- investing_algorithm_framework/domain/backtesting/backtest_date_range.py +96 -0
- investing_algorithm_framework/domain/backtesting/backtest_evaluation_focuss.py +242 -0
- investing_algorithm_framework/domain/backtesting/backtest_metrics.py +459 -0
- investing_algorithm_framework/domain/backtesting/backtest_permutation_test.py +275 -0
- investing_algorithm_framework/domain/backtesting/backtest_run.py +605 -0
- investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py +162 -0
- investing_algorithm_framework/domain/backtesting/combine_backtests.py +280 -0
- investing_algorithm_framework/domain/config.py +27 -0
- investing_algorithm_framework/domain/constants.py +6 -34
- investing_algorithm_framework/domain/data_provider.py +200 -56
- investing_algorithm_framework/domain/exceptions.py +34 -1
- investing_algorithm_framework/domain/models/__init__.py +10 -19
- investing_algorithm_framework/domain/models/base_model.py +0 -6
- investing_algorithm_framework/domain/models/data/__init__.py +7 -0
- investing_algorithm_framework/domain/models/data/data_source.py +214 -0
- investing_algorithm_framework/domain/models/{market_data_type.py → data/data_type.py} +7 -7
- investing_algorithm_framework/domain/models/market/market_credential.py +6 -0
- investing_algorithm_framework/domain/models/order/order.py +34 -13
- investing_algorithm_framework/domain/models/order/order_status.py +1 -1
- investing_algorithm_framework/domain/models/order/order_type.py +1 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio.py +14 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_configuration.py +5 -1
- investing_algorithm_framework/domain/models/portfolio/portfolio_snapshot.py +51 -11
- investing_algorithm_framework/domain/models/position/__init__.py +2 -1
- investing_algorithm_framework/domain/models/position/position.py +9 -0
- investing_algorithm_framework/domain/models/position/position_size.py +41 -0
- investing_algorithm_framework/domain/models/risk_rules/__init__.py +7 -0
- investing_algorithm_framework/domain/models/risk_rules/stop_loss_rule.py +51 -0
- investing_algorithm_framework/domain/models/risk_rules/take_profit_rule.py +55 -0
- investing_algorithm_framework/domain/models/snapshot_interval.py +0 -1
- investing_algorithm_framework/domain/models/strategy_profile.py +19 -151
- investing_algorithm_framework/domain/models/time_frame.py +7 -0
- investing_algorithm_framework/domain/models/time_interval.py +33 -0
- investing_algorithm_framework/domain/models/time_unit.py +63 -1
- investing_algorithm_framework/domain/models/trade/__init__.py +0 -2
- investing_algorithm_framework/domain/models/trade/trade.py +56 -32
- investing_algorithm_framework/domain/models/trade/trade_status.py +8 -2
- investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +106 -41
- investing_algorithm_framework/domain/models/trade/trade_take_profit.py +161 -99
- investing_algorithm_framework/domain/order_executor.py +19 -0
- investing_algorithm_framework/domain/portfolio_provider.py +20 -1
- investing_algorithm_framework/domain/services/__init__.py +0 -13
- investing_algorithm_framework/domain/strategy.py +1 -29
- investing_algorithm_framework/domain/utils/__init__.py +5 -1
- investing_algorithm_framework/domain/utils/custom_tqdm.py +22 -0
- investing_algorithm_framework/domain/utils/jupyter_notebook_detection.py +19 -0
- investing_algorithm_framework/domain/utils/polars.py +17 -14
- investing_algorithm_framework/download_data.py +40 -10
- investing_algorithm_framework/infrastructure/__init__.py +13 -25
- investing_algorithm_framework/infrastructure/data_providers/__init__.py +7 -4
- investing_algorithm_framework/infrastructure/data_providers/ccxt.py +811 -546
- investing_algorithm_framework/infrastructure/data_providers/csv.py +433 -122
- investing_algorithm_framework/infrastructure/data_providers/pandas.py +599 -0
- investing_algorithm_framework/infrastructure/database/__init__.py +6 -2
- investing_algorithm_framework/infrastructure/database/sql_alchemy.py +81 -0
- investing_algorithm_framework/infrastructure/models/__init__.py +0 -13
- investing_algorithm_framework/infrastructure/models/order/order.py +9 -3
- investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +27 -8
- investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +21 -7
- investing_algorithm_framework/infrastructure/order_executors/__init__.py +2 -0
- investing_algorithm_framework/infrastructure/order_executors/backtest_oder_executor.py +28 -0
- investing_algorithm_framework/infrastructure/repositories/repository.py +16 -2
- investing_algorithm_framework/infrastructure/repositories/trade_repository.py +2 -2
- investing_algorithm_framework/infrastructure/repositories/trade_stop_loss_repository.py +6 -0
- investing_algorithm_framework/infrastructure/repositories/trade_take_profit_repository.py +6 -0
- investing_algorithm_framework/infrastructure/services/__init__.py +0 -4
- investing_algorithm_framework/services/__init__.py +105 -8
- investing_algorithm_framework/services/backtesting/backtest_service.py +536 -476
- investing_algorithm_framework/services/configuration_service.py +14 -4
- investing_algorithm_framework/services/data_providers/__init__.py +5 -0
- investing_algorithm_framework/services/data_providers/data_provider_service.py +850 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/__init__.py +48 -17
- investing_algorithm_framework/{app/reporting → services}/metrics/drawdown.py +10 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/equity_curve.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/exposure.py +60 -2
- investing_algorithm_framework/services/metrics/generate.py +358 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/profit_factor.py +36 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/recovery.py +2 -2
- investing_algorithm_framework/{app/reporting → services}/metrics/returns.py +146 -147
- investing_algorithm_framework/services/metrics/risk_free_rate.py +28 -0
- investing_algorithm_framework/{app/reporting/metrics/sharp_ratio.py → services/metrics/sharpe_ratio.py} +6 -10
- investing_algorithm_framework/{app/reporting → services}/metrics/sortino_ratio.py +3 -7
- investing_algorithm_framework/services/metrics/trades.py +500 -0
- investing_algorithm_framework/services/metrics/volatility.py +97 -0
- investing_algorithm_framework/{app/reporting → services}/metrics/win_rate.py +70 -3
- investing_algorithm_framework/services/order_service/order_backtest_service.py +21 -31
- investing_algorithm_framework/services/order_service/order_service.py +9 -71
- investing_algorithm_framework/services/portfolios/portfolio_provider_lookup.py +0 -2
- investing_algorithm_framework/services/portfolios/portfolio_service.py +3 -13
- investing_algorithm_framework/services/portfolios/portfolio_snapshot_service.py +62 -96
- investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +0 -3
- investing_algorithm_framework/services/repository_service.py +5 -2
- investing_algorithm_framework/services/trade_order_evaluator/__init__.py +9 -0
- investing_algorithm_framework/services/trade_order_evaluator/backtest_trade_oder_evaluator.py +113 -0
- investing_algorithm_framework/services/trade_order_evaluator/default_trade_order_evaluator.py +51 -0
- investing_algorithm_framework/services/trade_order_evaluator/trade_order_evaluator.py +80 -0
- investing_algorithm_framework/services/trade_service/__init__.py +7 -1
- investing_algorithm_framework/services/trade_service/trade_service.py +51 -29
- investing_algorithm_framework/services/trade_service/trade_stop_loss_service.py +39 -0
- investing_algorithm_framework/services/trade_service/trade_take_profit_service.py +41 -0
- investing_algorithm_framework-7.19.15.dist-info/METADATA +537 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/RECORD +159 -148
- investing_algorithm_framework/app/reporting/evaluation.py +0 -243
- investing_algorithm_framework/app/reporting/metrics/risk_free_rate.py +0 -8
- investing_algorithm_framework/app/reporting/metrics/volatility.py +0 -69
- investing_algorithm_framework/cli/templates/requirements_azure_function.txt.template +0 -3
- investing_algorithm_framework/domain/models/backtesting/__init__.py +0 -9
- investing_algorithm_framework/domain/models/backtesting/backtest_date_range.py +0 -47
- investing_algorithm_framework/domain/models/backtesting/backtest_position.py +0 -120
- investing_algorithm_framework/domain/models/backtesting/backtest_reports_evaluation.py +0 -0
- investing_algorithm_framework/domain/models/backtesting/backtest_results.py +0 -440
- investing_algorithm_framework/domain/models/data_source.py +0 -21
- investing_algorithm_framework/domain/models/date_range.py +0 -64
- investing_algorithm_framework/domain/models/trade/trade_risk_type.py +0 -34
- investing_algorithm_framework/domain/models/trading_data_types.py +0 -48
- investing_algorithm_framework/domain/models/trading_time_frame.py +0 -223
- investing_algorithm_framework/domain/services/market_data_sources.py +0 -543
- investing_algorithm_framework/domain/services/market_service.py +0 -153
- investing_algorithm_framework/domain/services/observable.py +0 -51
- investing_algorithm_framework/domain/services/observer.py +0 -19
- investing_algorithm_framework/infrastructure/models/market_data_sources/__init__.py +0 -16
- investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +0 -746
- investing_algorithm_framework/infrastructure/models/market_data_sources/csv.py +0 -270
- investing_algorithm_framework/infrastructure/models/market_data_sources/pandas.py +0 -312
- investing_algorithm_framework/infrastructure/services/market_service/__init__.py +0 -5
- investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +0 -471
- investing_algorithm_framework/infrastructure/services/performance_service/__init__.py +0 -7
- investing_algorithm_framework/infrastructure/services/performance_service/backtest_performance_service.py +0 -2
- investing_algorithm_framework/infrastructure/services/performance_service/performance_service.py +0 -322
- investing_algorithm_framework/services/market_data_source_service/__init__.py +0 -10
- investing_algorithm_framework/services/market_data_source_service/backtest_market_data_source_service.py +0 -269
- investing_algorithm_framework/services/market_data_source_service/data_provider_service.py +0 -350
- investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +0 -377
- investing_algorithm_framework/services/strategy_orchestrator_service.py +0 -296
- investing_algorithm_framework-6.9.1.dist-info/METADATA +0 -440
- /investing_algorithm_framework/{app/reporting → services}/metrics/alpha.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/beta.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/cagr.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/calmar_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/mean_daily_return.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/price_efficiency.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/standard_deviation.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/treynor_ratio.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/ulcer.py +0 -0
- /investing_algorithm_framework/{app/reporting → services}/metrics/value_at_risk.py +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/LICENSE +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/WHEEL +0 -0
- {investing_algorithm_framework-6.9.1.dist-info → investing_algorithm_framework-7.19.15.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
4
|
+
class DataType(Enum):
|
|
5
5
|
OHLCV = "OHLCV"
|
|
6
6
|
TICKER = "TICKER"
|
|
7
7
|
ORDER_BOOK = "ORDER_BOOK"
|
|
@@ -12,24 +12,24 @@ class MarketDataType(Enum):
|
|
|
12
12
|
|
|
13
13
|
if isinstance(value, str):
|
|
14
14
|
|
|
15
|
-
for entry in
|
|
15
|
+
for entry in DataType:
|
|
16
16
|
|
|
17
17
|
if value.upper() == entry.value:
|
|
18
18
|
return entry
|
|
19
19
|
|
|
20
20
|
raise ValueError(
|
|
21
|
-
f"Could not convert {value} to
|
|
21
|
+
f"Could not convert {value} to DataType"
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
@staticmethod
|
|
25
25
|
def from_value(value):
|
|
26
26
|
|
|
27
27
|
if isinstance(value, str):
|
|
28
|
-
return
|
|
28
|
+
return DataType.from_string(value)
|
|
29
29
|
|
|
30
|
-
if isinstance(value,
|
|
30
|
+
if isinstance(value, DataType):
|
|
31
31
|
|
|
32
|
-
for entry in
|
|
32
|
+
for entry in DataType:
|
|
33
33
|
|
|
34
34
|
if value == entry:
|
|
35
35
|
return entry
|
|
@@ -43,4 +43,4 @@ class MarketDataType(Enum):
|
|
|
43
43
|
if isinstance(other, Enum):
|
|
44
44
|
return self.value == other.value
|
|
45
45
|
else:
|
|
46
|
-
return
|
|
46
|
+
return DataType.from_string(other) == self
|
|
@@ -52,6 +52,12 @@ class MarketCredential:
|
|
|
52
52
|
environment_variable = f"{self.market.upper()}_SECRET_KEY"
|
|
53
53
|
self._secret_key = os.getenv(environment_variable)
|
|
54
54
|
|
|
55
|
+
if self.api_key is not None:
|
|
56
|
+
self._api_key = self.api_key.strip()
|
|
57
|
+
|
|
58
|
+
if self.secret_key is not None:
|
|
59
|
+
self._secret_key = self.secret_key.strip()
|
|
60
|
+
|
|
55
61
|
def get_api_key(self):
|
|
56
62
|
return self.api_key
|
|
57
63
|
|
|
@@ -25,7 +25,7 @@ class Order(BaseModel):
|
|
|
25
25
|
order_type,
|
|
26
26
|
order_side,
|
|
27
27
|
amount,
|
|
28
|
-
status=OrderStatus.CREATED.value,
|
|
28
|
+
status: OrderStatus | None | str = OrderStatus.CREATED.value,
|
|
29
29
|
target_symbol=None,
|
|
30
30
|
trading_symbol=None,
|
|
31
31
|
price=None,
|
|
@@ -40,7 +40,8 @@ class Order(BaseModel):
|
|
|
40
40
|
order_fee=None,
|
|
41
41
|
order_fee_currency=None,
|
|
42
42
|
order_fee_rate=None,
|
|
43
|
-
id=None
|
|
43
|
+
id=None,
|
|
44
|
+
metadata=None,
|
|
44
45
|
):
|
|
45
46
|
if target_symbol is None:
|
|
46
47
|
raise OperationalException("Target symbol is not specified")
|
|
@@ -85,6 +86,7 @@ class Order(BaseModel):
|
|
|
85
86
|
self.order_fee_rate = order_fee_rate
|
|
86
87
|
self.id = id
|
|
87
88
|
self.cost = cost
|
|
89
|
+
self.metadata = metadata if metadata is not None else {}
|
|
88
90
|
|
|
89
91
|
def get_id(self):
|
|
90
92
|
return self.id
|
|
@@ -191,6 +193,11 @@ class Order(BaseModel):
|
|
|
191
193
|
return (self.get_target_symbol().upper() + "/"
|
|
192
194
|
+ self.get_trading_symbol().upper())
|
|
193
195
|
|
|
196
|
+
@property
|
|
197
|
+
def symbol(self):
|
|
198
|
+
return (self.get_target_symbol().upper() + "/"
|
|
199
|
+
+ self.get_trading_symbol().upper())
|
|
200
|
+
|
|
194
201
|
def get_available_amount(self):
|
|
195
202
|
|
|
196
203
|
if self._available_amount is None:
|
|
@@ -222,14 +229,18 @@ class Order(BaseModel):
|
|
|
222
229
|
dict: A dictionary representation of the Order object.
|
|
223
230
|
"""
|
|
224
231
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
def ensure_iso(value):
|
|
233
|
+
if hasattr(value, "isoformat"):
|
|
234
|
+
if value.tzinfo is None:
|
|
235
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
236
|
+
return value.isoformat()
|
|
237
|
+
return value
|
|
238
|
+
|
|
239
|
+
created_at = ensure_iso(self.created_at) if self.created_at else None
|
|
240
|
+
updated_at = ensure_iso(self.updated_at) if self.updated_at else None
|
|
241
|
+
|
|
242
|
+
# Ensure status is a string
|
|
243
|
+
self.status = OrderStatus.from_value(self.status).value
|
|
233
244
|
|
|
234
245
|
return {
|
|
235
246
|
"id": self.id,
|
|
@@ -249,6 +260,7 @@ class Order(BaseModel):
|
|
|
249
260
|
"order_fee_currency": self.order_fee_currency,
|
|
250
261
|
"order_fee_rate": self.order_fee_rate,
|
|
251
262
|
"order_fee": self.order_fee,
|
|
263
|
+
"metadata": self.metadata if hasattr(self, 'metadata') else {},
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
@staticmethod
|
|
@@ -271,7 +283,8 @@ class Order(BaseModel):
|
|
|
271
283
|
updated_at = parse(updated_at)
|
|
272
284
|
|
|
273
285
|
order = Order(
|
|
274
|
-
|
|
286
|
+
id=data.get("id", None),
|
|
287
|
+
external_id=data.get("external_id", None),
|
|
275
288
|
target_symbol=target_symbol,
|
|
276
289
|
trading_symbol=trading_symbol,
|
|
277
290
|
price=data.get("price", None),
|
|
@@ -282,12 +295,13 @@ class Order(BaseModel):
|
|
|
282
295
|
filled=data.get("filled", None),
|
|
283
296
|
remaining=data.get("remaining", None),
|
|
284
297
|
fee=data.get("fee", None),
|
|
298
|
+
cost=data.get("cost", None),
|
|
285
299
|
created_at=created_at,
|
|
286
300
|
updated_at=updated_at,
|
|
287
301
|
order_fee=data.get("order_fee", None),
|
|
288
302
|
order_fee_currency=data.get("order_fee_currency", None),
|
|
289
303
|
order_fee_rate=data.get("order_fee_rate", None),
|
|
290
|
-
|
|
304
|
+
metadata=data.get("metadata", {}),
|
|
291
305
|
)
|
|
292
306
|
return order
|
|
293
307
|
|
|
@@ -311,6 +325,13 @@ class Order(BaseModel):
|
|
|
311
325
|
order_fee_currency = ccxt_fee.get("currency", None)
|
|
312
326
|
order_fee_rate = ccxt_fee.get("rate", None)
|
|
313
327
|
|
|
328
|
+
created_at = ccxt_order.get("datetime", None)
|
|
329
|
+
created_at = parse(created_at) if created_at else None
|
|
330
|
+
|
|
331
|
+
# Make sure that the created_at is a utc datetime object
|
|
332
|
+
if created_at and created_at.tzinfo is None:
|
|
333
|
+
created_at = created_at.replace(tzinfo=timezone.utc)
|
|
334
|
+
|
|
314
335
|
return Order(
|
|
315
336
|
external_id=ccxt_order.get("id", None),
|
|
316
337
|
target_symbol=target_symbol,
|
|
@@ -326,7 +347,7 @@ class Order(BaseModel):
|
|
|
326
347
|
order_fee=order_fee,
|
|
327
348
|
order_fee_currency=order_fee_currency,
|
|
328
349
|
order_fee_rate=order_fee_rate,
|
|
329
|
-
created_at=
|
|
350
|
+
created_at=created_at
|
|
330
351
|
)
|
|
331
352
|
|
|
332
353
|
def __repr__(self):
|
|
@@ -31,7 +31,7 @@ class OrderStatus(Enum):
|
|
|
31
31
|
elif isinstance(value, str):
|
|
32
32
|
return OrderStatus.from_string(value)
|
|
33
33
|
|
|
34
|
-
raise ValueError("Could not convert value to OrderStatus")
|
|
34
|
+
raise ValueError(f"Could not convert value: {value} to OrderStatus")
|
|
35
35
|
|
|
36
36
|
def equals(self, other):
|
|
37
37
|
return OrderStatus.from_value(other) == self
|
|
@@ -13,7 +13,7 @@ class OrderType(Enum):
|
|
|
13
13
|
if value.upper() == order_type.value:
|
|
14
14
|
return order_type
|
|
15
15
|
|
|
16
|
-
raise ValueError("Could not convert value to OrderType")
|
|
16
|
+
raise ValueError(f"Could not convert value: {value} to OrderType")
|
|
17
17
|
|
|
18
18
|
@staticmethod
|
|
19
19
|
def from_value(value):
|
|
@@ -40,6 +40,7 @@ class Portfolio(BaseModel):
|
|
|
40
40
|
trading_symbol,
|
|
41
41
|
net_size,
|
|
42
42
|
unallocated,
|
|
43
|
+
initial_balance,
|
|
43
44
|
market,
|
|
44
45
|
realized=0,
|
|
45
46
|
total_revenue=0,
|
|
@@ -49,7 +50,6 @@ class Portfolio(BaseModel):
|
|
|
49
50
|
created_at=None,
|
|
50
51
|
updated_at=None,
|
|
51
52
|
initialized=False,
|
|
52
|
-
initial_balance=None
|
|
53
53
|
):
|
|
54
54
|
self.identifier = identifier
|
|
55
55
|
self.updated_at = None
|
|
@@ -66,6 +66,7 @@ class Portfolio(BaseModel):
|
|
|
66
66
|
self.created_at = created_at
|
|
67
67
|
self.updated_at = updated_at
|
|
68
68
|
self.initialized = initialized
|
|
69
|
+
self._allocated = None
|
|
69
70
|
|
|
70
71
|
def __repr__(self):
|
|
71
72
|
return self.repr(
|
|
@@ -119,6 +120,18 @@ class Portfolio(BaseModel):
|
|
|
119
120
|
def get_initial_balance(self):
|
|
120
121
|
return self.initial_balance
|
|
121
122
|
|
|
123
|
+
@property
|
|
124
|
+
def allocated(self):
|
|
125
|
+
|
|
126
|
+
if self._allocated is None:
|
|
127
|
+
return 0.0
|
|
128
|
+
|
|
129
|
+
return self._allocated
|
|
130
|
+
|
|
131
|
+
@allocated.setter
|
|
132
|
+
def allocated(self, value):
|
|
133
|
+
self._allocated = value
|
|
134
|
+
|
|
122
135
|
@staticmethod
|
|
123
136
|
def from_portfolio_configuration(portfolio_configuration):
|
|
124
137
|
"""
|
|
@@ -10,7 +10,7 @@ class PortfolioConfiguration(BaseModel):
|
|
|
10
10
|
This class represents a portfolio configuration. It is used to
|
|
11
11
|
configure the portfolio that the user wants to create.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Attributes:
|
|
14
14
|
- market: The market where the portfolio will be created
|
|
15
15
|
- trading_symbol: The trading symbol of the portfolio
|
|
16
16
|
- track_from: The date from which the portfolio will be tracked
|
|
@@ -33,6 +33,10 @@ class PortfolioConfiguration(BaseModel):
|
|
|
33
33
|
initial_balance=None,
|
|
34
34
|
):
|
|
35
35
|
self._market = market
|
|
36
|
+
|
|
37
|
+
if self._market is not None:
|
|
38
|
+
self._market = self._market.upper()
|
|
39
|
+
|
|
36
40
|
self._track_from = None
|
|
37
41
|
self._trading_symbol = trading_symbol.upper()
|
|
38
42
|
self._identifier = identifier
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from datetime import timezone
|
|
2
|
+
|
|
1
3
|
from dateutil import parser
|
|
4
|
+
|
|
2
5
|
from investing_algorithm_framework.domain.models.base_model import BaseModel
|
|
3
6
|
|
|
4
7
|
|
|
@@ -17,7 +20,8 @@ class PortfolioSnapshot(BaseModel):
|
|
|
17
20
|
total_value=None,
|
|
18
21
|
cash_flow=None,
|
|
19
22
|
created_at=None,
|
|
20
|
-
position_snapshots=None
|
|
23
|
+
position_snapshots=None,
|
|
24
|
+
metadata=None,
|
|
21
25
|
):
|
|
22
26
|
self.portfolio_id = portfolio_id
|
|
23
27
|
self.trading_symbol = trading_symbol
|
|
@@ -29,7 +33,15 @@ class PortfolioSnapshot(BaseModel):
|
|
|
29
33
|
self.net_size = net_size
|
|
30
34
|
self.total_cost = total_cost
|
|
31
35
|
self.cash_flow = cash_flow
|
|
32
|
-
self.
|
|
36
|
+
self.metadata = metadata if metadata is not None else {}
|
|
37
|
+
|
|
38
|
+
if created_at is not None and isinstance(created_at, str):
|
|
39
|
+
self.created_at = parser.parse(created_at)
|
|
40
|
+
else:
|
|
41
|
+
self.created_at = created_at
|
|
42
|
+
|
|
43
|
+
# Make sure that created_at is a timezone aware datetime object
|
|
44
|
+
self.created_at = self.created_at.replace(tzinfo=timezone.utc)
|
|
33
45
|
|
|
34
46
|
if position_snapshots is None:
|
|
35
47
|
position_snapshots = []
|
|
@@ -120,6 +132,7 @@ class PortfolioSnapshot(BaseModel):
|
|
|
120
132
|
total_revenue=self.total_revenue,
|
|
121
133
|
total_cost=self.total_cost,
|
|
122
134
|
cash_flow=self.cash_flow,
|
|
135
|
+
metadata=self.metadata,
|
|
123
136
|
)
|
|
124
137
|
|
|
125
138
|
def to_dict(self, datetime_format=None):
|
|
@@ -134,18 +147,30 @@ class PortfolioSnapshot(BaseModel):
|
|
|
134
147
|
Returns:
|
|
135
148
|
dict: A dictionary representation of the portfolio snapshot object.
|
|
136
149
|
"""
|
|
150
|
+
def ensure_iso(value):
|
|
151
|
+
if hasattr(value, "isoformat"):
|
|
152
|
+
if value.tzinfo is None:
|
|
153
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
154
|
+
return value.isoformat()
|
|
155
|
+
return value
|
|
137
156
|
|
|
138
|
-
if
|
|
139
|
-
created_at = self.created_at.strftime(datetime_format) \
|
|
140
|
-
if self.created_at else None
|
|
141
|
-
|
|
142
|
-
else:
|
|
143
|
-
created_at = self.created_at
|
|
157
|
+
created_at = ensure_iso(self.created_at) if self.created_at else None
|
|
144
158
|
|
|
145
159
|
return {
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
160
|
+
"metadata": self.metadata if self.metadata else {},
|
|
161
|
+
"portfolio_id": self.portfolio_id if self.portfolio_id else "",
|
|
162
|
+
"trading_symbol": self.trading_symbol
|
|
163
|
+
if self.trading_symbol else "",
|
|
164
|
+
"pending_value": self.pending_value if self.pending_value else 0.0,
|
|
165
|
+
"unallocated": self.unallocated if self.unallocated else 0.0,
|
|
166
|
+
"total_net_gain": self.total_net_gain
|
|
167
|
+
if self.total_net_gain else 0.0,
|
|
168
|
+
"total_revenue": self.total_revenue if self.total_revenue else 0.0,
|
|
169
|
+
"total_cost": self.total_cost if self.total_cost else 0.0,
|
|
170
|
+
"cash_flow": self.cash_flow if self.cash_flow else 0.0,
|
|
171
|
+
"net_size": self.net_size if self.net_size else 0.0,
|
|
172
|
+
"created_at": created_at if created_at else "",
|
|
173
|
+
"total_value": self.total_value if self.total_value else 0.0,
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
@staticmethod
|
|
@@ -161,8 +186,23 @@ class PortfolioSnapshot(BaseModel):
|
|
|
161
186
|
"""
|
|
162
187
|
created_at_str = data.get("created_at")
|
|
163
188
|
created_at = parser.parse(created_at_str)
|
|
189
|
+
|
|
190
|
+
# Ensure created_at is timezone aware
|
|
191
|
+
created_at = created_at.replace(tzinfo=timezone.utc)
|
|
192
|
+
|
|
164
193
|
return PortfolioSnapshot(
|
|
165
194
|
net_size=data.get("net_size", 0.0),
|
|
166
195
|
created_at=created_at,
|
|
167
196
|
total_value=data.get("total_value", 0.0),
|
|
197
|
+
trading_symbol=data.get(
|
|
198
|
+
"trading_symbol", None
|
|
199
|
+
),
|
|
200
|
+
portfolio_id=data.get("portfolio_id", None),
|
|
201
|
+
pending_value=data.get("pending_value", 0.0),
|
|
202
|
+
unallocated=data.get("unallocated", 0.0),
|
|
203
|
+
total_net_gain=data.get("total_net_gain", 0.0),
|
|
204
|
+
total_revenue=data.get("total_revenue", 0.0),
|
|
205
|
+
total_cost=data.get("total_cost", 0.0),
|
|
206
|
+
cash_flow=data.get("cash_flow", 0.0),
|
|
207
|
+
metadata=data.get("metadata", {})
|
|
168
208
|
)
|
|
@@ -50,6 +50,15 @@ class Position(BaseModel):
|
|
|
50
50
|
"portfolio_id": self.portfolio_id,
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
@staticmethod
|
|
54
|
+
def from_dict(data: dict):
|
|
55
|
+
return Position(
|
|
56
|
+
symbol=data.get("symbol"),
|
|
57
|
+
amount=data.get("amount", 0),
|
|
58
|
+
cost=data.get("cost", 0),
|
|
59
|
+
portfolio_id=data.get("portfolio_id"),
|
|
60
|
+
)
|
|
61
|
+
|
|
53
62
|
def __repr__(self):
|
|
54
63
|
return self.repr(
|
|
55
64
|
symbol=self.symbol,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from investing_algorithm_framework.domain.exceptions import \
|
|
3
|
+
OperationalException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PositionSize:
|
|
7
|
+
"""
|
|
8
|
+
Defines how much capital to allocate to a specific symbol.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
symbol: str,
|
|
14
|
+
percentage_of_portfolio: Optional[float] = None,
|
|
15
|
+
fixed_amount: Optional[float] = None
|
|
16
|
+
):
|
|
17
|
+
self.symbol = symbol
|
|
18
|
+
self.percentage_of_portfolio = percentage_of_portfolio
|
|
19
|
+
self.fixed_amount = fixed_amount
|
|
20
|
+
|
|
21
|
+
def get_size(self, portfolio, asset_price) -> float:
|
|
22
|
+
"""
|
|
23
|
+
Calculate size in currency/units.
|
|
24
|
+
"""
|
|
25
|
+
if self.fixed_amount is not None:
|
|
26
|
+
return self.fixed_amount
|
|
27
|
+
elif self.percentage_of_portfolio is not None:
|
|
28
|
+
total_value = portfolio.get_unallocated() + portfolio.allocated
|
|
29
|
+
return total_value * (self.percentage_of_portfolio / 100)
|
|
30
|
+
else:
|
|
31
|
+
raise OperationalException(
|
|
32
|
+
"A position size object must have either a fixed amount or a "
|
|
33
|
+
"percentage of the portfolio defined."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
return (
|
|
38
|
+
f"PositionSize(symbol={self.symbol}, "
|
|
39
|
+
f"percentage_of_portfolio={self.percentage_of_portfolio}, "
|
|
40
|
+
f"fixed_amount={self.fixed_amount})"
|
|
41
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class StopLossRule:
|
|
2
|
+
"""
|
|
3
|
+
A rule that defines when to trigger a stop loss based
|
|
4
|
+
on a specified threshold such as a percentage drop in price or
|
|
5
|
+
a fixed amount.
|
|
6
|
+
|
|
7
|
+
if trade_risk_type is fixed, the stop loss price is calculated as follows:
|
|
8
|
+
You buy an asset at $100.
|
|
9
|
+
You set a 5% stop loss, meaning you will sell if
|
|
10
|
+
the price drops to $95.
|
|
11
|
+
If the price rises to $120, the stop loss is not triggered.
|
|
12
|
+
But if the price keeps falling to $95, the stop loss triggers,
|
|
13
|
+
and you exit with a $5 loss.
|
|
14
|
+
|
|
15
|
+
if trade_risk_type is trailing, the stop loss price is
|
|
16
|
+
calculated as follows:
|
|
17
|
+
You buy an asset at $100.
|
|
18
|
+
You set a 5% trailing stop loss, meaning you will sell if
|
|
19
|
+
the price drops 5% from its peak at $96
|
|
20
|
+
If the price rises to $120, the stop loss adjusts
|
|
21
|
+
to $114 (5% below $120).
|
|
22
|
+
If the price falls to $114, the position is
|
|
23
|
+
closed, securing a $14 profit.
|
|
24
|
+
But if the price keeps rising to $150, the stop
|
|
25
|
+
loss moves up to $142.50.
|
|
26
|
+
If the price drops from $150 to $142.50, the stop
|
|
27
|
+
loss triggers, and you exit with a $42.50 profit.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
- percentage_threshold (float): The percentage drop in price
|
|
31
|
+
that triggers the stop loss.
|
|
32
|
+
- trailing (bool): Indicates whether the stop loss is trailing
|
|
33
|
+
or fixed.
|
|
34
|
+
- sell_percentage (float): The percentage of the position to sell
|
|
35
|
+
when the stop loss is triggered.
|
|
36
|
+
- symbol (str): The symbol of the asset the stop loss rule
|
|
37
|
+
applies to. Symbol is defined as the target symbol
|
|
38
|
+
(the asset being traded) combined with the trading symbol
|
|
39
|
+
(the asset used to trade the target symbol), e.g., 'BTC-EUR'.
|
|
40
|
+
"""
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
percentage_threshold: float,
|
|
44
|
+
sell_percentage: float,
|
|
45
|
+
symbol: str,
|
|
46
|
+
trailing: bool = False,
|
|
47
|
+
):
|
|
48
|
+
self.percentage_threshold = percentage_threshold
|
|
49
|
+
self.trailing = trailing
|
|
50
|
+
self.sell_percentage = sell_percentage
|
|
51
|
+
self.symbol = symbol
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class TakeProfitRule:
|
|
2
|
+
"""
|
|
3
|
+
A rule that defines when to trigger a take profit based
|
|
4
|
+
on a specified threshold such as a percentage gain in price or
|
|
5
|
+
a fixed amount.
|
|
6
|
+
|
|
7
|
+
if trailing is set to true, the take profit price is
|
|
8
|
+
calculated as follows:
|
|
9
|
+
You buy an asset at $100.
|
|
10
|
+
You set a 5% take profit, meaning you will sell if the price
|
|
11
|
+
rises to $105.
|
|
12
|
+
If the price rises to $120, the take profit triggers,
|
|
13
|
+
and you exit with a $20 profit.
|
|
14
|
+
But if the price keeps falling below $105, the take profit is not
|
|
15
|
+
triggered.
|
|
16
|
+
|
|
17
|
+
if trailing is set to true, the take profit price is
|
|
18
|
+
calculated as follows:
|
|
19
|
+
You buy an asset at $100.
|
|
20
|
+
You set a 5% trailing take profit, the moment the price rises
|
|
21
|
+
5% the initial take profit mark will be set. This means you
|
|
22
|
+
will set the take_profit_price initially at none and
|
|
23
|
+
only if the price hits $105, you will set the
|
|
24
|
+
take_profit_price to $105.
|
|
25
|
+
if the price drops below $105, the take profit is triggered.
|
|
26
|
+
If the price rises to $120, the take profit adjusts to
|
|
27
|
+
$114 (5% below $120).
|
|
28
|
+
If the price falls to $114, the position is closed,
|
|
29
|
+
securing a $14 profit.
|
|
30
|
+
But if the price keeps rising to $150, the take profit
|
|
31
|
+
moves up to $142.50.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
- percentage_threshold (float): The percentage gain in price
|
|
36
|
+
that triggers the stop loss.
|
|
37
|
+
- trailing (bool): Indicates whether the take profit is trailing
|
|
38
|
+
or fixed.
|
|
39
|
+
- sell_percentage (float): The percentage of the position to sell
|
|
40
|
+
when the take profit is triggered.
|
|
41
|
+
- symbol (str): The symbol of the asset the take profit rule
|
|
42
|
+
applies to. Symbol is defined as only the target symbol.
|
|
43
|
+
So for example, 'BTC' in 'BTC-EUR' or 'META' in 'META-USD'.
|
|
44
|
+
"""
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
percentage_threshold: float,
|
|
48
|
+
sell_percentage: float,
|
|
49
|
+
symbol: str,
|
|
50
|
+
trailing: bool = False,
|
|
51
|
+
):
|
|
52
|
+
self.percentage_threshold = percentage_threshold
|
|
53
|
+
self.trailing = trailing
|
|
54
|
+
self.sell_percentage = sell_percentage
|
|
55
|
+
self.symbol = symbol
|