Skip to content

Commit 3d5e8f9

Browse files
committed
Fix app hook registration
1 parent 3dddec4 commit 3d5e8f9

File tree

6 files changed

+222
-18
lines changed

6 files changed

+222
-18
lines changed

investing_algorithm_framework/app/app.py

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,51 @@
3434

3535

3636
class AppHook:
37+
"""
38+
Abstract class for app hooks. App hooks are used to run code before
39+
actions of the framework are executed. This is useful for running code
40+
that needs to be run before the following events:
41+
- App initialization
42+
- Strategy run
43+
"""
3744

3845
@abstractmethod
39-
def on_run(self, app, algorithm: Algorithm):
46+
def on_run(self, context) -> None:
47+
"""
48+
Method to run the app hook. This method should be implemented
49+
by the user. This method will be called when the app is performing
50+
a specific action.
51+
52+
Args:
53+
context: The context of the app. This can be used to get the
54+
current state of the trading bot, such as portfolios,
55+
orders, positions, etc.
56+
57+
Returns:
58+
None
59+
"""
4060
raise NotImplementedError()
4161

4262

4363
class App:
64+
"""
65+
Class to represent the app. This class is used to initialize the
66+
application and run your trading bot.
67+
68+
Attributes:
69+
- container: The dependency container for the app. This is used
70+
to store all the services and repositories for the app.
71+
- algorithm: The algorithm to run. This is used to run the
72+
trading bot.
73+
- flask_app: The flask app instance. This is used to run the
74+
web app.
75+
- state_handler: The state handler for the app. This is used
76+
to save and load the state of the app.
77+
- name: The name of the app. This is used to identify the app
78+
in logs and other places.
79+
- started: A boolean value that indicates if the app has been
80+
started or not.
81+
"""
4482

4583
def __init__(self, state_handler=None, name=None):
4684
self._flask_app: Optional[Flask] = None
@@ -54,6 +92,7 @@ def __init__(self, state_handler=None, name=None):
5492
self._market_credential_service: \
5593
Optional[MarketCredentialService] = None
5694
self._on_initialize_hooks = []
95+
self._on_strategy_run_hooks = []
5796
self._on_after_initialize_hooks = []
5897
self._state_handler = state_handler
5998
self._name = name
@@ -114,13 +153,25 @@ def set_config_with_dict(self, dictionary) -> None:
114153
configuration_service.add_dict(dictionary)
115154

116155
def initialize_services(self) -> None:
117-
self._configuration_service = self.container.configuration_service()
118-
self._configuration_service.initialize()
156+
"""
157+
Method to initialize the services for the app. This method should
158+
be called before running the application. This method initializes
159+
all services so that they are ready to be used.
160+
161+
Returns:
162+
None
163+
"""
119164
self._market_data_source_service = \
120165
self.container.market_data_source_service()
121166
self._market_credential_service = \
122167
self.container.market_credential_service()
123168

169+
strategy_orchestrator_service = \
170+
self.container.strategy_orchestrator_service()
171+
172+
for app_hook in self._on_strategy_run_hooks:
173+
strategy_orchestrator_service.add_app_hook(app_hook)
174+
124175
@algorithm.setter
125176
def algorithm(self, algorithm: Algorithm) -> None:
126177
self._algorithm = algorithm
@@ -212,6 +263,7 @@ def initialize(self):
212263
None
213264
"""
214265
logger.info("Initializing app")
266+
self.initialize_services()
215267
self._initialize_default_order_executors()
216268
self._initialize_default_portfolio_providers()
217269

@@ -418,7 +470,7 @@ def run(
418470

419471
# Run all on_initialize hooks
420472
for hook in self._on_initialize_hooks:
421-
hook.on_run(self, self.algorithm)
473+
hook.on_run(self.context)
422474

423475
configuration_service = self.container.configuration_service()
424476
config = configuration_service.get_config()
@@ -580,7 +632,28 @@ def get_portfolio_configurations(self):
580632
.portfolio_configuration_service()
581633
return portfolio_configuration_service.get_all()
582634

583-
def get_market_credentials(self):
635+
def get_market_credential(self, market: str) -> MarketCredential:
636+
"""
637+
Function to get a market credential from the app. This method
638+
should be called when you want to get a market credential.
639+
640+
Args:
641+
market (str): The market to get the credential for
642+
643+
Returns:
644+
MarketCredential: Instance of MarketCredential
645+
"""
646+
647+
market_credential_service = self.container \
648+
.market_credential_service()
649+
market_credential = market_credential_service.get(market)
650+
if market_credential is None:
651+
raise OperationalException(
652+
f"Market credential for {market} not found"
653+
)
654+
return market_credential
655+
656+
def get_market_credentials(self) -> List[MarketCredential]:
584657
"""
585658
Function to get all market credentials from the app. This method
586659
should be called when you want to get all market credentials.
@@ -852,17 +925,40 @@ def add_market_credential(self, market_credential: MarketCredential):
852925
market_credential.market = market_credential.market.upper()
853926
self._market_credential_service.add(market_credential)
854927

