diff --git a/docs/docs/ressources/how-tos/how-to-install-development-tools.md b/docs/docs/ressources/how-tos/how-to-install-development-tools.md index f1e5b80a3..f43baff59 100644 --- a/docs/docs/ressources/how-tos/how-to-install-development-tools.md +++ b/docs/docs/ressources/how-tos/how-to-install-development-tools.md @@ -24,7 +24,7 @@ sudo apt-get -y install git ## [Java](https://openjdk.java.net/install/) ```bash -sudo apt-get -y install openjdk-11-jdk +sudo apt-get -y install openjdk-17-jdk ``` ## [Maven](https://maven.apache.org/) diff --git a/docs/docs/ressources/how-tos/how-to-review-code.md b/docs/docs/ressources/how-tos/how-to-review-code.md index 559bf1534..9171aee94 100644 --- a/docs/docs/ressources/how-tos/how-to-review-code.md +++ b/docs/docs/ressources/how-tos/how-to-review-code.md @@ -11,4 +11,5 @@ description: How to review Cassandre code * No new line between method start ({) and method end (}). * When a variable represents an id, say it in the name like 'carId'. * When a variable is DTO, say it in the name like `carDTO`. -* Check logs texts. \ No newline at end of file +* Check logs texts. +* No point at the end of logs texts. \ No newline at end of file diff --git a/spring-boot-starter-api/spring-boot-starter-api-graphql/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/GraphQLAPIKeyAuthenticationManager.java b/spring-boot-starter-api/spring-boot-starter-api-graphql/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/GraphQLAPIKeyAuthenticationManager.java index b22325480..a487c7c0c 100644 --- a/spring-boot-starter-api/spring-boot-starter-api-graphql/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/GraphQLAPIKeyAuthenticationManager.java +++ b/spring-boot-starter-api/spring-boot-starter-api-graphql/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/GraphQLAPIKeyAuthenticationManager.java @@ -26,7 +26,7 @@ public GraphQLAPIKeyAuthenticationManager(final String newKey) { public final Authentication authenticate(final Authentication authentication) throws AuthenticationException { String principal = (String) authentication.getPrincipal(); if (key != null && !key.equals(principal)) { - throw new BadCredentialsException("The API key was not found or not the expected value."); + throw new BadCredentialsException("The API key was not found or not the expected valueUnknownExchangeTest.checkErrorMessages"); } authentication.setAuthenticated(true); return authentication; diff --git a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/DatabaseAutoConfiguration.java b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/DatabaseAutoConfiguration.java index 04cd6143b..142e7b4b2 100644 --- a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/DatabaseAutoConfiguration.java +++ b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/DatabaseAutoConfiguration.java @@ -15,8 +15,8 @@ * DatabaseAutoConfiguration configures the database. */ @Configuration -@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider") @EntityScan(basePackages = "tech.cassandre.trading.bot.domain") +@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider") @EnableJpaRepositories(basePackages = "tech.cassandre.trading.bot.repository") public class DatabaseAutoConfiguration extends BaseConfiguration { diff --git a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ExchangeAutoConfiguration.java b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ExchangeAutoConfiguration.java index c4c6f3a93..4db29e3bb 100644 --- a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ExchangeAutoConfiguration.java +++ b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ExchangeAutoConfiguration.java @@ -43,12 +43,6 @@ @RequiredArgsConstructor public class ExchangeAutoConfiguration extends BaseConfiguration { - /** XChange user sandbox parameter. */ - private static final String USE_SANDBOX_PARAMETER = "Use_Sandbox"; - - /** XChange passphrase parameter. */ - private static final String PASSPHRASE_PARAMETER = "passphrase"; - /** Unauthorized http status code. */ private static final int UNAUTHORIZED_STATUS_CODE = 401; @@ -112,34 +106,22 @@ public class ExchangeAutoConfiguration extends BaseConfiguration { @PostConstruct public void configure() { try { - // Instantiate exchange. + // Instantiate exchange class. Class exchangeClass = Class.forName(getExchangeClassName()).asSubclass(Exchange.class); ExchangeSpecification exchangeSpecification = new ExchangeSpecification(exchangeClass); // Exchange configuration. - exchangeSpecification.setExchangeSpecificParametersItem(USE_SANDBOX_PARAMETER, exchangeParameters.getModes().getSandbox()); exchangeSpecification.setUserName(exchangeParameters.getUsername()); - exchangeSpecification.setExchangeSpecificParametersItem(PASSPHRASE_PARAMETER, exchangeParameters.getPassphrase()); exchangeSpecification.setApiKey(exchangeParameters.getKey()); exchangeSpecification.setSecretKey(exchangeParameters.getSecret()); exchangeSpecification.getResilience().setRateLimiterEnabled(true); - - // Specific parameters. - if (exchangeParameters.getProxyHost() != null) { - exchangeSpecification.setProxyHost(exchangeParameters.getProxyHost()); - } - if (exchangeParameters.getProxyPort() != null) { - exchangeSpecification.setProxyPort(exchangeParameters.getProxyPort()); - } - if (exchangeParameters.getSslUri() != null) { - exchangeSpecification.setSslUri(exchangeParameters.getSslUri()); - } - if (exchangeParameters.getPlainTextUri() != null) { - exchangeSpecification.setPlainTextUri(exchangeParameters.getPlainTextUri()); - } - if (exchangeParameters.getHost() != null) { - exchangeSpecification.setHost(exchangeParameters.getHost()); - } + exchangeSpecification.setExchangeSpecificParametersItem("Use_Sandbox", exchangeParameters.getModes().getSandbox()); + exchangeSpecification.setExchangeSpecificParametersItem("passphrase", exchangeParameters.getPassphrase()); + exchangeSpecification.setProxyHost(exchangeParameters.getProxyHost()); + exchangeSpecification.setProxyPort(exchangeParameters.getProxyPort()); + exchangeSpecification.setSslUri(exchangeParameters.getSslUri()); + exchangeSpecification.setPlainTextUri(exchangeParameters.getPlainTextUri()); + exchangeSpecification.setHost(exchangeParameters.getHost()); if (exchangeParameters.getPort() != null) { exchangeSpecification.setPort(Integer.parseInt(exchangeParameters.getPort())); } @@ -151,16 +133,16 @@ public void configure() { xChangeTradeService = xChangeExchange.getTradeService(); // Force login to check credentials. - logger.info("Exchange connection with {} driver.", exchangeParameters.getDriverClassName()); + logger.info("Exchange connection with driver {}", exchangeParameters.getDriverClassName()); xChangeAccountService.getAccountInfo(); - logger.info("Exchange connection with username {} successful (Dry mode: {} / Sandbox: {})", + logger.info("Exchange connection successful with username {} (Dry mode: {} / Sandbox: {})", exchangeParameters.getUsername(), exchangeParameters.getModes().getDry(), exchangeParameters.getModes().getSandbox()); } catch (ClassNotFoundException e) { // If we can't find the exchange class. - throw new ConfigurationException("Impossible to find the exchange you requested: " + exchangeParameters.getDriverClassName(), - "Choose a valid exchange (https://github.com/knowm/XChange) and add the dependency to Cassandre"); + throw new ConfigurationException("Impossible to find the exchange driver class you requested: " + exchangeParameters.getDriverClassName(), + "Choose and configure a valid exchange (https://trading-bot.cassandre.tech/learn/exchange-connection-configuration.html#how-does-it-works)"); } catch (HttpStatusIOException e) { if (e.getHttpStatusCode() == UNAUTHORIZED_STATUS_CODE) { // Authorization failure. @@ -182,23 +164,18 @@ public void configure() { * @return XChange class name */ private String getExchangeClassName() { - // If the name contains a dot, it means that it's the XChange class name. + // If the name contains a dot, it means that the user set the complete XChange class name in the configuration. if (exchangeParameters.getDriverClassName() != null && exchangeParameters.getDriverClassName().contains(".")) { return exchangeParameters.getDriverClassName(); + } else { + // Try to guess the XChange class package name from the exchange name parameter. + return "org.knowm.xchange." // Package (org.knowm.xchange.). + .concat(exchangeParameters.getDriverClassName().toLowerCase()) // domain (kucoin). + .concat(".") // A dot (.) + .concat(exchangeParameters.getDriverClassName().substring(0, 1).toUpperCase()) // First letter uppercase (K). + .concat(exchangeParameters.getDriverClassName().substring(1).toLowerCase()) // The rest of the exchange name (ucoin). + .concat("Exchange"); // Adding exchange (Exchange). } - - // XChange class package name and suffix. - final String xChangeClassPackage = "org.knowm.xchange."; - final String xChangeCLassSuffix = "Exchange"; - - // Returns the XChange package name from exchange name. - assert exchangeParameters.getDriverClassName() != null; - return xChangeClassPackage // Package (org.knowm.xchange.). - .concat(exchangeParameters.getDriverClassName().toLowerCase()) // domain (kucoin). - .concat(".") // A dot (.) - .concat(exchangeParameters.getDriverClassName().substring(0, 1).toUpperCase()) // First letter uppercase (K). - .concat(exchangeParameters.getDriverClassName().substring(1).toLowerCase()) // The rest of the exchange name (ucoin). - .concat(xChangeCLassSuffix); // Adding exchange (Exchange). } /** diff --git a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ScheduleAutoConfiguration.java b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ScheduleAutoConfiguration.java index e6e13e048..b81b6397f 100644 --- a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ScheduleAutoConfiguration.java +++ b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/ScheduleAutoConfiguration.java @@ -19,9 +19,13 @@ /** * ScheduleAutoConfiguration configures the flux calls. + * Three scheduled tasks: + * - One calling account flux. + * - One calling ticker flux. + * - One calling order & trade flux. */ -@Configuration @Profile("!schedule-disabled") +@Configuration @EnableScheduling @RequiredArgsConstructor public class ScheduleAutoConfiguration extends BaseConfiguration { @@ -29,15 +33,12 @@ public class ScheduleAutoConfiguration extends BaseConfiguration { /** Scheduler pool size. */ private static final int SCHEDULER_POOL_SIZE = 3; - /** Start delay in milliseconds. */ + /** Start delay in milliseconds (1 000 ms = 1 second). */ private static final int START_DELAY_IN_MILLISECONDS = 1_000; - /** Termination delay in milliseconds. */ + /** Termination delay in milliseconds (10 000 ms = 10 seconds). */ private static final int TERMINATION_DELAY_IN_MILLISECONDS = 10_000; - /** Thread prefix for schedulers. */ - private static final String THREAD_NAME_PREFIX = "cassandre-flux-"; - /** Flux continues to run as long as enabled is set to true. */ private final AtomicBoolean enabled = new AtomicBoolean(true); @@ -53,28 +54,6 @@ public class ScheduleAutoConfiguration extends BaseConfiguration { /** Trade flux. */ private final TradeFlux tradeFlux; - /** - * Configure the task scheduler. - * - * @return task scheduler - */ - @Bean - public TaskScheduler taskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setWaitForTasksToCompleteOnShutdown(true); - scheduler.setAwaitTerminationMillis(TERMINATION_DELAY_IN_MILLISECONDS); - scheduler.setThreadNamePrefix(THREAD_NAME_PREFIX); - scheduler.setPoolSize(SCHEDULER_POOL_SIZE); - scheduler.setErrorHandler(t -> { - try { - logger.error("Error in scheduled tasks: {}", t.getMessage()); - } catch (Exception e) { - logger.error("Error in scheduled tasks: {}", e.getMessage()); - } - }); - return scheduler; - } - /** * Recurrent calls to the account flux. */ @@ -108,11 +87,33 @@ public void orderAndTradeFluxUpdate() { /** * This method is called before the application shutdown. - * We stop the flux. + * We stop calling the flux. */ @PreDestroy public void shutdown() { enabled.set(false); } + /** + * Configure the task scheduler. + * + * @return task scheduler + */ + @Bean + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setWaitForTasksToCompleteOnShutdown(true); + scheduler.setAwaitTerminationMillis(TERMINATION_DELAY_IN_MILLISECONDS); + scheduler.setThreadNamePrefix("cassandre-flux-"); + scheduler.setPoolSize(SCHEDULER_POOL_SIZE); + scheduler.setErrorHandler(throwable -> { + try { + logger.error("Error in scheduled tasks: {}", throwable.getMessage()); + } catch (Exception exception) { + logger.error("Error in scheduled tasks: {}", exception.getMessage()); + } + }); + return scheduler; + } + } diff --git a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/StrategiesAutoConfiguration.java b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/StrategiesAutoConfiguration.java index 9ca40a0aa..e6eb0a9ef 100644 --- a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/StrategiesAutoConfiguration.java +++ b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/configuration/StrategiesAutoConfiguration.java @@ -70,12 +70,6 @@ @RequiredArgsConstructor public class StrategiesAutoConfiguration extends BaseConfiguration { - /** Tickers file prefix. */ - private static final String TICKERS_FILE_PREFIX = "tickers-to-import"; - - /** Tickers file suffix. */ - private static final String TICKERS_FILE_SUFFIX = ".csv"; - /** Application context. */ private final ApplicationContext applicationContext; @@ -134,106 +128,13 @@ public void configure() { final Map strategies = applicationContext.getBeansWithAnnotation(CassandreStrategy.class); // ============================================================================================================= - // Check if everything is ok. - // Prints all the supported currency pairs. - logger.info("Supported currency pairs by the exchange: {}.", - exchangeService.getAvailableCurrencyPairs() - .stream() - .map(CurrencyPairDTO::toString) - .collect(Collectors.joining(", "))); - - // Retrieve accounts information. - final Optional user = userService.getUser(); - if (user.isEmpty()) { - throw new ConfigurationException("Impossible to retrieve your user information", - "Impossible to retrieve your user information - Check logs"); - } else { - if (user.get().getAccounts().isEmpty()) { - // We were able to retrieve the user from the exchange but no account was found. - throw new ConfigurationException("User information retrieved but no associated accounts found", - "Check the permissions you set on the API you created"); - } else { - logger.info("Available accounts on the exchange:"); - user.get() - .getAccounts() - .values() - .forEach(account -> { - logger.info("- Account id / name: {} / {}.", - account.getAccountId(), - account.getName()); - account.getBalances() - .stream() - .filter(balance -> balance.getAvailable().compareTo(ZERO) != 0) - .forEach(balance -> logger.info(" - {} {}.", balance.getAvailable(), balance.getCurrency())); - }); - } - } - - // Check that there is at least one strategy. - if (strategies.isEmpty()) { - throw new ConfigurationException("No strategy found", "You must have one class with @CassandreStrategy annotation"); - } - - // Check that all strategies extends CassandreStrategyInterface. - Set strategiesWithoutExtends = strategies.values() - .stream() - .filter(strategy -> !(strategy instanceof CassandreStrategyInterface)) - .map(strategy -> strategy.getClass().getSimpleName()) - .collect(Collectors.toSet()); - if (!strategiesWithoutExtends.isEmpty()) { - final String list = String.join(",", strategiesWithoutExtends); - throw new ConfigurationException(list + " doesn't extend BasicCassandreStrategy or BasicTa4jCassandreStrategy", - list + " must extend BasicCassandreStrategy or BasicTa4jCassandreStrategy"); - } - - // Check that all strategies specifies an existing trade account. - final Set accountsAvailableOnExchange = new HashSet<>(user.get().getAccounts().values()); - Set strategiesWithoutTradeAccount = strategies.values() - .stream() - .filter(strategy -> ((CassandreStrategyInterface) strategy).getTradeAccount(accountsAvailableOnExchange).isEmpty()) - .map(strategy -> strategy.getClass().toString()) - .collect(Collectors.toSet()); - if (!strategiesWithoutTradeAccount.isEmpty()) { - final String strategyList = String.join(",", strategiesWithoutTradeAccount); - throw new ConfigurationException("Your strategies specify a trading account that doesn't exist", - "Check your getTradeAccount(Set accounts) method as it returns an empty result - Strategies in error: " + strategyList + "\r\n" - + "See https://trading-bot.cassandre.tech/ressources/how-tos/how-to-fix-common-problems.html#your-strategies-specifies-a-trading-account-that-doesn-t-exist"); - } - - // Check that there is no duplicated strategy ids. - final List strategyIds = strategies.values() - .stream() - .map(o -> o.getClass().getAnnotation(CassandreStrategy.class).strategyId()) - .toList(); - final Set duplicatedStrategyIds = strategies.values() - .stream() - .map(o -> o.getClass().getAnnotation(CassandreStrategy.class).strategyId()) - .filter(strategyId -> Collections.frequency(strategyIds, strategyId) > 1) - .collect(Collectors.toSet()); - if (!duplicatedStrategyIds.isEmpty()) { - throw new ConfigurationException("You have duplicated strategy ids", - "You have duplicated strategy ids: " + String.join(", ", duplicatedStrategyIds)); - } - - // Check that the currency pairs required by the strategies are available on the exchange. - final Set availableCurrencyPairs = exchangeService.getAvailableCurrencyPairs(); - final Set notAvailableCurrencyPairs = applicationContext - .getBeansWithAnnotation(CassandreStrategy.class) - .values() - .stream() - .map(o -> (CassandreStrategyInterface) o) - .map(CassandreStrategyInterface::getRequestedCurrencyPairs) - .flatMap(Set::stream) - .filter(currencyPairDTO -> !availableCurrencyPairs.contains(currencyPairDTO)) - .map(CurrencyPairDTO::toString) - .collect(Collectors.toSet()); - if (!notAvailableCurrencyPairs.isEmpty()) { - logger.warn("Your exchange doesn't support the following currency pairs you requested: {}.", String.join(", ", notAvailableCurrencyPairs)); - } + // Configuration check. + // We run tests to display and check if everything is ok with the configuration. + final UserDTO user = checkConfiguration(strategies); // ============================================================================================================= // Maintenance code. - // If a position was blocked in OPENING or CLOSING, we send again the trades. + // If a position is blocked in OPENING or CLOSING, we send again the trades. // This could happen if cassandre crashes after saving a trade and did not have time to send it to // positionService. Here we force the status recalculation, and we save it. positionRepository.findByStatusIn(Stream.of(OPENING, CLOSING).collect(Collectors.toSet())) @@ -260,6 +161,7 @@ public void configure() { // ============================================================================================================= // Configuring strategies. + // Data in database, services, flux... logger.info("Running the following strategies:"); strategies.values() .forEach(s -> { @@ -267,45 +169,44 @@ public void configure() { CassandreStrategy annotation = s.getClass().getAnnotation(CassandreStrategy.class); // Displaying information about strategy. - logger.info("- Strategy '{}/{}' (requires {}).", + logger.info("- Strategy '{}/{}' (requires {})", annotation.strategyId(), annotation.strategyName(), strategy.getRequestedCurrencyPairs().stream() .map(CurrencyPairDTO::toString) .collect(Collectors.joining(", "))); - // Saving or updating strategy in database. + // Saving or updating the strategy in database. strategyRepository.findByStrategyId(annotation.strategyId()).ifPresentOrElse(existingStrategy -> { - // Update. + // Updates strategy. existingStrategy.setName(annotation.strategyName()); strategyRepository.save(existingStrategy); final StrategyDTO strategyDTO = STRATEGY_MAPPER.mapToStrategyDTO(existingStrategy); strategyDTO.initializeLastPositionIdUsed(positionRepository.getLastPositionIdUsedByStrategy(strategyDTO.getId())); strategy.setStrategy(strategyDTO); - logger.debug("Strategy updated in database: {}.", existingStrategy); + logger.debug("Strategy updated in database: {}", existingStrategy); }, () -> { - // Creation. + // Creates strategy. Strategy newStrategy = new Strategy(); newStrategy.setStrategyId(annotation.strategyId()); newStrategy.setName(annotation.strategyName()); - // Set type. if (strategy instanceof BasicCassandreStrategy) { newStrategy.setType(BASIC_STRATEGY); } if (strategy instanceof BasicTa4jCassandreStrategy) { newStrategy.setType(BASIC_TA4J_STRATEGY); } - logger.debug("Strategy created in database: {}.", newStrategy); StrategyDTO strategyDTO = STRATEGY_MAPPER.mapToStrategyDTO(strategyRepository.save(newStrategy)); strategyDTO.initializeLastPositionIdUsed(positionRepository.getLastPositionIdUsedByStrategy(strategyDTO.getId())); strategy.setStrategy(strategyDTO); + logger.debug("Strategy created in database: {}", newStrategy); }); // Gives configuration information to the strategy. strategy.setDryModeIndicator(exchangeParameters.getModes().getDry()); // Initialize accounts values in strategy. - strategy.initializeAccounts(user.get().getAccounts()); + strategy.initializeAccounts(user.getAccounts()); // Setting services & repositories to strategy. strategy.setPositionFlux(positionFlux); @@ -321,14 +222,15 @@ public void configure() { strategy.initialize(); // Connecting flux to strategy. - connectableAccountFlux.subscribe(strategy::accountsUpdates, throwable -> logger.error("AccountsUpdates failing: {}.", throwable.getMessage())); - connectablePositionFlux.subscribe(strategy::positionsUpdates, throwable -> logger.error("PositionsUpdates failing: {}.", throwable.getMessage())); - connectableOrderFlux.subscribe(strategy::ordersUpdates, throwable -> logger.error("OrdersUpdates failing: {}.", throwable.getMessage())); - connectableTradeFlux.subscribe(strategy::tradesUpdates, throwable -> logger.error("TradesUpdates failing: {}.", throwable.getMessage())); - connectableTickerFlux.subscribe(strategy::tickersUpdates, throwable -> logger.error("TickersUpdates failing: {}.", throwable.getMessage())); + connectableAccountFlux.subscribe(strategy::accountsUpdates, throwable -> logger.error("AccountsUpdates failing: {}", throwable.getMessage())); + connectablePositionFlux.subscribe(strategy::positionsUpdates, throwable -> logger.error("PositionsUpdates failing: {}", throwable.getMessage())); + connectableOrderFlux.subscribe(strategy::ordersUpdates, throwable -> logger.error("OrdersUpdates failing: {}", throwable.getMessage())); + connectableTradeFlux.subscribe(strategy::tradesUpdates, throwable -> logger.error("TradesUpdates failing: {}", throwable.getMessage())); + connectableTickerFlux.subscribe(strategy::tickersUpdates, throwable -> logger.error("TickersUpdates failing: {}", throwable.getMessage())); }); - // Start flux. + // ============================================================================================================= + // Starting flux. connectableAccountFlux.connect(); connectablePositionFlux.connect(); connectableOrderFlux.connect(); @@ -337,13 +239,110 @@ public void configure() { } /** - * Getter for positionService. + * Check and display Cassandre configuration. * - * @return positionService + * @param strategies strategies + * @return user information */ - @Bean - public PositionService getPositionService() { - return positionService; + private UserDTO checkConfiguration(final Map strategies) { + // Prints all the supported currency pairs. + logger.info("Supported currency pairs by the exchange: {}", + exchangeService.getAvailableCurrencyPairs() + .stream() + .map(CurrencyPairDTO::toString) + .collect(Collectors.joining(", "))); + + // Retrieve accounts information. + final Optional user = userService.getUser(); + if (user.isEmpty()) { + // Unable to retrieve user information. + throw new ConfigurationException("Impossible to retrieve your user information", + "Impossible to retrieve your user information - Check logs"); + } else { + if (user.get().getAccounts().isEmpty()) { + // We were able to retrieve the user from the exchange but no account was found. + throw new ConfigurationException("User information retrieved but no associated accounts found", + "Check the permissions you set on the API you created"); + } else { + logger.info("Accounts available on the exchange:"); + user.get() + .getAccounts() + .values() + .forEach(account -> { + logger.info("- Account id / name: {} / {}", + account.getAccountId(), + account.getName()); + account.getBalances() + .stream() + .filter(balance -> balance.getAvailable().compareTo(ZERO) != 0) + .forEach(balance -> logger.info(" - {} {}", balance.getAvailable(), balance.getCurrency())); + }); + } + } + + // Check that there is at least one strategy. + if (strategies.isEmpty()) { + throw new ConfigurationException("No strategy found", "You must have, at least, one class with @CassandreStrategy annotation"); + } + + // Check that all strategies extends CassandreStrategyInterface. + Set strategiesWithoutExtends = strategies.values() + .stream() + .filter(strategy -> !(strategy instanceof CassandreStrategyInterface)) + .map(strategy -> strategy.getClass().getSimpleName()) + .collect(Collectors.toSet()); + if (!strategiesWithoutExtends.isEmpty()) { + final String list = String.join(",", strategiesWithoutExtends); + throw new ConfigurationException(list + " doesn't extend BasicCassandreStrategy or BasicTa4jCassandreStrategy", + list + " must extend BasicCassandreStrategy or BasicTa4jCassandreStrategy"); + } + + // Check that all strategies specifies an existing trade account. + final Set accountsAvailableOnExchange = new HashSet<>(user.get().getAccounts().values()); + Set strategiesWithoutTradeAccount = strategies.values() + .stream() + .filter(strategy -> ((CassandreStrategyInterface) strategy).getTradeAccount(accountsAvailableOnExchange).isEmpty()) + .map(strategy -> strategy.getClass().toString()) + .collect(Collectors.toSet()); + if (!strategiesWithoutTradeAccount.isEmpty()) { + final String strategyList = String.join(",", strategiesWithoutTradeAccount); + throw new ConfigurationException("Your strategies specify a trading account that doesn't exist", + "Check your getTradeAccount(Set accounts) method as it returns an empty result - Strategies in error: " + strategyList + "\r\n" + + "See https://trading-bot.cassandre.tech/ressources/how-tos/how-to-fix-common-problems.html#your-strategies-specifies-a-trading-account-that-doesn-t-exist"); + } + + // Check that there is no duplicated strategy ids. + final List strategyIds = strategies.values() + .stream() + .map(o -> o.getClass().getAnnotation(CassandreStrategy.class).strategyId()) + .toList(); + final Set duplicatedStrategyIds = strategies.values() + .stream() + .map(o -> o.getClass().getAnnotation(CassandreStrategy.class).strategyId()) + .filter(strategyId -> Collections.frequency(strategyIds, strategyId) > 1) + .collect(Collectors.toSet()); + if (!duplicatedStrategyIds.isEmpty()) { + throw new ConfigurationException("You have duplicated strategy ids", + "You have duplicated strategy ids: " + String.join(", ", duplicatedStrategyIds)); + } + + // Check that the currency pairs required by the strategies are available on the exchange. + final Set availableCurrencyPairs = exchangeService.getAvailableCurrencyPairs(); + final Set notAvailableCurrencyPairs = applicationContext + .getBeansWithAnnotation(CassandreStrategy.class) + .values() + .stream() + .map(o -> (CassandreStrategyInterface) o) + .map(CassandreStrategyInterface::getRequestedCurrencyPairs) + .flatMap(Set::stream) + .filter(currencyPairDTO -> !availableCurrencyPairs.contains(currencyPairDTO)) + .map(CurrencyPairDTO::toString) + .collect(Collectors.toSet()); + if (!notAvailableCurrencyPairs.isEmpty()) { + logger.warn("Your exchange doesn't support the following currency pairs you requested: {}", String.join(", ", notAvailableCurrencyPairs)); + } + + return user.get(); } /** @@ -359,7 +358,7 @@ private void loadImportedTickers() { getFilesToLoad() .parallelStream() .filter(resource -> resource.getFilename() != null) - .peek(resource -> logger.info("Importing file {}.", resource.getFilename())) + .peek(resource -> logger.info("Importing file {}", resource.getFilename())) .forEach(resource -> { try { // Insert the tickers in database. @@ -369,15 +368,15 @@ private void loadImportedTickers() { .build() .parse() .forEach(importedTicker -> { - logger.debug("Importing ticker {}.", importedTicker); + logger.debug("Importing ticker {}", importedTicker); importedTicker.setId(counter.incrementAndGet()); importedTickersRepository.save(importedTicker); }); } catch (IOException e) { - logger.error("Impossible to load imported tickers: {}.", e.getMessage()); + logger.error("Impossible to load imported tickers: {}", e.getMessage()); } }); - logger.info("{} tickers imported.", importedTickersRepository.count()); + logger.info("{} tickers imported", importedTickersRepository.count()); } /** @@ -388,12 +387,22 @@ private void loadImportedTickers() { public List getFilesToLoad() { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); try { - final Resource[] resources = resolver.getResources("classpath*:" + TICKERS_FILE_PREFIX + "*" + TICKERS_FILE_SUFFIX); + final Resource[] resources = resolver.getResources("classpath*:tickers-to-import*csv"); return Arrays.asList(resources); } catch (IOException e) { - logger.error("Impossible to load imported tickers: {}.", e.getMessage()); + logger.error("Impossible to load imported tickers: {}", e.getMessage()); } return Collections.emptyList(); } + /** + * Getter for positionService. + * + * @return positionService + */ + @Bean + public PositionService getPositionService() { + return positionService; + } + } diff --git a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/AccountMapper.java b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/AccountMapper.java index 84ea728f7..cce5fd141 100644 --- a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/AccountMapper.java +++ b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/AccountMapper.java @@ -17,7 +17,7 @@ /** * Account mapper. */ -@Mapper(uses = CurrencyMapper.class) +@Mapper(uses = {CurrencyMapper.class}) public interface AccountMapper { // ================================================================================================================= diff --git a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/TickerMapper.java b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/TickerMapper.java index 16b7e0863..5c5e5302b 100644 --- a/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/TickerMapper.java +++ b/spring-boot-starter/autoconfigure/src/main/java/tech/cassandre/trading/bot/util/mapper/TickerMapper.java @@ -9,7 +9,7 @@ /** * Ticker mapper. */ -@Mapper(uses = CurrencyMapper.class) +@Mapper(uses = {CurrencyMapper.class}) public interface TickerMapper { // ================================================================================================================= diff --git a/spring-boot-starter/autoconfigure/src/test/java/tech/cassandre/trading/bot/test/core/configuration/exchange/UnknownExchangeTest.java b/spring-boot-starter/autoconfigure/src/test/java/tech/cassandre/trading/bot/test/core/configuration/exchange/UnknownExchangeTest.java index 841b8898d..1a31c30e8 100644 --- a/spring-boot-starter/autoconfigure/src/test/java/tech/cassandre/trading/bot/test/core/configuration/exchange/UnknownExchangeTest.java +++ b/spring-boot-starter/autoconfigure/src/test/java/tech/cassandre/trading/bot/test/core/configuration/exchange/UnknownExchangeTest.java @@ -28,7 +28,7 @@ public void checkErrorMessages() { fail("Exception not raised"); } catch (Exception e) { final String message = ExceptionUtils.getRootCause(e).getMessage(); - assertTrue(message.contains("Impossible to find the exchange you requested: foo")); + assertTrue(message.contains("Impossible to find the exchange driver class you requested: foo")); } }