diff --git a/CHANGELOG.md b/CHANGELOG.md
index f151b7a74..609beb31c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,13 +6,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
*It is strongly advised to perform an update of your tentacles after updating OctoBot. (start.py tentacles --install --all)*
+## [1.0.2] - 2023-10-29
+### Added
+- [ChatGPT] Backtesting on supported configurations, full list on https://www.octobot.cloud/features/chatgpt-trading
+- [TradingView] Support cancel orders signal
+- [GridTrading] Optimise initial portfolio command to split funds in an optimized way before starting the trading mode
+- [DCATrading] Option not to cancel previously created orders
+- [DCATrading] Option to always create initial orders at startup when in evaluators mode
+- [Webhook] Support Ngrok custom domains
+### Updated
+- [ChatGPT] Default GPT Trading profile now uses the DCA trading mode
+- [TradingView] Revamped docs on https://www.octobot.cloud/guides/octobot-interfaces/tradingview
+- [DCATrading] Improved error messages
+- [WebInterface] Do not select duplicated profiles by default
+- [DataCollector] Make errors clearer
+- [Links] Migrate to https://www.octobot.cloud/
+### Fixed
+- [Kucoin] Issue regarding minimal order sizes
+- [Backtesting] Issue on orders that could being filled on the same candle they were created at
+- [DCATrading] Time based trigger issues
+- [Grid & Staggered orders] Fix order creation when close to spread
+
## [1.0.1] - 2023-09-28
### Fixed
- [Community] OctoBot account connection timeout error
## [1.0.0] - 2023-09-26
### Updated
-- [Community] Migrate to the updated octobot.cloud. Full details on https://blog.octobot.online/introducing-the-new-octobot-cloud
+- [Community] Migrate to the updated octobot.cloud. Full details on https://www.octobot.cloud/blog/introducing-the-new-octobot-cloud
- [Logs] Improve debug logs
### Fixed
- [GridTrading] Mirror order rebalance issues
diff --git a/README.md b/README.md
index b4ab88130..784ea586b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# OctoBot [1.0.1](https://octobot.click/gh-changelog)
+# OctoBot [1.0.2](https://octobot.click/gh-changelog)
[![PyPI](https://img.shields.io/pypi/v/OctoBot.svg?logo=pypi)](https://octobot.click/gh-pypi)
[![Downloads](https://pepy.tech/badge/octobot/month)](https://pepy.tech/project/octobot)
[![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot.svg?logo=docker)](https://octobot.click/gh-dockerhub)
@@ -14,15 +14,15 @@
[![YouTube](https://img.shields.io/youtube/channel/views/UC2YAaBeWY8y_Olqs79b_X8A?label=youtube&style=social)](https://octobot.click/gh-youtube)
-
+
-
+
## Launch of the new OctoBot cloud
-The OctoBot team is proud to announce the launch of the [new octobot.cloud](https://octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme)
+The OctoBot team is proud to announce the launch of the [new octobot.cloud](https://octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=launch_of_octobot_cloud)
where 90% of trading strategies can be automated **in a very easy way** and **for free**.
[![try octobot cloud now](https://img.shields.io/static/v1?label=Try%20the%20new%20OctoBot%20cloud&message=now&color=007bff&style=for-the-badge)](https://octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=production_annoucements&utm_content=readme_button)
@@ -31,31 +31,35 @@ We are looking forward to receiving your feedback on our new OctoBot based syste
## What is Octobot ?
-
+
+
+
-
+
+
+
-[Octobot](https://www.octobot.online/) is a powerful open-source cryptocurrency trading robot.
+[Octobot](https://www.octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_what_is_octobot) is a powerful open-source cryptocurrency trading robot.
OctoBot is highly customizable using its configuration and tentacles system.
-You can build your own bot using the infinite [configuration](https://www.octobot.info/configuration/profile-configuration) possibilities such as **technical analysis**, **social media processing** or even **external statistics management** like google trends.
+You can build your own bot using the infinite [configuration](https://www.octobot.cloud/guides/octobot-configuration/profile-configuration?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=configuration) possibilities such as **technical analysis**, **social media processing** or even **external statistics management** like google trends.
OctoBot is **AI ready**: Python being the main language for OctoBot, it's easy to integrate machine-learning libraries such as [Tensorflow](https://github.com/tensorflow/tensorflow) or any other libraries and take advantage of all the available data and create a very powerful trading strategy.
Octobot's main feature is **evolution**, you can :
- Share your configurations with other octobot users.
-- [Install](https://www.octobot.info/advanced_usage/tentacle-manager), [modify](https://developer.octobot.info/tentacles/tentacle-development) and even [create](https://developer.octobot.info/tentacles/tentacle-development) new tentacles to build your ideal cryptocurrency trading robot.
-- [Contribute](https://developer.octobot.info/installation/developer-installation/octobot-developer-installation) to improve OctoBot core repositories and tentacles.
+- [Install](https://www.octobot.cloud/guides/octobot-advanced-usage/tentacle-manager?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=install_tentacles), [modify](https://www.octobot.cloud/guides/octobot-tentacles-development/create-a-tentacle?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=modify_tentacles) and even [create](https://www.octobot.cloud/guides/octobot-tentacles-development/create-a-tentacle?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=create_tentacles) new tentacles to build your ideal cryptocurrency trading robot.
+- [Contribute](https://www.octobot.cloud/guides/octobot-developers-environment/setup-your-environment?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=contribute) to improve OctoBot core repositories and tentacles.
-Looking for more info ? Check out our Octobot guides at [octobot.info](https://www.octobot.info/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme)
+Looking for more info ? Check out our Octobot guides at [octobot.cloud/guides](https://www.octobot.cloud/guides/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=checkout_guides)
## Installation
OctoBot's installation is **very simple**, you can either:
- [Deploy your OctoBot on OctoBot Cloud](https://octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_deploy_on_cloud). With OctoBot cloud, experience hassle-free installation, updates, and maintenance - leave it all to us! Your robot will also benefit from cloud only features.
-- [Download and install](https://www.octobot.info/installation/local-installation) OctoBot on your computer or server and enjoy all features for free.
-- Install OctoBot [using docker](https://www.octobot.info/installation/local-installation#option-2-with-docker).
+- [Download and install](https://www.octobot.cloud/guides/octobot-installation/local?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_local_installation) OctoBot on your computer or server and enjoy all features for free.
+- Install OctoBot [using docker](https://www.octobot.cloud/guides/octobot-installation/with-docker?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_docker_installation).
Docker install in one line summary:
```
@@ -64,18 +68,18 @@ OctoBot's installation is **very simple**, you can either:
Your OctoBot will be accessible on [http://localhost](http://localhost).
## Exchanges
-[![Binance](../assets/binance-logo.png)](https://octobot.click/gh-binance)
-[![Okx](../assets/okex-logo.png)](https://octobot.click/gh-okex)
-[![GateIO](../assets/gateio-logo.png)](https://octobot.click/gh-gateio)
-[![Huobi](../assets/huobi-logo.png)](https://octobot.click/gh-huobi)
-[![Hollaex](../assets/hollaex-logo.png)](https://octobot.click/gh-hollaex)
-[![Coinbase](../assets/coinbasepro-logo.png)](https://pro.coinbase.com)
-[![Kucoin](../assets/kucoin-logo.png)](https://www.kucoin.com)
-[![Bitmex](../assets/bitmex-logo.png)](https://bitmex.com)
-[![Ascendex](../assets/ascendex-logo.png)](https://octobot.click/gh-ascendex)
+[![Binance supported exchange partnership](../assets/binance-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/binance?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=binance)
+[![Okx supported exchange partnership](../assets/okex-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/okx?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=okx)
+[![Kucoin supported exchange partnership](../assets/kucoin-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/kucoin?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=kucoin)
+[![Crypto.com supported exchange partnership](../assets/cryptocom-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/crypto-com?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=crypto-com)
+[![Huobi supported exchange partnership](../assets/huobi-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/huobi?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=huobi)
+[![Hollaex supported exchange partnership](../assets/hollaex-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/hollaex?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=hollaex)
+[![Coinbase supported exchange](../assets/coinbasepro-logo.png)](https://www.octobot.cloud/guides/octobot-supported-exchanges/coinbase?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=coinbase)
+[![GateIO supported exchange partnership](../assets/gateio-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/gateio?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=gateio)
+[![Ascendex supported exchange partnership](../assets/ascendex-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/ascendex?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=ascendex)
-Octobot supports many [exchanges](https://octobot.click/gh-exchanges) thanks to the [ccxt library](https://github.com/ccxt/ccxt).
-To activate trading on an exchange, just configure OctoBot with your API keys as described [on the exchange documentation](https://www.octobot.info/configuration/exchanges).
+Octobot supports many [exchanges](https://www.octobot.cloud/guides/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges) thanks to the [ccxt library](https://github.com/ccxt/ccxt).
+To activate trading on an exchange, just configure OctoBot with your API keys as described [on the exchange setup guides](https://www.octobot.cloud/guides/octobot-configuration/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_setup_guides).
### Paper trading
@@ -84,20 +88,20 @@ To trade on any exchange, just enable the exchange in your OctoBot. This you to
No exchange credential is required.
### Real trading
-To use your real exchange account with OctoBot, enter your exchange API keys as described [on the exchange documentation](https://octobot.click/gh-exchanges).
+To use your real exchange account with OctoBot, enter your exchange API keys as described [on the exchange guides](https://www.octobot.cloud/guides/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_guides).
## Testing trading strategies
OctoBot comes with its builtin backtesting engine which enables you to trade with simulated money using historical exchange data.
-[![Backtesting report](../assets/backtesting_report.jpg)](https://github.com/Drakkar-Software/OctoBot/blob/assets/backtesting_report.jpg)
+[![Backtesting report using grid trading on eth btc with 8 percent profit](../assets/backtesting_report.jpg)](https://github.com/Drakkar-Software/OctoBot/blob/assets/backtesting_report.jpg)
Backtesting will give you accurate insights on the past performance and behavior of strategies using OctoBot.
## Contribute from a browser IDE
Make changes and contribute to OctoBot in a single click with an **already setup and ready to code developer environment** using Gitpod !
-[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Drakkar-Software/OctoBot)
+[![Contribute from Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Drakkar-Software/OctoBot)
## Hardware requirements
- CPU : 1 Core / 1GHz
@@ -116,7 +120,7 @@ Please feel free to read the source code and understand the mechanism of this bo
## License
GNU General Public License v3.0 or later.
-See [LICENSE](https://octobot.click/gh-license) to see the full text.
+See [GPL-3.0 LICENSE](https://octobot.click/gh-license) to see the full text.
## Sponsors
diff --git a/octobot/__init__.py b/octobot/__init__.py
index ac3470fe0..99ea438c9 100644
--- a/octobot/__init__.py
+++ b/octobot/__init__.py
@@ -16,5 +16,5 @@
PROJECT_NAME = "OctoBot"
AUTHOR = "Drakkar-Software"
-VERSION = "1.0.1" # major.minor.revision
+VERSION = "1.0.2" # major.minor.revision
LONG_VERSION = f"{VERSION}"
diff --git a/octobot/api/__init__.py b/octobot/api/__init__.py
index 63b79e180..e8996efd5 100644
--- a/octobot/api/__init__.py
+++ b/octobot/api/__init__.py
@@ -34,6 +34,7 @@
initialize_and_run_independent_backtesting,
join_independent_backtesting,
initialize_independent_backtesting_config,
+ clear_backtesting_fetched_data,
stop_independent_backtesting,
join_independent_backtesting_stop,
get_independent_backtesting_report,
@@ -86,6 +87,7 @@
"initialize_and_run_independent_backtesting",
"join_independent_backtesting",
"initialize_independent_backtesting_config",
+ "clear_backtesting_fetched_data",
"stop_independent_backtesting",
"join_independent_backtesting_stop",
"get_independent_backtesting_report",
diff --git a/octobot/api/backtesting.py b/octobot/api/backtesting.py
index 2a99271b1..5dba18105 100644
--- a/octobot/api/backtesting.py
+++ b/octobot/api/backtesting.py
@@ -17,23 +17,25 @@
import octobot_backtesting.constants as constants
-def create_independent_backtesting(config,
- tentacles_setup_config,
- data_files,
- data_file_path=constants.BACKTESTING_FILE_PATH,
- join_backtesting_timeout=constants.BACKTESTING_DEFAULT_JOIN_TIMEOUT,
- run_on_common_part_only=True,
- start_timestamp=None,
- end_timestamp=None,
- enable_logs=True,
- stop_when_finished=False,
- name=None,
- enforce_total_databases_max_size_after_run=True,
- enable_storage=True,
- run_on_all_available_time_frames=False,
- backtesting_data=None,
- config_by_tentacle=None) \
- -> backtesting.IndependentBacktesting:
+def create_independent_backtesting(
+ config,
+ tentacles_setup_config,
+ data_files,
+ data_file_path=constants.BACKTESTING_FILE_PATH,
+ join_backtesting_timeout=constants.BACKTESTING_DEFAULT_JOIN_TIMEOUT,
+ run_on_common_part_only=True,
+ start_timestamp=None,
+ end_timestamp=None,
+ enable_logs=True,
+ stop_when_finished=False,
+ name=None,
+ enforce_total_databases_max_size_after_run=True,
+ enable_storage=True,
+ run_on_all_available_time_frames=False,
+ backtesting_data=None,
+ config_by_tentacle=None,
+ services_config=None
+) -> backtesting.IndependentBacktesting:
return backtesting.IndependentBacktesting(
config, tentacles_setup_config, data_files,
data_file_path,
@@ -48,7 +50,8 @@ def create_independent_backtesting(config,
enable_storage=enable_storage,
run_on_all_available_time_frames=run_on_all_available_time_frames,
backtesting_data=backtesting_data,
- config_by_tentacle=config_by_tentacle
+ config_by_tentacle=config_by_tentacle,
+ services_config=services_config,
)
@@ -64,6 +67,10 @@ async def initialize_independent_backtesting_config(independent_backtesting) ->
return await independent_backtesting.initialize_config()
+async def clear_backtesting_fetched_data(independent_backtesting):
+ await independent_backtesting.clear_fetched_data()
+
+
async def stop_independent_backtesting(independent_backtesting, memory_check=False, should_raise=False) -> None:
await independent_backtesting.stop(memory_check=memory_check, should_raise=should_raise)
diff --git a/octobot/backtesting/independent_backtesting.py b/octobot/backtesting/independent_backtesting.py
index 03dfa3ba7..e018f1475 100644
--- a/octobot/backtesting/independent_backtesting.py
+++ b/octobot/backtesting/independent_backtesting.py
@@ -43,22 +43,26 @@
class IndependentBacktesting:
- def __init__(self, config,
- tentacles_setup_config,
- backtesting_files,
- data_file_path=backtesting_constants.BACKTESTING_FILE_PATH,
- run_on_common_part_only=True,
- join_backtesting_timeout=backtesting_constants.BACKTESTING_DEFAULT_JOIN_TIMEOUT,
- start_timestamp=None,
- end_timestamp=None,
- enable_logs=True,
- stop_when_finished=False,
- name=None,
- enforce_total_databases_max_size_after_run=True,
- enable_storage=True,
- run_on_all_available_time_frames=False,
- backtesting_data=None,
- config_by_tentacle=None):
+ def __init__(
+ self,
+ config,
+ tentacles_setup_config,
+ backtesting_files,
+ data_file_path=backtesting_constants.BACKTESTING_FILE_PATH,
+ run_on_common_part_only=True,
+ join_backtesting_timeout=backtesting_constants.BACKTESTING_DEFAULT_JOIN_TIMEOUT,
+ start_timestamp=None,
+ end_timestamp=None,
+ enable_logs=True,
+ stop_when_finished=False,
+ name=None,
+ enforce_total_databases_max_size_after_run=True,
+ enable_storage=True,
+ run_on_all_available_time_frames=False,
+ backtesting_data=None,
+ config_by_tentacle=None,
+ services_config=None,
+ ):
self.octobot_origin_config = config
self.tentacles_setup_config = tentacles_setup_config
self.backtesting_config = {}
@@ -83,19 +87,22 @@ def __init__(self, config,
self.previous_handlers_log_level = commons_logging.get_logger_level_per_handler()
self.enforce_total_databases_max_size_after_run = enforce_total_databases_max_size_after_run
self.backtesting_data = backtesting_data
- self.octobot_backtesting = backtesting.OctoBotBacktesting(self.backtesting_config,
- self.tentacles_setup_config,
- self.symbols_to_create_exchange_classes,
- self.backtesting_files,
- run_on_common_part_only,
- start_timestamp=start_timestamp,
- end_timestamp=end_timestamp,
- enable_logs=self.enable_logs,
- enable_storage=enable_storage,
- run_on_all_available_time_frames=run_on_all_available_time_frames,
- backtesting_data=self.backtesting_data,
- name=name,
- config_by_tentacle=config_by_tentacle)
+ self.octobot_backtesting = backtesting.OctoBotBacktesting(
+ self.backtesting_config,
+ self.tentacles_setup_config,
+ self.symbols_to_create_exchange_classes,
+ self.backtesting_files,
+ run_on_common_part_only,
+ start_timestamp=start_timestamp,
+ end_timestamp=end_timestamp,
+ enable_logs=self.enable_logs,
+ enable_storage=enable_storage,
+ run_on_all_available_time_frames=run_on_all_available_time_frames,
+ backtesting_data=self.backtesting_data,
+ name=name,
+ config_by_tentacle=config_by_tentacle,
+ services_config=services_config,
+ )
async def initialize_and_run(self, log_errors=True):
try:
@@ -134,6 +141,9 @@ async def join_backtesting_updater(self, timeout=None):
if self.octobot_backtesting.backtesting is not None:
await asyncio.wait_for(self.octobot_backtesting.backtesting.time_updater.finished_event.wait(), timeout)
+ async def clear_fetched_data(self):
+ await self.octobot_backtesting.clear_fetched_data()
+
async def stop(self, memory_check=False, should_raise=False):
try:
if not self.stopped:
diff --git a/octobot/backtesting/octobot_backtesting.py b/octobot/backtesting/octobot_backtesting.py
index 343e2f35b..12eee546a 100644
--- a/octobot/backtesting/octobot_backtesting.py
+++ b/octobot/backtesting/octobot_backtesting.py
@@ -24,6 +24,8 @@
import octobot_commons.configuration as commons_configuration
import octobot_commons.databases as commons_databases
import octobot_commons.constants as commons_constants
+import octobot_commons.enums as commons_enums
+import octobot_commons.list_util as list_util
import octobot_commons.asyncio_tools as asyncio_tools
import octobot_backtesting.api as backtesting_api
@@ -48,20 +50,23 @@
class OctoBotBacktesting:
-
- def __init__(self, backtesting_config,
- tentacles_setup_config,
- symbols_to_create_exchange_classes,
- backtesting_files,
- run_on_common_part_only,
- start_timestamp=None,
- end_timestamp=None,
- enable_logs=True,
- enable_storage=True,
- run_on_all_available_time_frames=False,
- backtesting_data=None,
- name=None,
- config_by_tentacle=None):
+ def __init__(
+ self,
+ backtesting_config,
+ tentacles_setup_config,
+ symbols_to_create_exchange_classes,
+ backtesting_files,
+ run_on_common_part_only,
+ start_timestamp=None,
+ end_timestamp=None,
+ enable_logs=True,
+ enable_storage=True,
+ run_on_all_available_time_frames=False,
+ backtesting_data=None,
+ name=None,
+ config_by_tentacle=None,
+ services_config=None,
+ ):
self.logger = commons_logging.get_logger(self.__class__.__name__)
self.backtesting_config = backtesting_config
self.tentacles_setup_config = tentacles_setup_config
@@ -91,6 +96,8 @@ def __init__(self, backtesting_config,
self.enable_storage = enable_storage
self.run_on_all_available_time_frames = run_on_all_available_time_frames
self._has_started = False
+ self.has_fetched_data = False
+ self.services_config = services_config
async def initialize_and_run(self):
if not constants.ENABLE_BACKTESTING:
@@ -110,9 +117,13 @@ async def initialize_and_run(self):
await self._init_backtesting()
await self._init_evaluators()
await self._init_service_feeds()
+ min_timestamp, max_timestamp = await self._configure_backtesting_time_window()
await self._init_exchanges()
self._ensure_limits()
await self._create_evaluators()
+ await self._fetch_backtesting_extra_data_if_any(
+ min_timestamp, max_timestamp
+ )
await self._create_service_feeds()
await backtesting_api.start_backtesting(self.backtesting)
if logger.BOT_CHANNEL_LOGGER is not None and self.enable_logs:
@@ -275,7 +286,9 @@ async def _init_matrix(self):
self.matrix_id = evaluator_api.create_matrix()
async def _init_evaluators(self):
- await evaluator_api.initialize_evaluators(self.backtesting_config, self.tentacles_setup_config)
+ await evaluator_api.initialize_evaluators(
+ self.backtesting_config, self.tentacles_setup_config, config_by_evaluator=self.config_by_tentacle
+ )
if (not self.backtesting_config[commons_constants.CONFIG_TIME_FRAME]) and \
evaluator_constants.CONFIG_FORCED_TIME_FRAME in self.backtesting_config:
self.backtesting_config[commons_constants.CONFIG_TIME_FRAME] = self.backtesting_config[
@@ -341,16 +354,61 @@ async def _init_backtesting(self):
for tf in self.backtesting.importers[0].time_frames
]
- async def _init_exchanges(self):
+ async def _configure_backtesting_time_window(self):
# modify_backtesting_channels before creating exchanges as they require the current backtesting time to
# initialize
- await backtesting_api.adapt_backtesting_channels(self.backtesting,
- self.backtesting_config,
- importers.ExchangeDataImporter,
- run_on_common_part_only=self.run_on_common_part_only,
- start_timestamp=self.start_timestamp,
- end_timestamp=self.end_timestamp)
+ min_timestamp, max_timestamp = await backtesting_api.adapt_backtesting_channels(
+ self.backtesting,
+ self.backtesting_config,
+ importers.ExchangeDataImporter,
+ run_on_common_part_only=self.run_on_common_part_only,
+ start_timestamp=self.start_timestamp,
+ end_timestamp=self.end_timestamp
+ )
+ return min_timestamp, max_timestamp
+ async def _fetch_backtesting_extra_data_if_any(
+ self, min_timestamp: float, max_timestamp: float
+ ):
+ if not self.evaluators:
+ return
+ handled_classes = set()
+ coros = []
+ for evaluator in list_util.flatten_list(self.evaluators):
+ if evaluator and evaluator.get_name() not in handled_classes:
+ if evaluator.get_signals_history_type() == commons_enums.SignalHistoryTypes.GPT:
+ coros.append(self._fetch_gpt_history(evaluator, min_timestamp, max_timestamp))
+ handled_classes.add(evaluator.get_name())
+ if coros:
+ self.has_fetched_data = True
+ await asyncio.gather(*coros)
+
+ async def _fetch_gpt_history(self, evaluator, min_timestamp: float, max_timestamp: float):
+ # prevent circular import
+ import tentacles.Services.Services_bases.gpt_service as gpt_service
+ service = await service_api.get_service(gpt_service.GPTService, True, self.services_config)
+ version = evaluator.get_version()
+ for exchange_id in self.exchange_manager_ids:
+ exchange_configuration = trading_api.get_exchange_configuration_from_exchange_id(exchange_id)
+ exchange_name = trading_api.get_exchange_name(
+ trading_api.get_exchange_manager_from_exchange_id(exchange_id)
+ )
+ await service.fetch_gpt_history(
+ exchange_name,
+ [str(symbol) for symbol in self.symbols_to_create_exchange_classes.get(exchange_name, [])],
+ exchange_configuration.available_required_time_frames,
+ version,
+ min_timestamp,
+ max_timestamp
+ )
+
+ async def clear_fetched_data(self):
+ if self.has_fetched_data:
+ # prevent circular import
+ import tentacles.Services.Services_bases.gpt_service as gpt_service
+ (await service_api.get_service(gpt_service.GPTService, True, self.services_config)).clear_signal_history()
+
+ async def _init_exchanges(self):
for exchange_class_string in self.symbols_to_create_exchange_classes.keys():
is_future = self.exchange_type_by_exchange[exchange_class_string] == \
commons_constants.CONFIG_EXCHANGE_FUTURE
diff --git a/octobot/community/authentication.py b/octobot/community/authentication.py
index 5c20eaaba..f3745aee6 100644
--- a/octobot/community/authentication.py
+++ b/octobot/community/authentication.py
@@ -64,9 +64,11 @@ class CommunityAuthentication(authentication.Authenticator):
SESSION_HEADER = "X-Session"
GQL_AUTHORIZATION_HEADER = "Authorization"
- def __init__(self, feed_url, config=None):
+ def __init__(self, feed_url, config=None, backend_url=None, backend_key=None):
super().__init__()
self.feed_url = feed_url
+ self.backend_url = backend_url or identifiers_provider.IdentifiersProvider.BACKEND_URL
+ self.backend_key = backend_key or identifiers_provider.IdentifiersProvider.BACKEND_KEY
self.configuration_storage = supabase_backend.SyncConfigurationStorage(config)
self.supabase_client = self._create_client()
self.user_account = community_user_account.CommunityUserAccount()
@@ -80,10 +82,11 @@ def __init__(self, feed_url, config=None):
self._fetch_account_task = None
@staticmethod
- def create(configuration: commons_configuration.Configuration):
+ def create(configuration: commons_configuration.Configuration, **kwargs):
return CommunityAuthentication.instance(
None,
config=configuration,
+ **kwargs,
)
def update(self, configuration: commons_configuration.Configuration):
@@ -134,6 +137,19 @@ async def get_deployment_url(self):
)
return self.user_account.get_bot_deployment_url(deployment_url_data)
+ async def get_gpt_signal(
+ self, exchange: str, symbol: str, time_frame: commons_enums.TimeFrames, candle_open_time: float, version: str
+ ) -> str:
+ return await self.supabase_client.fetch_gpt_signal(exchange, symbol, time_frame, candle_open_time, version)
+
+ async def get_gpt_signals_history(
+ self, exchange: str, symbol: str, time_frame: commons_enums.TimeFrames,
+ first_open_time: float, last_open_time: float, version: str
+ ) -> dict:
+ return await self.supabase_client.fetch_gpt_signals_history(
+ exchange, symbol, time_frame, first_open_time, last_open_time, version
+ )
+
def get_is_signal_receiver(self):
if self._community_feed is None:
return False
@@ -165,8 +181,8 @@ def _supports_mock():
def _create_client(self):
return supabase_backend.CommunitySupabaseClient(
- identifiers_provider.IdentifiersProvider.BACKEND_URL,
- identifiers_provider.IdentifiersProvider.BACKEND_KEY,
+ self.backend_url,
+ self.backend_key,
self.configuration_storage
)
@@ -540,6 +556,11 @@ async def update_bot_config_and_stats(self, profitability):
formatted_portfolio = formatters.format_portfolio_with_profitability(profitability)
if self.user_account.get_selected_bot_current_portfolio_id() is None:
await self.refresh_selected_bot()
- formatted_portfolio[backend_enums.PortfolioKeys.ID.value] = \
- self.user_account.get_selected_bot_current_portfolio_id()
- await self.supabase_client.update_portfolio(formatted_portfolio)
+ if self.user_account.get_selected_bot_current_portfolio_id() is None:
+ self.logger.debug(
+ f"Skipping portfolio update: current bot {self.user_account.bot_id} has no current portfolio_id"
+ )
+ else:
+ formatted_portfolio[backend_enums.PortfolioKeys.ID.value] = \
+ self.user_account.get_selected_bot_current_portfolio_id()
+ await self.supabase_client.update_portfolio(formatted_portfolio)
diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py
index fa3008247..d2024af69 100644
--- a/octobot/community/supabase_backend/community_supabase_client.py
+++ b/octobot/community/supabase_backend/community_supabase_client.py
@@ -27,6 +27,8 @@
import octobot_commons.authentication as authentication
import octobot_commons.logging as commons_logging
import octobot_commons.profiles as commons_profiles
+import octobot_commons.enums as commons_enums
+import octobot_commons.constants as commons_constants
import octobot.constants as constants
import octobot.community.errors as errors
import octobot.community.supabase_backend.enums as enums
@@ -57,6 +59,7 @@ def __init__(
self.event_loop = None
super().__init__(supabase_url, supabase_key, options=options)
self.is_admin = False
+ self.production_anon_client = None
async def sign_in(self, email: str, password: str) -> None:
try:
@@ -322,6 +325,153 @@ async def upsert_portfolio_history(self, portfolio_histories) -> list:
on_conflict=f"{enums.PortfolioHistoryKeys.TIME.value},{enums.PortfolioHistoryKeys.PORTFOLIO_ID.value}"
).execute()).data
+ async def fetch_candles_history_range(
+ self, exchange: str, symbol: str, time_frame: commons_enums.TimeFrames
+ ) -> (typing.Union[float, None], typing.Union[float, None]):
+ min_max = json.loads(
+ (await self.postgres_functions().invoke(
+ "get_ohlcv_range",
+ {"body": {
+ "exchange_internal_name": exchange,
+ "symbol": symbol,
+ "time_frame": time_frame.value,
+ }}
+ ))["data"]
+ )[0]
+ return (
+ self.get_parsed_time(min_max["min_value"]).timestamp() if min_max["min_value"] else None,
+ self.get_parsed_time(min_max["max_value"]).timestamp() if min_max["max_value"] else None,
+ )
+
+ async def fetch_candles_history(
+ self, exchange: str, symbol: str, time_frame: commons_enums.TimeFrames,
+ first_open_time: float, last_open_time: float
+ ) -> list:
+ historical_candles = await self._fetch_paginated_history(
+ await self.get_production_anon_client(),
+ "temp_ohlcv_history",
+ "timestamp, open, high, low, close, volume",
+ {
+ "exchange_internal_name": exchange,
+ "symbol": symbol,
+ "time_frame": time_frame.value,
+ },
+ commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MINUTE_TO_SECONDS,
+ first_open_time,
+ last_open_time
+ )
+ return self._format_ohlcvs(historical_candles)
+
+ async def fetch_gpt_signal(
+ self, exchange: str, symbol: str, time_frame: commons_enums.TimeFrames, timestamp: float, version: str
+ ) -> str:
+ signals = (await (await self.get_production_anon_client()).table("temp_chatgpt_signals").select("signal").match(
+ {
+ "timestamp": self.get_formatted_time(timestamp),
+ "exchange_internal_name": exchange,
+ "symbol": symbol,
+ "time_frame": time_frame.value,
+ "metadata->>version": version,
+ },
+ ).execute()).data
+ if signals:
+ return signals[0]["signal"]["content"]
+ return ""
+
+ async def fetch_gpt_signals_history(
+ self, exchange: str, symbol: str, time_frame: commons_enums.TimeFrames,
+ first_open_time: float, last_open_time: float, version: str
+ ) -> dict:
+ historical_signals = await self._fetch_paginated_history(
+ await self.get_production_anon_client(),
+ "temp_chatgpt_signals",
+ "timestamp, signal",
+ {
+ "exchange_internal_name": exchange,
+ "symbol": symbol,
+ "time_frame": time_frame.value,
+ "metadata->>version": version,
+ },
+ commons_enums.TimeFramesMinutes[time_frame] * commons_constants.MINUTE_TO_SECONDS,
+ first_open_time,
+ last_open_time
+ )
+ return self._format_gpt_signals(historical_signals)
+
+ async def get_production_anon_client(self):
+ if self.production_anon_client is None:
+ self.production_anon_client = await self.init_other_postgrest_client(
+ supabase_url=constants.COMMUNITY_PRODUCTION_BACKEND_URL,
+ supabase_key=constants.COMMUNITY_PRODUCTION_BACKEND_KEY,
+ )
+ return self.production_anon_client
+
+ async def _fetch_paginated_history(
+ self, client, table_name: str, select: str, matcher: dict,
+ time_interval: float, first_open_time: float, last_open_time: float
+ ) -> list:
+ total_elements_count = (last_open_time - first_open_time) // time_interval
+ offset = 0
+ max_size = 0
+ total_elements = []
+ max_requests_count = 100
+ request_count = 0
+ while request_count < max_requests_count:
+ request = (
+ client.table(table_name).select(select)
+ .match(matcher).gte(
+ "timestamp", self.get_formatted_time(first_open_time)
+ ).lte(
+ "timestamp", self.get_formatted_time(last_open_time)
+ ).order(
+ "timestamp", desc=False
+ )
+ )
+ if offset:
+ request = request.range(offset, offset+max_size)
+ fetched_elements = (await request.execute()).data
+ total_elements += fetched_elements
+ if len(fetched_elements) < max_size or (max_size == 0 and len(fetched_elements) == total_elements_count):
+ # fetched everything
+ break
+ offset += len(fetched_elements)
+ if max_size == 0:
+ max_size = offset
+ request_count += 1
+
+ if request_count == max_requests_count:
+ commons_logging.get_logger(self.__class__.__name__).info(
+ f"paginated fetch error on {table_name} with matcher: {matcher}: "
+ f"too many requests ({request_count}), fetched: {len(total_elements)} elements"
+ )
+ return total_elements
+
+ def _format_gpt_signals(self, signals: list):
+ return {
+ self.get_parsed_time(signal["timestamp"]).timestamp(): signal["signal"]["content"]
+ for signal in signals
+ }
+
+ def _format_ohlcvs(self, ohlcvs: list):
+ # uses PriceIndexes order
+ # IND_PRICE_TIME = 0
+ # IND_PRICE_OPEN = 1
+ # IND_PRICE_HIGH = 2
+ # IND_PRICE_LOW = 3
+ # IND_PRICE_CLOSE = 4
+ # IND_PRICE_VOL = 5
+ return [
+ [
+ int(self.get_parsed_time(ohlcv["timestamp"]).timestamp()),
+ ohlcv["open"],
+ ohlcv["high"],
+ ohlcv["low"],
+ ohlcv["close"],
+ ohlcv["volume"],
+ ]
+ for ohlcv in ohlcvs
+ ]
+
async def get_asset_id(self, bucket_id: str, asset_name: str) -> str:
"""
Not implemented for authenticated users
@@ -384,3 +534,13 @@ def get_parsed_time(str_time: str) -> datetime.datetime:
async def _get_user(self) -> gotrue.User:
return self.auth.get_user().user
+
+ async def close(self):
+ await super().close()
+ if self.production_anon_client is not None:
+ try:
+ await self.production_anon_client.aclose()
+ except RuntimeError:
+ # happens when the event loop is closed already
+ pass
+ self.production_anon_client = None
diff --git a/octobot/community/supabase_backend/supabase_client.py b/octobot/community/supabase_backend/supabase_client.py
index 777f48f0b..37a38ab3b 100644
--- a/octobot/community/supabase_backend/supabase_client.py
+++ b/octobot/community/supabase_backend/supabase_client.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see .
import contextlib
+import copy
import typing
import storage3
import storage3.constants
@@ -61,8 +62,8 @@ def _init_postgrest_client(
rest_url: str,
supabase_key: str,
headers: typing.Dict[str, str],
- schema: str,
timeout, # skip typing to avoid httpx import
+ schema: str = "public",
) -> postgrest.AsyncPostgrestClient:
"""Private helper for creating an instance of the Postgrest client."""
# Override to use postgrest.AsyncPostgrestClient and allow async requests
@@ -96,21 +97,33 @@ def table(self, table_name: str) -> postgrest.AsyncRequestBuilder: # typing ove
return self.from_(table_name)
@contextlib.asynccontextmanager
- async def other_postgres_client(self, schema):
- other_postgres: postgrest.AsyncPostgrestClient = None
+ async def other_postgres_client(self, supabase_url: str = None, supabase_key: str = None, schema: str = "public"):
+ other_postgres = None
try:
- other_postgres = AuthenticatedAsyncSupabaseClient._init_postgrest_client(
- rest_url=self.rest_url,
- supabase_key=self.supabase_key,
- headers=self.options.headers,
- schema=schema,
- timeout=self.options.postgrest_client_timeout,
+ other_postgres = await self.init_other_postgrest_client(
+ supabase_url=supabase_url, supabase_key=supabase_key, schema=schema
)
yield other_postgres
finally:
if other_postgres is not None:
await other_postgres.aclose()
+ async def init_other_postgrest_client(
+ self, supabase_url: str = None, supabase_key: str = None, schema: str = "public"
+ ) -> postgrest.AsyncPostgrestClient:
+ supabase_key = supabase_key or self.supabase_key
+ headers = self.options.headers
+ if supabase_key != self.supabase_key:
+ headers = copy.deepcopy(postgrest.constants.DEFAULT_POSTGREST_CLIENT_HEADERS)
+ headers.update(self._format_auth_headers(supabase_key, supabase_key))
+ return AuthenticatedAsyncSupabaseClient._init_postgrest_client(
+ rest_url=f"{supabase_url}/rest/v1" if supabase_url else self.rest_url,
+ supabase_key=supabase_key,
+ headers=headers,
+ timeout=self.options.postgrest_client_timeout,
+ schema=schema,
+ )
+
async def close(self):
# timer has to be stopped, there is no public stop api
if self.auth._refresh_token_timer:
@@ -148,9 +161,12 @@ def _use_auth_session(self, event: gotrue.AuthChangeEvent, _):
def _get_auth_headers(self):
"""Helper method to get auth headers."""
# What's the corresponding method to get the token
+ return self._format_auth_headers(self.supabase_key, self._get_auth_key())
+
+ def _format_auth_headers(self, supabase_key, auth_token):
return {
- "apiKey": self.supabase_key,
- "Authorization": f"Bearer {self._get_auth_key()}",
+ "apiKey": supabase_key,
+ "Authorization": f"Bearer {auth_token}",
}
def _get_auth_key(self):
diff --git a/octobot/configuration_manager.py b/octobot/configuration_manager.py
index e1e2f9c5e..e4f79926b 100644
--- a/octobot/configuration_manager.py
+++ b/octobot/configuration_manager.py
@@ -154,7 +154,7 @@ def get_default_tentacles_url(version=None):
if constants.TENTACLES_REQUIRED_VERSION else constants.LONG_VERSION
return os.getenv(
constants.ENV_TENTACLES_URL,
- f"{constants.OCTOBOT_ONLINE}/"
+ f"{constants.STATIC_OCTOBOT_ONLINE}/"
f"{tentacles_repository}/"
f"{os.getenv(constants.ENV_TENTACLES_PACKAGES_SOURCE, constants.OFFICIALS)}/"
f"{os.getenv(constants.ENV_TENTACLES_PACKAGES_TYPE, constants.TENTACLE_PACKAGES)}/"
@@ -168,7 +168,7 @@ def get_default_tentacles_url(version=None):
def get_default_compiled_tentacles_url():
return os.getenv(
constants.ENV_COMPILED_TENTACLES_URL,
- f"{constants.OCTOBOT_ONLINE}/{constants.TENTACLES_REPOSITORY}/"
+ f"{constants.STATIC_OCTOBOT_ONLINE}/{constants.TENTACLES_REPOSITORY}/"
f"{os.getenv(constants.ENV_TENTACLES_PACKAGES_SOURCE, constants.OFFICIALS)}/"
f"{os.getenv(constants.ENV_COMPILED_TENTACLES_PACKAGES_TYPE, constants.TENTACLE_PACKAGES)}/"
f"{os.getenv(constants.ENV_COMPILED_TENTACLES_CATEGORY, constants.COMPILED_TENTACLE_CATEGORY)}/"
diff --git a/octobot/constants.py b/octobot/constants.py
index 80516dcfc..1f739e2e4 100644
--- a/octobot/constants.py
+++ b/octobot/constants.py
@@ -35,12 +35,12 @@
dotenv.load_dotenv(os.path.join(DOTENV_PATH, ".env"), verbose=False)
# OctoBot urls
-OCTOBOT_WEBSITE_URL = os.getenv("OCTOBOT_ONLINE_URL", "https://www.octobot.online")
-OCTOBOT_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.info")
-EXCHANGES_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://exchanges.octobot.info/")
-DEVELOPER_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://developer.octobot.info/")
-OCTOBOT_ONLINE = os.getenv("TENTACLES_OCTOBOT_ONLINE_URL", "https://static.octobot.online")
-OCTOBOT_FEEDBACK = os.getenv("FEEDBACK_OCTOBOT_ONLINE_URL", "https://feedback.octobot.online/")
+OCTOBOT_WEBSITE_URL = os.getenv("OCTOBOT_ONLINE_URL", "https://www.octobot.cloud")
+OCTOBOT_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/guides")
+EXCHANGES_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/guides/exchanges/")
+DEVELOPER_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/guides/developers/")
+STATIC_OCTOBOT_ONLINE = os.getenv("TENTACLES_OCTOBOT_ONLINE_URL", "https://static.octobot.online")
+OCTOBOT_FEEDBACK = os.getenv("FEEDBACK_OCTOBOT_ONLINE_URL", "https://feedback.octobot.cloud/")
TENTACLES_REPOSITORY = "tentacles"
BETA_TENTACLES_REPOSITORY = "dev-tentacles"
OFFICIALS = "officials"
@@ -63,11 +63,10 @@
OCTOBOT_COMMUNITY_LANDING_URL = os.getenv("COMMUNITY_SERVER_URL", "https://octobot.cloud")
OCTOBOT_COMMUNITY_URL = os.getenv("COMMUNITY_SERVER_URL", "https://app.octobot.cloud")
OCTOBOT_COMMUNITY_RECOVER_PASSWORD_URL = OCTOBOT_COMMUNITY_URL
-# todo use real production db
+# default env
COMMUNITY_BACKEND_URL = os.getenv("COMMUNITY_BACKEND_URL", "https://nwhpvrguwcihhizrnyoe.supabase.co")
COMMUNITY_BACKEND_KEY = os.getenv("COMMUNITY_BACKEND_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im53aHB2cmd1d2NpaGhpenJueW9lIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTU2NDQxMDcsImV4cCI6MjAxMTIyMDEwN30.AILcgv0l6hl_0IUEPlWh1wiu9RIpgrkGZGERM5uXftE")
-
# staging env SHOULD ONLY BE USED THROUGH CommunityIdentifiersProvider
STAGING_OCTOBOT_COMMUNITY_LANDING_URL = os.getenv("COMMUNITY_SERVER_URL", "https://beta.octobot.cloud")
STAGING_OCTOBOT_COMMUNITY_URL = os.getenv("COMMUNITY_SERVER_URL", "https://app-beta.octobot.cloud/")
@@ -75,7 +74,9 @@
STAGING_COMMUNITY_BACKEND_URL = os.getenv("COMMUNITY_BACKEND_URL", "https://wmfkgvgzokyzhvxowbyg.supabase.co")
STAGING_COMMUNITY_BACKEND_KEY = os.getenv("COMMUNITY_BACKEND_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndtZmtndmd6b2t5emh2eG93YnlnIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTE0NDA1MTEsImV4cCI6MjAwNzAxNjUxMX0.YZQl7LYgvnzO_Jizs0UKfPEaqPoV2EwhjunH8gime8o")
-
+# production env, ignored by CommunityIdentifiersProvider
+COMMUNITY_PRODUCTION_BACKEND_URL = os.getenv("COMMUNITY_PRODUCTION_BACKEND_URL", COMMUNITY_BACKEND_URL)
+COMMUNITY_PRODUCTION_BACKEND_KEY = os.getenv("COMMUNITY_PRODUCTION_BACKEND_KEY", COMMUNITY_BACKEND_KEY)
CONFIG_COMMUNITY = "community"
CONFIG_COMMUNITY_BOT_ID = "bot_id"
diff --git a/requirements.txt b/requirements.txt
index 47ee41c5b..cf8199313 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,10 @@
# Drakkar-Software requirements
-OctoBot-Commons==1.9.28
-OctoBot-Trading==2.4.35
-OctoBot-Evaluators==1.9.1
+OctoBot-Commons==1.9.29
+OctoBot-Trading==2.4.37
+OctoBot-Evaluators==1.9.2
OctoBot-Tentacles-Manager==2.9.5
-OctoBot-Services==1.6.4
-OctoBot-Backtesting==1.9.4
+OctoBot-Services==1.6.5
+OctoBot-Backtesting==1.9.5
Async-Channel==2.2.1
trading-backend==1.2.9