diff --git a/.gitignore b/.gitignore index e24a8e30..19a92065 100644 --- a/.gitignore +++ b/.gitignore @@ -145,5 +145,6 @@ lib/ bumpversion.egg-info/ *.sqlite3 -*/backtest_data/ -*/backtest_reports/ \ No newline at end of file +**/backtest_data/* +*/backtest_reports/ +**/backtest_reports/* diff --git a/README.md b/README.md index 147d0ed8..ad586e34 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,13 @@ It also exposes an REST API that allows you to interact with the algorithm. import pathlib from investing_algorithm_framework import create_app, PortfolioConfiguration, \ RESOURCE_DIRECTORY, TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \ - CCXTTickerMarketDataSource, MarketCredential + CCXTTickerMarketDataSource, MarketCredential, SYMBOLS + +# Define resource directory and the symbols you want to trade +config = { + RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve() + SYMBOLS: ["BTC/EUR"] +} # Define market data sources bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource( @@ -54,7 +60,7 @@ bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( market="BITVAVO", symbol="BTC/EUR", ) -app = create_app({RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve()}) +app = create_app(config=config) algorithm = Algorithm() app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h) diff --git a/examples/backtest/app.py b/examples/backtest/app.py index 077ab098..2214545b 100644 --- a/examples/backtest/app.py +++ b/examples/backtest/app.py @@ -5,3 +5,7 @@ app = create_app( config={RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve()} ) + + +if __name__ == "__main__": + app.run() diff --git a/examples/bitvavo_trading_bot/bitvavo.py b/examples/bitvavo_trading_bot/bitvavo.py index 00890463..e815eff1 100644 --- a/examples/bitvavo_trading_bot/bitvavo.py +++ b/examples/bitvavo_trading_bot/bitvavo.py @@ -1,6 +1,7 @@ +import os from investing_algorithm_framework import MarketCredential, TimeUnit, \ CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, TradingStrategy, \ - create_app, PortfolioConfiguration, Algorithm + create_app, PortfolioConfiguration, Algorithm, SYMBOLS, RESOURCE_DIRECTORY """ Bitvavo trading bot example with market data sources of bitvavo. @@ -45,12 +46,18 @@ def apply_strategy(self, algorithm, market_data): print(market_data["BTC/EUR-ohlcv"]) print(market_data["BTC/EUR-ticker"]) + +config = { + SYMBOLS: ["BTC/EUR"], + RESOURCE_DIRECTORY: os.path.join(os.path.dirname(__file__), "resources") +} + # Create an algorithm and link your trading strategy to it algorithm = Algorithm() algorithm.add_strategy(BitvavoTradingStrategy) # Create an app and add the market data sources and market credentials to it -app = create_app() +app = create_app(config=config) app.add_market_credential(bitvavo_market_credential) app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h) app.add_market_data_source(bitvavo_btc_eur_ticker) diff --git a/examples/coinbase_trading_bot/coinbase.py b/examples/coinbase_trading_bot/coinbase.py index 4893b420..eab0d491 100644 --- a/examples/coinbase_trading_bot/coinbase.py +++ b/examples/coinbase_trading_bot/coinbase.py @@ -1,6 +1,7 @@ +import os from investing_algorithm_framework import MarketCredential, TimeUnit, \ CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, TradingStrategy, \ - create_app, PortfolioConfiguration, Algorithm + create_app, PortfolioConfiguration, Algorithm, SYMBOLS, RESOURCE_DIRECTORY """ Coinbase market data sources example. Coinbase requires you to have an API key @@ -42,11 +43,15 @@ class CoinBaseTradingStrategy(TradingStrategy): def apply_strategy(self, algorithm, market_data): pass +config = { + SYMBOLS: ["BTC/EUR"], + RESOURCE_DIRECTORY: os.path.join(os.path.dirname(__file__), "resources") +} algorithm = Algorithm() algorithm.add_strategy(CoinBaseTradingStrategy) -app = create_app() +app = create_app(config=config) app.add_algorithm(algorithm) app.add_market_credential(coinbase_market_credential) app.add_market_data_source(coinbase_btc_eur_ohlcv_2h) diff --git a/examples/crossover_moving_average_trading_bot/app.py b/examples/crossover_moving_average_trading_bot/app.py index d3773e04..77b387cb 100644 --- a/examples/crossover_moving_average_trading_bot/app.py +++ b/examples/crossover_moving_average_trading_bot/app.py @@ -1,8 +1,11 @@ import pathlib -from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY +from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \ + SYMBOLS -app = create_app( - config={RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve()} -) +config = { + SYMBOLS: ["BTC/EUR"], + RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve() +} +app = create_app(config=config) diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py index 82619284..a425b4f4 100644 --- a/investing_algorithm_framework/__init__.py +++ b/investing_algorithm_framework/__init__.py @@ -5,7 +5,7 @@ OrderStatus, OrderSide, Config, TimeUnit, TimeInterval, Order, Portfolio, \ Position, TimeFrame, BACKTESTING_INDEX_DATETIME, MarketCredential, \ PortfolioConfiguration, RESOURCE_DIRECTORY, pretty_print_backtest, \ - Trade, OHLCVMarketDataSource, OrderBookMarketDataSource, \ + Trade, OHLCVMarketDataSource, OrderBookMarketDataSource, SYMBOLS, \ TickerMarketDataSource, MarketService, BacktestReportsEvaluation, \ pretty_print_backtest_reports_evaluation, load_backtest_reports from investing_algorithm_framework.app import TradingStrategy, \ @@ -55,4 +55,5 @@ "pretty_print_backtest_reports_evaluation", "BacktestReportsEvaluation", "load_backtest_reports", + "SYMBOLS" ] diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index e102d749..4c505ade 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -248,6 +248,7 @@ def _initialize_app_for_backtest( # Override the portfolio service with the backtest portfolio service self.container.portfolio_service.override( BacktestPortfolioService( + configuration_service=self.container.configuration_service(), market_credential_service=self.container .market_credential_service(), market_service=self.container.market_service(), diff --git a/investing_algorithm_framework/dependency_container.py b/investing_algorithm_framework/dependency_container.py index 8c52daec..66b1b803 100644 --- a/investing_algorithm_framework/dependency_container.py +++ b/investing_algorithm_framework/dependency_container.py @@ -85,6 +85,7 @@ class DependencyContainer(containers.DeclarativeContainer): ) portfolio_service = providers.Factory( PortfolioService, + configuration_service=configuration_service, market_credential_service=market_credential_service, market_service=market_service, position_repository=position_repository, diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py index b29ac251..28734b45 100644 --- a/investing_algorithm_framework/domain/__init__.py +++ b/investing_algorithm_framework/domain/__init__.py @@ -13,7 +13,7 @@ DATETIME_FORMAT, DATETIME_FORMAT_BACKTESTING, BACKTESTING_FLAG, \ BACKTESTING_INDEX_DATETIME, BACKTESTING_START_DATE, CCXT_DATETIME_FORMAT, \ BACKTEST_DATA_DIRECTORY_NAME, TICKER_DATA_TYPE, OHLCV_DATA_TYPE, \ - CURRENT_UTC_DATETIME, BACKTESTING_END_DATE, \ + CURRENT_UTC_DATETIME, BACKTESTING_END_DATE, SYMBOLS, \ CCXT_DATETIME_FORMAT_WITH_TIMEZONE, \ BACKTESTING_PENDING_ORDER_CHECK_INTERVAL from .singleton import Singleton @@ -105,4 +105,5 @@ "BacktestReportsEvaluation", "load_csv_into_dict", "load_backtest_reports", + "SYMBOLS" ] diff --git a/investing_algorithm_framework/domain/config.py b/investing_algorithm_framework/domain/config.py index 1e73fe40..3bb127cd 100644 --- a/investing_algorithm_framework/domain/config.py +++ b/investing_algorithm_framework/domain/config.py @@ -76,6 +76,7 @@ class Config(dict): SQLITE_ENABLED = True SQLITE_INITIALIZED = False BACKTEST_DATA_DIRECTORY_NAME = "backtest_data" + SYMBOLS = None def __init__(self, resource_directory=None): super().__init__() @@ -85,54 +86,6 @@ def __init__(self, resource_directory=None): super().__init__(vars(self.__class__)) - # def __setitem__(self, key, item): - # self.__dict__[key] = item - # - # def __getitem__(self, key): - # return self.__dict__[key] - # - # def __repr__(self): - # return repr(self.__dict__) - # - # def __len__(self): - # return len(self.__dict__) - # - # def __delitem__(self, key): - # del self.__dict__[key] - # - # def clear(self): - # return self.__dict__.clear() - # - # def copy(self): - # return self.__dict__.copy() - # - # def has_key(self, k): - # return k in self.__dict__ - # - # def update(self, *args, **kwargs): - # return self.__dict__.update(*args, **kwargs) - # - # def keys(self): - # return self.__dict__.keys() - # - # def values(self): - # return self.__dict__.values() - # - # def items(self): - # return self.__dict__.items() - # - # def pop(self, *args): - # return self.__dict__.pop(*args) - # - # def __cmp__(self, dict_): - # return self.__cmp__(self.__dict__, dict_) - # - # def __contains__(self, item): - # return item in self.__dict__ - # - # def __iter__(self): - # return iter(self.__dict__) - def __str__(self): field_strings = [] diff --git a/investing_algorithm_framework/domain/constants.py b/investing_algorithm_framework/domain/constants.py index ecc49a43..6fdb0077 100644 --- a/investing_algorithm_framework/domain/constants.py +++ b/investing_algorithm_framework/domain/constants.py @@ -11,6 +11,7 @@ DATABASE_URL = 'DATABASE_URL' DEFAULT_DATABASE_NAME = "database" +SYMBOLS = "SYMBOLS" RESOURCE_DIRECTORY = "RESOURCE_DIRECTORY" BACKTEST_DATA_DIRECTORY_NAME = "BACKTEST_DATA_DIRECTORY_NAME" LOG_LEVEL = 'LOG_LEVEL' diff --git a/investing_algorithm_framework/services/portfolio_service/portfolio_service.py b/investing_algorithm_framework/services/portfolio_service/portfolio_service.py index 949e0b86..c3f752ce 100644 --- a/investing_algorithm_framework/services/portfolio_service/portfolio_service.py +++ b/investing_algorithm_framework/services/portfolio_service/portfolio_service.py @@ -2,9 +2,11 @@ from datetime import datetime from investing_algorithm_framework.domain import OrderSide, OrderStatus, \ - OperationalException, MarketService, MarketCredentialService + OperationalException, MarketService, MarketCredentialService, SYMBOLS from investing_algorithm_framework.services.repository_service \ import RepositoryService +from investing_algorithm_framework.services.configuration_service import \ + ConfigurationService logger = logging.getLogger("investing_algorithm_framework") @@ -18,6 +20,7 @@ class PortfolioService(RepositoryService): def __init__( self, + configuration_service: ConfigurationService, market_service: MarketService, market_credential_service: MarketCredentialService, position_repository, @@ -26,6 +29,7 @@ def __init__( portfolio_configuration_service, portfolio_snapshot_service, ): + self.configuration_service = configuration_service self.market_credential_service = market_credential_service self.market_service = market_service self.position_repository = position_repository @@ -235,12 +239,24 @@ def sync_portfolio_orders(self, portfolio): portfolio_configuration = self.portfolio_configuration_service \ .get(portfolio.identifier) - # Get all available symbols for the market and check if - # there are orders - available_symbols = self.market_service.get_symbols( - market=portfolio.market - ) + # Check if the symbols param in the configuration is set + config = self.configuration_service.config + + if SYMBOLS in config and config[SYMBOLS] is not None: + available_symbols = config[SYMBOLS] + + if not isinstance(available_symbols, list): + raise OperationalException( + "The symbols configuration should be a list of strings" + ) + else: + # if not, get all available symbols for the market and check if + # there are orders + available_symbols = self.market_service.get_symbols( + market=portfolio.market + ) + # Check if there are orders for the available symbols for symbol in available_symbols: orders = self.market_service.get_orders( symbol=symbol, diff --git a/tests/services/test_portfolio_service.py b/tests/services/test_portfolio_service.py index 858b8921..43c6bd85 100644 --- a/tests/services/test_portfolio_service.py +++ b/tests/services/test_portfolio_service.py @@ -168,3 +168,149 @@ def test_sync_portfolio_orders(self): pending_orders = self.iaf_app.algorithm.get_pending_orders() self.assertEqual(1, len(pending_orders)) self.assertEqual(10, pending_orders[0].amount) + + def test_sync_portfolio_orders_with_symbols_config(self): + """ + Test that the portfolio service can sync existing orders with + symbols configuration in the app config. + + The test should make sure that the portfolio service can sync + existing orders from the market service to the order service. It + should also only sync orders that are in the symbols configuration + in the app config. + """ + configuration_service = self.iaf_app.container.configuration_service() + configuration_service.config["SYMBOLS"] = ["BTC/EUR", "DOT/EUR"] + portfolio_service: PortfolioService \ + = self.iaf_app.container.portfolio_service() + market_service_stub = MarketServiceStub(None) + market_service_stub.orders = [ + Order.from_dict( + { + "id": "1323", + "side": "buy", + "symbol": "BTC/EUR", + "amount": 10, + "price": 10.0, + "status": "CLOSED", + "order_type": "limit", + "order_side": "buy", + "created_at": "2023-08-08T14:40:56.626362Z", + "filled": 10, + "remaining": 0, + }, + ), + Order.from_dict( + { + "id": "2332", + "side": "sell", + "symbol": "BTC/EUR", + "amount": 10, + "price": 20.0, + "status": "CLOSED", + "order_type": "limit", + "order_side": "sell", + "created_at": "2023-08-10T14:40:56.626362Z", + "filled": 10, + "remaining": 0, + }, + ), + Order.from_dict( + { + "id": "14354", + "side": "buy", + "symbol": "DOT/EUR", + "amount": 10, + "price": 10.0, + "status": "CLOSED", + "order_type": "limit", + "order_side": "buy", + "created_at": "2023-09-22T14:40:56.626362Z", + "filled": 10, + "remaining": 0, + }, + ), + Order.from_dict( + { + "id": "49394", + "side": "buy", + "symbol": "ETH/EUR", + "amount": 10, + "price": 10.0, + "status": "OPEN", + "order_type": "limit", + "order_side": "buy", + "created_at": "2023-08-08T14:40:56.626362Z", + "filled": 0, + "remaining": 0, + }, + ), + ] + market_service_stub.symbols = [ + "BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR" + ] + portfolio_service.market_service = market_service_stub + portfolio = portfolio_service.find({"market": "binance"}) + portfolio_service.sync_portfolio_orders(portfolio) + + # Check that the portfolio has the correct amount of orders + order_service = self.iaf_app.container.order_service() + self.assertEqual(3, order_service.count()) + self.assertEqual( + 3, order_service.count({"portfolio": portfolio.id}) + ) + self.assertEqual( + 2, order_service.count({"target_symbol": "BTC"}) + ) + self.assertEqual( + 0, order_service.count({"portfolio_id": 2321}) + ) + self.assertEqual( + 1, order_service.count({"target_symbol": "DOT"}) + ) + self.assertEqual( + 0, order_service.count({"target_symbol": "ETH"}) + ) + + # Check that the portfolio has the correct amount of trades + trade_service = self.iaf_app.container.trade_service() + self.assertEqual(2, trade_service.count()) + self.assertEqual( + 1, trade_service.count( + {"portfolio_id": portfolio.id, "status": "CLOSED"} + ) + ) + self.assertEqual( + 1, trade_service.count( + {"portfolio_id": portfolio.id, "status": "OPEN"} + ) + ) + + # Check if all positions are made + position_service = self.iaf_app.container.position_service() + self.assertEqual(3, position_service.count()) + + # Check if btc position exists + btc_position = position_service.find( + {"portfolio_id": portfolio.id, "symbol": "BTC"} + ) + self.assertEqual(0, btc_position.amount) + + # Check if dot position exists + dot_position = position_service.find( + {"portfolio_id": portfolio.id, "symbol": "DOT"} + ) + self.assertEqual(10, dot_position.amount) + + # Check if eur position exists + eur_position = position_service.find( + {"portfolio_id": portfolio.id, "symbol": "EUR"} + ) + self.assertEqual(1000, eur_position.amount) + + # Check that there is the correct amount of pending orders + order_service: OrderService = self.iaf_app.container.order_service() + self.assertEqual(0, order_service.count({"status": "OPEN"})) + pending_orders = self.iaf_app.algorithm.get_pending_orders() + self.assertEqual(0, len(pending_orders)) +