855-
def on_initialize(self, app_hook: AppHook):
928+
def on_initialize(self, app_hook):
856929
"""
857930
Function to add a hook that runs when the app is initialized. The hook
858931
should be an instance of AppHook.
859932
"""
860933

934+
# Check if the app_hook inherits from AppHook
935+
if not issubclass(app_hook, AppHook):
936+
raise OperationalException(
937+
"App hook should be an instance of AppHook"
938+
)
939+
861940
if inspect.isclass(app_hook):
862941
app_hook = app_hook()
863942

864943
self._on_initialize_hooks.append(app_hook)
865944

945+
def on_strategy_run(self, app_hook):
946+
"""
947+
Function to add a hook that runs when a strategy is run. The hook
948+
should be an instance of AppHook.
949+
"""
950+
951+
# Check if the app_hook inherits from AppHook
952+
if not issubclass(app_hook, AppHook):
953+
raise OperationalException(
954+
"App hook should be an instance of AppHook"
955+
)
956+
957+
if inspect.isclass(app_hook):
958+
app_hook = app_hook()
959+
960+
self._on_strategy_run_hooks.append(app_hook)
961+
866962
def after_initialize(self, app_hook: AppHook):
867963
"""
868964
Function to add a hook that runs after the app is initialized.

investing_algorithm_framework/dependency_container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class DependencyContainer(containers.DeclarativeContainer):
132132
market_service=market_service,
133133
portfolio_provider_lookup=portfolio_provider_lookup,
134134
)
135-
strategy_orchestrator_service = providers.Factory(
135+
strategy_orchestrator_service = providers.ThreadSafeSingleton(
136136
StrategyOrchestratorService,
137137
market_data_source_service=market_data_source_service,
138138
configuration_service=configuration_service,

investing_algorithm_framework/domain/models/market/market_credential.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,6 @@ def initialize(self):
5151
environment_variable = f"{self.market.upper()}_SECRET_KEY"
5252
self._secret_key = os.getenv(environment_variable)
5353

54-
if self.secret_key is None:
55-
raise OperationalException(
56-
f"Market credential for market {self.market}"
57-
" requires a secret key, either"
58-
" as an argument or as an environment variable"
59-
f" named as {self._market.upper()}_SECRET_KEY"
60-
)
61-
6254
def get_api_key(self):
6355
return self.api_key
6456

investing_algorithm_framework/infrastructure/order_executors/ccxt_order_executor.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import ccxt
44

55
from investing_algorithm_framework.domain import OrderExecutor, \
6-
OperationalException, Order, OrderStatus, OrderSide, OrderType
6+
OperationalException, Order, OrderStatus, OrderSide, OrderType, \
7+
MarketCredential
78

89
logger = getLogger("investing_algorithm_framework")
910

@@ -136,12 +137,55 @@ def initialize_exchange(market, market_credential):
136137
f"No market service found for market id {market}"
137138
)
138139

140+
# Check the credentials for the exchange
141+
CCXTOrderExecutor.check_credentials(exchange_class, market_credential)
139142
exchange = exchange_class({
140143
'apiKey': market_credential.api_key,
141144
'secret': market_credential.secret_key,
142145
})
143146
return exchange
144147

148+
@staticmethod
149+
def check_credentials(
150+
exchange_class, market_credential: MarketCredential
151+
):
152+
"""
153+
Function to check if the credentials are valid for the exchange.
154+
155+
Args:
156+
exchange_class: The exchange class to check the credentials for
157+
market_credential: The market credential to use for the exchange
158+
159+
Raises:
160+
OperationalException: If the credentials are not valid
161+
162+
Returns:
163+
None
164+
"""
165+
exchange = exchange_class()
166+
credentials_info = exchange.requiredCredentials
167+
market = market_credential.get_market()
168+
169+
if ('apiKey' in credentials_info
170+
and credentials_info["apiKey"]
171+
and market_credential.get_api_key() is None):
172+
raise OperationalException(
173+
f"Market credential for market {market}"
174+
" requires an api key, either"
175+
" as an argument or as an environment variable"
176+
f" named as {market.upper()}_API_KEY"
177+
)
178+
179+
if ('secret' in credentials_info
180+
and credentials_info["secret"]
181+
and market_credential.get_secret_key() is None):
182+
raise OperationalException(
183+
f"Market credential for market {market}"
184+
" requires a secret key, either"
185+
" as an argument or as an environment variable"
186+
f" named as {market.upper()}_SECRET_KEY"
187+
)
188+
145189
def supports_market(self, market):
146190
"""
147191
Function to check if the market is supported by the portfolio

investing_algorithm_framework/infrastructure/portfolio_providers/ccxt_portfolio_provider.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Union
44

55
from investing_algorithm_framework.domain import PortfolioProvider, \
6-
OperationalException, Order, Position
6+
OperationalException, Order, Position, MarketCredential
77

88

99
logger = getLogger("investing_algorithm_framework")
@@ -110,6 +110,17 @@ def get_position(
110110

111111
@staticmethod
112112
def initialize_exchange(market, market_credential):
113+
"""
114+
Function to initialize the exchange for the market.
115+
116+
Args:
117+
market (str): The market to initialize the exchange for
118+
market_credential (MarketCredential): The market credential to use
119+
for the exchange
120+
121+
Returns:
122+
123+
"""
113124
market = market.lower()
114125

115126
if not hasattr(ccxt, market):
@@ -124,12 +135,56 @@ def initialize_exchange(market, market_credential):
124135
f"No market service found for market id {market}"
125136
)
126137

138+
# Check the credentials for the exchange
139+
(CCXTPortfolioProvider
140+
.check_credentials(exchange_class, market_credential))
127141
exchange = exchange_class({
128142
'apiKey': market_credential.api_key,
129143
'secret': market_credential.secret_key,
130144
})
131145
return exchange
132146

147+
@staticmethod
148+
def check_credentials(
149+
exchange_class, market_credential: MarketCredential
150+
):
151+
"""
152+
Function to check if the credentials are valid for the exchange.
153+
154+
Args:
155+
exchange_class: The exchange class to check the credentials for
156+
market_credential: The market credential to use for the exchange
157+
158+
Raises:
159+
OperationalException: If the credentials are not valid
160+
161+
Returns:
162+
None
163+
"""
164+
exchange = exchange_class()
165+
credentials_info = exchange.requiredCredentials
166+
market = market_credential.get_market()
167+
168+
if ('apiKey' in credentials_info
169+
and credentials_info["apiKey"]
170+
and market_credential.get_api_key() is None):
171+
raise OperationalException(
172+
f"Market credential for market {market}"
173+
" requires an api key, either"
174+
" as an argument or as an environment variable"
175+
f" named as {market.upper()}_API_KEY"
176+
)
177+
178+
if ('secret' in credentials_info
179+
and credentials_info["secret"]
180+
and market_credential.get_secret_key() is None):
181+
raise OperationalException(
182+
f"Market credential for market {market}"
183+
" requires a secret key, either"
184+
" as an argument or as an environment variable"
185+
f" named as {market.upper()}_SECRET_KEY"
186+
)
187+
133188
def supports_market(self, market):
134189
"""
135190
Function to check if the market is supported by the portfolio

investing_algorithm_framework/services/strategy_orchestrator_service.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ class StrategyOrchestratorService:
3636
def __init__(
3737
self,
3838
market_data_source_service: MarketDataSourceService,
39-
configuration_service
39+
configuration_service,
4040
):
41+
self._app_hooks = []
4142
self.history = {}
4243
self._strategies = []
4344
self._tasks = []
@@ -49,6 +50,18 @@ def __init__(
4950
= market_data_source_service
5051
self.configuration_service = configuration_service
5152

53+
def add_app_hook(self, app_hook):
54+
"""
55+
Add an app hook to the list of app hooks
56+
57+
Args:
58+
app_hook (AppHook): The app hook to add
59+
60+
Returns:
61+
None
62+
"""
63+
self._app_hooks.append(app_hook)
64+
5265
def cleanup_threads(self):
5366

5467
for stoppable in self.threads:
@@ -73,6 +86,10 @@ def run_strategy(self, strategy, context, sync=False):
7386

7487
logger.info(f"Running strategy {strategy.worker_id}")
7588

89+
# Run the app hooks
90+
for app_hook in self._app_hooks:
91+
app_hook.on_run(context=context)
92+
7693
if sync:
7794
strategy.run_strategy(
7895
market_data=market_data,

0 commit comments

Comments
 (0)