From 0dc86efb931f64153e875f96f4a15c0d721d559c Mon Sep 17 00:00:00 2001 From: Tom JEANNESSON Date: Wed, 13 Mar 2024 19:31:59 +0100 Subject: [PATCH 1/2] fix(celery): now works asynchronously --- Makefile | 4 +- .../core/management/commands/create_dca.py | 2 +- django_napse/core/models/bots/architecture.py | 46 ++++++---- .../models/bots/architectures/single_pair.py | 4 +- django_napse/core/models/bots/bot.py | 16 ++-- django_napse/core/models/bots/controller.py | 88 +++++++++++++------ .../bots/implementations/dca/strategy.py | 8 +- .../implementations/turbo_dca/strategy.py | 2 +- django_napse/core/models/bots/plugins/sbv.py | 2 +- .../core/models/connections/connection.py | 8 +- .../core/models/modifications/architecture.py | 2 +- .../core/models/orders/managers/order.py | 3 - django_napse/core/models/orders/order.py | 13 ++- django_napse/core/models/wallets/currency.py | 9 -- django_napse/core/models/wallets/wallet.py | 3 +- django_napse/core/pydantic/__init__.py | 1 + django_napse/core/pydantic/candle.py | 14 +++ django_napse/core/pydantic/currency.py | 9 ++ django_napse/core/settings.py | 13 +++ django_napse/core/tasks/base_task.py | 56 ++++++++---- django_napse/core/tasks/candle_collector.py | 29 ++++-- django_napse/core/tasks/controller_update.py | 6 +- django_napse/core/tasks/history_update.py | 4 +- .../core/tasks/order_process_executor.py | 23 +++-- .../models/simulations/simulation_queue.py | 9 +- .../simulations/tasks/dataset_queue.py | 2 +- .../simulations/tasks/simulation_queue.py | 2 +- django_napse/utils/usefull_functions.py | 4 +- 28 files changed, 256 insertions(+), 126 deletions(-) create mode 100644 django_napse/core/pydantic/__init__.py create mode 100644 django_napse/core/pydantic/candle.py create mode 100644 django_napse/core/pydantic/currency.py diff --git a/Makefile b/Makefile index 6a9423bb..0d115cb5 100644 --- a/Makefile +++ b/Makefile @@ -32,10 +32,10 @@ up: make makemigrations && make migrate && make runserver clean: - rm tests/test_app/db.sqlite3 + rm tests/test_app/db.sqlite3 && rm dump.rdb celery: - source .venv/bin/activate && watchfiles --filter python celery.__main__.main --args "-A tests.test_app worker --beat -l INFO" --verbosity info + source .venv/bin/activate && watchfiles --filter python celery.__main__.main --args "-A tests.test_app worker --beat --scheduler django -l INFO" --verbosity info shell: source .venv/bin/activate && python tests/test_app/manage.py shell diff --git a/django_napse/core/management/commands/create_dca.py b/django_napse/core/management/commands/create_dca.py index 17a791e5..19d65277 100644 --- a/django_napse/core/management/commands/create_dca.py +++ b/django_napse/core/management/commands/create_dca.py @@ -14,7 +14,7 @@ def add_arguments(self, parser): # noqa def handle(self, *args, **options): # noqa exchange_account = ExchangeAccount.objects.first() space = Space.objects.first() - config = DCABotConfig.objects.create(space=space, settings={"timeframe": timedelta(hours=1)}) + config = DCABotConfig.objects.create(space=space, settings={"timeframe": timedelta(minutes=5)}) controller = Controller.get( exchange_account=exchange_account, base="BTC", diff --git a/django_napse/core/models/bots/architecture.py b/django_napse/core/models/bots/architecture.py index fd23d184..449b6141 100644 --- a/django_napse/core/models/bots/architecture.py +++ b/django_napse/core/models/bots/architecture.py @@ -2,18 +2,30 @@ from django.db import models +from django_napse.core.models.bots.controller import Controller from django_napse.core.models.bots.managers import ArchitectureManager -from django_napse.core.models.wallets.currency import CurrencyPydantic +from django_napse.core.pydantic.candle import CandlePydantic +from django_napse.core.pydantic.currency import CurrencyPydantic from django_napse.utils.constants import ORDER_LEEWAY_PERCENTAGE, PLUGIN_CATEGORIES, SIDES from django_napse.utils.errors.orders import OrderError from django_napse.utils.findable_class import FindableClass if TYPE_CHECKING: - from django_napse.core.models.bots.controller import Controller from django_napse.core.models.bots.plugin import Plugin from django_napse.core.models.bots.strategy import Strategy from django_napse.core.models.connections.connection import Connection - +DataType = dict[ + Literal[ + "candles", + "extras", + ], + Union[ + dict[ + Controller, + dict[Literal["current", "latest"], Union[CandlePydantic]], + ] + ], +] DBDataType = dict[ Literal[ "strategy", @@ -91,30 +103,30 @@ def accepted_investment_tickers(self): # pragma: no cover # noqa: ANN201, D102 ) raise NotImplementedError(error_msg) - def get_extras(self): # noqa + def get_extras(self): return {} - def skip(self, data: dict) -> bool: # noqa + def skip(self, data: dict) -> bool: return False - def strategy_modifications(self, order: dict, data) -> list[dict]: # noqa + def strategy_modifications(self, order: dict, data) -> list[dict]: # noqa: ARG002 """Return modifications.""" return [] - def connection_modifications(self, order: dict, data) -> list[dict]: # noqa + def connection_modifications(self, order: dict, data) -> list[dict]: # noqa: ARG002 """Return modifications.""" return [] - def architecture_modifications(self, order: dict, data) -> list[dict]: # noqa + def architecture_modifications(self, order: dict, data) -> list[dict]: # noqa: ARG002 """Return modifications.""" return [] - def prepare_data(self) -> dict[str, dict[str, any]]: - """Return candles data.""" - return { - "candles": {controller: self.get_candles(controller) for controller in self.controllers_dict().values()}, - "extras": self.get_extras(), - } + # def prepare_data(self) -> dict[str, dict[str, any]]: + # """Return candles data.""" + # return { + # "candles": {controller: self.get_candles(controller) for controller in self.controllers_dict().values()}, + # "extras": self.get_extras(), + # } def prepare_db_data( self, @@ -130,13 +142,14 @@ def prepare_db_data( "plugins": {category: self.strategy.plugins.filter(category=category) for category in PLUGIN_CATEGORIES}, } - def _get_orders(self, data: dict, no_db_data: DBDataType = None) -> list[dict]: - data = data or self.prepare_data() + def get_orders__no_db(self, data: DataType, no_db_data: DBDataType = None) -> list[dict]: + # data = data or self.prepare_data() no_db_data = no_db_data or self.prepare_db_data() strategy = no_db_data["strategy"] connections = no_db_data["connections"] architecture = no_db_data["architecture"] all_orders = [] + for connection in connections: new_data = {**data, **no_db_data, "connection": connection} if architecture.skip(data=new_data): @@ -151,6 +164,7 @@ def _get_orders(self, data: dict, no_db_data: DBDataType = None) -> list[dict]: order["ConnectionModifications"] += architecture.connection_modifications(order=order, data=new_data) order["ArchitectureModifications"] += architecture.architecture_modifications(order=order, data=new_data) required_amount = {} + for order in orders: required_amount[order["asked_for_ticker"]] = required_amount.get(order["asked_for_ticker"], 0) + order["asked_for_amount"] diff --git a/django_napse/core/models/bots/architectures/single_pair.py b/django_napse/core/models/bots/architectures/single_pair.py index fe682017..7e332b4b 100644 --- a/django_napse/core/models/bots/architectures/single_pair.py +++ b/django_napse/core/models/bots/architectures/single_pair.py @@ -31,7 +31,7 @@ def controllers_dict(self): return {"main": self.controller} def skip(self, data: dict) -> bool: - if data["candles"][data["controllers"]["main"]]["current"]["open_time"] < self.variable_last_candle_date + timedelta( + if data["candles"][data["controllers"]["main"]]["current"].open_time < self.variable_last_candle_date + timedelta( milliseconds=interval_to_milliseconds(data["controllers"]["main"].interval), ): return True @@ -41,7 +41,7 @@ def architecture_modifications(self, order: dict, data: dict): return [ { "key": "last_candle_date", - "value": str(data["candles"][data["controllers"]["main"]]["current"]["open_time"]), + "value": str(data["candles"][data["controllers"]["main"]]["current"].open_time), "target_type": "datetime", "ignore_failed_order": True, }, diff --git a/django_napse/core/models/bots/bot.py b/django_napse/core/models/bots/bot.py index 24b30ab7..96bf295b 100644 --- a/django_napse/core/models/bots/bot.py +++ b/django_napse/core/models/bots/bot.py @@ -11,6 +11,8 @@ from django_napse.utils.errors import BotError if TYPE_CHECKING: + from django_napse.core.models.bots.architecture import Architecture, DataType, DBDataType + from django_napse.core.models.bots.strategy import Strategy from django_napse.core.models.wallets.space_simulation_wallet import SpaceSimulationWallet from django_napse.core.models.wallets.space_wallet import SpaceWallet @@ -23,7 +25,7 @@ class Bot(models.Model): created_at = models.DateTimeField(auto_now_add=True) active = models.BooleanField(default=True) - strategy = models.OneToOneField("Strategy", on_delete=models.CASCADE, related_name="bot") + strategy: Strategy = models.OneToOneField("Strategy", on_delete=models.CASCADE, related_name="bot") def __str__(self) -> str: return f"BOT {self.pk=}" @@ -115,7 +117,7 @@ def _strategy(self): return self.strategy.find() @property - def architecture(self): + def architecture(self) -> Architecture: return self._strategy.architecture.find() @property @@ -140,12 +142,11 @@ def get_connections(self): def get_connection_data(self): return {connection: connection.to_dict() for connection in self.get_connections()} - def get_orders(self, data: Optional[dict] = None, no_db_data: Optional[dict] = None): + def get_orders(self, data: DataType, no_db_data: Optional[DBDataType] = None): if not self.active: error_msg = "Bot is hibernating." raise BotError.InvalidSetting(error_msg) - - orders = self._get_orders(data=data, no_db_data=no_db_data) + orders = self.get_orders__no_db(data=data, no_db_data=no_db_data) batches = {} order_objects = [] for order in orders: @@ -164,14 +165,15 @@ def get_orders(self, data: Optional[dict] = None, no_db_data: Optional[dict] = N ConnectionModification.objects.create(order=order, **modification) for modification in architecture_modifications: ArchitectureModification.objects.create(order=order, **modification) + for batch in batches.values(): batch.set_status_ready() return order_objects, batches - def _get_orders(self, data: Optional[dict] = None, no_db_data: Optional[dict] = None): + def get_orders__no_db(self, data: DataType, no_db_data: Optional[DBDataType] = None) -> list[dict]: """Get orders of the bot.""" - return self.architecture._get_orders(data=data, no_db_data=no_db_data) # noqa: SLF001 + return self.architecture.get_orders__no_db(data=data, no_db_data=no_db_data) def connect_to_wallet(self, wallet: SpaceSimulationWallet | SpaceWallet) -> Connection: """Connect the bot to a (sim)space's wallet.""" diff --git a/django_napse/core/models/bots/controller.py b/django_napse/core/models/bots/controller.py index de320caf..9cb0dccc 100644 --- a/django_napse/core/models/bots/controller.py +++ b/django_napse/core/models/bots/controller.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import math from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Optional @@ -5,6 +7,7 @@ from django.db import models from requests.exceptions import ConnectionError, ReadTimeout, SSLError +from django_napse.core.models.bots.bot import Bot from django_napse.core.models.bots.managers.controller import ControllerManager from django_napse.core.models.orders.order import Order, OrderBatch from django_napse.utils.constants import EXCHANGE_INTERVALS, EXCHANGE_PAIRS, ORDER_STATUS, SIDES, STABLECOINS @@ -13,7 +16,8 @@ if TYPE_CHECKING: from django_napse.core.models.accounts.exchange import Exchange, ExchangeAccount - from django_napse.core.models.bots.bot import Bot + from django_napse.core.models.bots.architecture import DataType + from django_napse.core.pydantic.candle import CandlePydantic class Controller(models.Model): @@ -119,6 +123,17 @@ def exchange(self) -> "Exchange": """Return the exchange of the controller.""" return self.exchange_account.exchange + @property + def single_pair_bots(self) -> list["Bot"]: + """Return the bots that are allowed to trade on the controller.""" + bots = [] + + for bot in Bot.objects.filter(strategy__architecture__in=self.single_pair_architectures.all()): + fleet = bot.fleet + if fleet is not None and fleet.running and bot.active and not bot.is_in_simulation: + bots.append(bot) + return bots + def update_variables(self) -> None: """If the variables are older than 1 minute, update them.""" if self.last_settings_update is None or self.last_settings_update < datetime.now(tz=timezone.utc) - timedelta(minutes=1): @@ -154,28 +169,37 @@ def update_variables_always(self) -> None: if self.pk: self.save() - def process_orders(self, no_db_data: Optional[dict] = None, *, testing: bool) -> list[Order]: + def process_orders__no_db(self, no_db_data: Optional[dict] = None, *, testing: bool) -> tuple[list[Order], list[OrderBatch]]: in_simulation = no_db_data is not None no_db_data = no_db_data or { - "buy_orders": Order.objects.filter( - order__batch__status=ORDER_STATUS.READY, - order__side=SIDES.BUY, - order__batch__controller=self, - order__testing=testing, - ), - "sell_orders": Order.objects.filter( - order__batch__status=ORDER_STATUS.READY, - order__side=SIDES.SELL, - order__batch__controller=self, - order__testing=testing, - ), - "keep_orders": Order.objects.filter( - order__batch__status=ORDER_STATUS.READY, - order__side=SIDES.KEEP, - order__batch__controller=self, - order__testing=testing, - ), - "batches": OrderBatch.objects.filter(status=ORDER_STATUS.READY, batch__controller=self), + "buy_orders": [ + order + for order in Order.objects.filter( + batch__status=ORDER_STATUS.READY, + side=SIDES.BUY, + batch__controller=self, + ) + if order.testing == testing + ], + "sell_orders": [ + order + for order in Order.objects.filter( + batch__status=ORDER_STATUS.READY, + side=SIDES.SELL, + batch__controller=self, + ) + if order.testing == testing + ], + "keep_orders": [ + order + for order in Order.objects.filter( + batch__status=ORDER_STATUS.READY, + side=SIDES.KEEP, + batch__controller=self, + ) + if order.testing == testing + ], + "batches": OrderBatch.objects.filter(status=ORDER_STATUS.READY, controller=self), "exchange_controller": self.exchange_controller, "min_trade": self.min_trade, "price": self.get_price(), @@ -203,12 +227,12 @@ def process_orders(self, no_db_data: Optional[dict] = None, *, testing: bool) -> order.calculate_batch_share(total=aggregated_order["sell_amount"]) for order in no_db_data["keep_orders"]: order.batch_share = 0 - receipt, executed_amounts_buy, executed_amounts_sell, fees_buy, fees_sell = no_db_data["exchange_controller"].submit_order( controller=self, aggregated_order=aggregated_order, testing=in_simulation or testing, ) + all_orders = [] for order in no_db_data["buy_orders"]: order.calculate_exit_amounts( @@ -233,16 +257,23 @@ def process_orders(self, no_db_data: Optional[dict] = None, *, testing: bool) -> all_orders.append(order) for batch in no_db_data["batches"]: - batch._set_status_post_process(receipt=receipt) + batch.set_status_post_process__no_db(receipt=receipt) - return all_orders + return all_orders, no_db_data["batches"] - def apply_orders(self, orders): + def apply_orders(self, orders: list[Order]) -> None: for order in orders: order.save() order.apply_swap() - def send_candles_to_bots(self, closed_candle, current_candle) -> list: + def apply_batches(self, batches: list[OrderBatch]) -> None: + for batch in batches: + batch.save() + + def prepare_candles(self, closed_candle: CandlePydantic, current_candle: CandlePydantic) -> DataType: + return {"candles": {self: {"current": current_candle, "latest": closed_candle}}, "extras": {}} + + def send_candles_to_bots(self, closed_candle: CandlePydantic, current_candle: CandlePydantic) -> list: """Scan all bots (that are allowed to trade) and get their orders. Args: @@ -255,9 +286,8 @@ def send_candles_to_bots(self, closed_candle, current_candle) -> list: list: A list of orders. """ orders = [] - for bot in self.bots.all().filter(is_simulation=False, fleet__running=True, can_trade=True): - bot: "Bot" - orders = [*orders, bot.give_order(closed_candle, current_candle)] + for bot in self.single_pair_bots: + orders = [*orders, bot.get_orders(data=self.prepare_candles(closed_candle, current_candle))[0]] return orders @staticmethod diff --git a/django_napse/core/models/bots/implementations/dca/strategy.py b/django_napse/core/models/bots/implementations/dca/strategy.py index 4fbb0eb7..70475966 100644 --- a/django_napse/core/models/bots/implementations/dca/strategy.py +++ b/django_napse/core/models/bots/implementations/dca/strategy.py @@ -34,7 +34,7 @@ def give_order(self, data: dict) -> list[dict]: controller = data["controllers"]["main"] if ( self.variable_last_buy_date is None - or data["candles"][controller]["current"]["open_time"] - self.variable_last_buy_date >= data["config"]["timeframe"] + or data["candles"][controller]["current"].open_time - self.variable_last_buy_date >= data["config"]["timeframe"] ): return [ { @@ -43,7 +43,7 @@ def give_order(self, data: dict) -> list[dict]: "StrategyModifications": [ { "key": "last_buy_date", - "value": str(data["candles"][controller]["current"]["open_time"]), + "value": str(data["candles"][controller]["current"].open_time), "target_type": "datetime", "ignore_failed_order": False, }, @@ -53,7 +53,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": 20, "asked_for_ticker": controller.quote, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.BUY, }, ] @@ -67,7 +67,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": 0, "asked_for_ticker": controller.quote, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.KEEP, }, ] diff --git a/django_napse/core/models/bots/implementations/turbo_dca/strategy.py b/django_napse/core/models/bots/implementations/turbo_dca/strategy.py index a935d614..c7c1b147 100644 --- a/django_napse/core/models/bots/implementations/turbo_dca/strategy.py +++ b/django_napse/core/models/bots/implementations/turbo_dca/strategy.py @@ -6,7 +6,7 @@ from django_napse.core.models.bots.implementations.turbo_dca.config import TurboDCABotConfig from django_napse.core.models.bots.plugins import LBOPlugin, MBPPlugin, SBVPlugin from django_napse.core.models.bots.strategy import Strategy -from django_napse.core.models.wallets.currency import CurrencyPydantic +from django_napse.core.pydantic.currency import CurrencyPydantic from django_napse.utils.constants import SIDES diff --git a/django_napse/core/models/bots/plugins/sbv.py b/django_napse/core/models/bots/plugins/sbv.py index e71a5fa3..156f5e30 100644 --- a/django_napse/core/models/bots/plugins/sbv.py +++ b/django_napse/core/models/bots/plugins/sbv.py @@ -1,6 +1,6 @@ from django_napse.core.models.bots.plugin import Plugin from django_napse.core.models.connections.connection import ConnectionSpecificArgs -from django_napse.core.models.wallets.currency import CurrencyPydantic +from django_napse.core.pydantic.currency import CurrencyPydantic from django_napse.utils.constants import PLUGIN_CATEGORIES, SIDES diff --git a/django_napse/core/models/connections/connection.py b/django_napse/core/models/connections/connection.py index 30a0c922..900a8e93 100644 --- a/django_napse/core/models/connections/connection.py +++ b/django_napse/core/models/connections/connection.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING from django.db import models @@ -9,13 +11,15 @@ if TYPE_CHECKING: from django_napse.core.models.accounts.space import Space + from django_napse.core.models.bots.bot import Bot + from django_napse.core.models.wallets.wallet import Wallet class Connection(models.Model): """Link between a bot & a wallet.""" - owner = models.ForeignKey("Wallet", on_delete=models.CASCADE, related_name="connections") - bot = models.ForeignKey("Bot", on_delete=models.CASCADE, related_name="connections") + owner: Wallet = models.ForeignKey("Wallet", on_delete=models.CASCADE, related_name="connections") + bot: Bot = models.ForeignKey("Bot", on_delete=models.CASCADE, related_name="connections") created_at = models.DateTimeField(auto_now_add=True, blank=True) diff --git a/django_napse/core/models/modifications/architecture.py b/django_napse/core/models/modifications/architecture.py index 50348564..bc58da53 100644 --- a/django_napse/core/models/modifications/architecture.py +++ b/django_napse/core/models/modifications/architecture.py @@ -9,7 +9,7 @@ class ArchitectureModification(Modification): def apply(self): architecture = self.order.connection.bot.architecture.find() - architectur, self = self.apply__no_db(architecture) + _, self = self.apply__no_db(architecture) architecture.save() self.save() diff --git a/django_napse/core/models/orders/managers/order.py b/django_napse/core/models/orders/managers/order.py index fcffcf93..515af3b7 100644 --- a/django_napse/core/models/orders/managers/order.py +++ b/django_napse/core/models/orders/managers/order.py @@ -28,9 +28,6 @@ def create( error_msg = f"Ticker {asked_for_ticker} is not valid for a sell order. Should be {batch.controller.base}." raise OrderError.InvalidOrder(error_msg) - # if side == SIDES.KEEP: - # return None - order = self.model( batch=batch, connection=connection, diff --git a/django_napse/core/models/orders/order.py b/django_napse/core/models/orders/order.py index 774f1b2d..bc2125da 100644 --- a/django_napse/core/models/orders/order.py +++ b/django_napse/core/models/orders/order.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Optional from django.db import models @@ -12,6 +14,7 @@ if TYPE_CHECKING: from django_napse.core.models.accounts.exchange import ExchangeAccount from django_napse.core.models.bots.controller import Controller + from django_napse.core.models.connections.connection import Connection from django_napse.core.models.modifications.modification import Modification @@ -33,7 +36,7 @@ def set_status_ready(self) -> None: error_msg = f"Order {self.pk} is not pending." raise OrderError.StatusError(error_msg) - def _set_status_post_process(self, receipt: dict) -> None: + def set_status_post_process__no_db(self, receipt: dict) -> None: if self.status != ORDER_STATUS.READY: error_msg = f"Order {self.pk} is not ready." raise OrderError.StatusError(error_msg) @@ -43,6 +46,7 @@ def _set_status_post_process(self, receipt: dict) -> None: buy_failed = True if "error" in receipt[SIDES.SELL]: sell_failed = True + print(receipt, buy_failed, sell_failed) if buy_failed and sell_failed: self.status = ORDER_STATUS.FAILED elif buy_failed: @@ -56,8 +60,8 @@ def _set_status_post_process(self, receipt: dict) -> None: class Order(models.Model): """An order market created by bots.""" - batch = models.ForeignKey("OrderBatch", on_delete=models.CASCADE, related_name="orders") - connection = models.ForeignKey("Connection", on_delete=models.CASCADE, related_name="orders") + batch: OrderBatch = models.ForeignKey("OrderBatch", on_delete=models.CASCADE, related_name="orders") + connection: Connection = models.ForeignKey("Connection", on_delete=models.CASCADE, related_name="orders") price = models.FloatField() pair = models.CharField(max_length=10) side = models.CharField(max_length=10) @@ -263,6 +267,7 @@ def apply_modifications(self) -> list["Modification"]: def apply_swap(self) -> None: """Swap quote into base (BUY) or base into quote (SELL).""" + self.info() if self.side == SIDES.BUY: Debit.objects.create( wallet=self.wallet, @@ -320,6 +325,8 @@ def process_payout(self) -> None: ticker=self.batch.controller.quote, transaction_type=TRANSACTION_TYPES.ORDER_REFUND, ) + self.completed = True + self.save() def tickers_info(self) -> dict[str, str]: """Give informations about received, spent & fee tickers.""" diff --git a/django_napse/core/models/wallets/currency.py b/django_napse/core/models/wallets/currency.py index 6860f3fa..17410e1e 100644 --- a/django_napse/core/models/wallets/currency.py +++ b/django_napse/core/models/wallets/currency.py @@ -1,20 +1,11 @@ from typing import TYPE_CHECKING from django.db import models -from pydantic import BaseModel if TYPE_CHECKING: from django_napse.core.models.wallets.wallet import Wallet -class CurrencyPydantic(BaseModel): - """A Pydantic model for the Currency class.""" - - ticker: str - amount: float - mbp: float - - class Currency(models.Model): """A Currency contains the amount of a ticker in a wallet, as well as the Mean Buy Price (MBP).""" diff --git a/django_napse/core/models/wallets/wallet.py b/django_napse/core/models/wallets/wallet.py index 777c8dee..b71ead34 100644 --- a/django_napse/core/models/wallets/wallet.py +++ b/django_napse/core/models/wallets/wallet.py @@ -6,8 +6,9 @@ from pydantic import BaseModel from django_napse.core.models.bots.controller import Controller -from django_napse.core.models.wallets.currency import Currency, CurrencyPydantic +from django_napse.core.models.wallets.currency import Currency from django_napse.core.models.wallets.managers import WalletManager +from django_napse.core.pydantic.currency import CurrencyPydantic from django_napse.utils.errors import WalletError from django_napse.utils.findable_class import FindableClass diff --git a/django_napse/core/pydantic/__init__.py b/django_napse/core/pydantic/__init__.py new file mode 100644 index 00000000..b28947c9 --- /dev/null +++ b/django_napse/core/pydantic/__init__.py @@ -0,0 +1 @@ +from .currency import CurrencyPydantic diff --git a/django_napse/core/pydantic/candle.py b/django_napse/core/pydantic/candle.py new file mode 100644 index 00000000..9c21f27b --- /dev/null +++ b/django_napse/core/pydantic/candle.py @@ -0,0 +1,14 @@ +from datetime import datetime + +from pydantic import BaseModel, PositiveFloat + + +class CandlePydantic(BaseModel): + """A Pydantic model for the candles (used for live trading).""" + + open_time: datetime + open: PositiveFloat + high: PositiveFloat + low: PositiveFloat + close: PositiveFloat + volume: PositiveFloat diff --git a/django_napse/core/pydantic/currency.py b/django_napse/core/pydantic/currency.py new file mode 100644 index 00000000..ad0e409f --- /dev/null +++ b/django_napse/core/pydantic/currency.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class CurrencyPydantic(BaseModel): + """A Pydantic model for the Currency class.""" + + ticker: str + amount: float + mbp: float diff --git a/django_napse/core/settings.py b/django_napse/core/settings.py index b81e55a4..1e9f1be8 100644 --- a/django_napse/core/settings.py +++ b/django_napse/core/settings.py @@ -114,3 +114,16 @@ def NAPSE_MASTER_KEY(self) -> str: # noqa: D102 if {*list(napse_settings.NAPSE_EXCHANGE_CONFIGS.keys())} != set(EXCHANGES): error_msg = "NAPSE_EXCHANGE_CONFIGS does not match the list of exchanges. Can't start the server." raise NapseError.SettingsError(error_msg) + + if "LOGGING" not in settings.__dir__(): + settings.LOGGING = {} + settings.LOGGING["version"] = 1 + settings.LOGGING["disable_existing_loggers"] = False + settings.LOGGING["handlers"] = {"console": {"class": "logging.StreamHandler"}, **settings.LOGGING.get("handlers", {})} + settings.LOGGING["loggers"] = { + "django": { + "handlers": ["console"], + "level": "INFO", + }, + **settings.LOGGING.get("loggers", {}), + } diff --git a/django_napse/core/tasks/base_task.py b/django_napse/core/tasks/base_task.py index 0c203e46..8e5de9b3 100644 --- a/django_napse/core/tasks/base_task.py +++ b/django_napse/core/tasks/base_task.py @@ -1,43 +1,63 @@ from datetime import datetime, timezone -from typing import Optional +from time import sleep, time import celery +import redis +from celery.app.task import ExceptionInfo from celery.utils.log import get_task_logger +from django.conf import settings from django.db import IntegrityError from django.db.utils import ProgrammingError from django_celery_beat.models import IntervalSchedule, PeriodicTask from django_napse.core.celery_app import celery_app, strategy_log_free +redis_client = redis.Redis(host=settings.CELERY_BROKER_URL.split("//")[1].split(":")[0], port=settings.CELERY_BROKER_URL.split(":")[2]) + class BaseTask(celery.Task): """Base class for all Celery tasks.""" name = "base_task" Strategy = strategy_log_free - logger = get_task_logger(name) + logger = get_task_logger("django") interval_time = 5 # Impossible to make dynamic modification because of celery + min_interval_time = 4 def __init__(self) -> None: super().__init__() + if self.min_interval_time > self.interval_time: + error_msg = f"min_interval_time ({self.min_interval_time}) must be lower than interval_time ({self.interval_time})" + raise ValueError(error_msg) self.logger.setLevel("INFO") def run(self) -> None: """Function called when running the task.""" + t = time() + redis_client.set(self.name, t, nx=True, ex=self.interval_time + 1) + lock = redis_client.get(self.name) + if lock is None or lock.decode("utf-8") != str(t): + return + self._run() + sleep(self.min_interval_time) # let all the other tasks fail + redis_client.delete(self.name) + + def _run(self) -> None: + """Run the task.""" def info(self, msg: str) -> None: """Log a message.""" - info = f"[{self.name} @{datetime.now(tz=timezone.utc)}] : {msg}" + info = f"[{datetime.now(tz=timezone.utc)} @{self.name}] : {msg}" self.logger.info(info) def error(self, msg: str) -> None: """Log an error.""" - error = f"[{self.name} @{datetime.now(tz=timezone.utc)}] : {msg}" + error = f"[{datetime.now(tz=timezone.utc)} @{self.name}] : {msg}" self.logger.error(error) def warning(self, msg: str) -> None: """Log a warning.""" - warning = f"[{self.name} @{datetime.now(tz=timezone.utc)}] : {msg}" + warning = f"[{datetime.now(tz=timezone.utc)} @{self.name}] : {msg}" self.logger.warning(warning) def create_task(self) -> None: @@ -95,18 +115,22 @@ def num_active_tasks(self) -> int: count += 1 return count - def avoid_overlap(self, *, verbose: Optional[bool] = False) -> bool: - """Avoid task overlap. + def on_failure(self, exc: Exception, task_id: str, args: tuple, kwargs: dict, einfo: ExceptionInfo) -> None: # noqa: ARG002 + """Error handler. + + This is run by the worker when the task fails. - Args: - verbose (bool, optional): Whether to print logs. Defaults to False. + Arguments: + exc (Exception): The exception raised by the task. + task_id (str): Unique id of the failed task. + args (Tuple): Original arguments for the task that failed. + kwargs (Dict): Original keyword arguments for the task that failed. + einfo (~billiard.einfo.ExceptionInfo): Exception information. Returns: - bool: True if task is not running, False otherwise + None: The return value of this handler is ignored. """ - if self.num_active_tasks() > 1: - if verbose: - info = f"Period task {self.name} already running" - self.info(info) - return False - return True + """Log error on failure.""" + error = f"[{self.name} @{datetime.now(tz=timezone.utc)}] : {exc}" + self.error(error) + self.error(einfo) diff --git a/django_napse/core/tasks/candle_collector.py b/django_napse/core/tasks/candle_collector.py index fd2d0dba..83c3b4f9 100644 --- a/django_napse/core/tasks/candle_collector.py +++ b/django_napse/core/tasks/candle_collector.py @@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError from django_napse.core.models.bots.controller import Controller +from django_napse.core.pydantic.candle import CandlePydantic from django_napse.core.tasks.base_task import BaseTask @@ -9,10 +10,10 @@ class CandleCollectorTask(BaseTask): """Task to collect candles from binance's api and send it to controllers.""" name = "candle_collector" - interval_time = 30 + interval_time = 15 @staticmethod - def build_candle(request_json: list[list[int], list[int]]) -> tuple[dict[str, int | float], dict[str, int | float]]: + def build_candle(request_json: list[list[int], list[int]]) -> tuple[CandlePydantic, CandlePydantic]: """Structure close_candle & current candle from the request.json(). Candle shape: {"T": 1623000000000, "O": 1.0, "H": 1.0, "L": 1.0, "C": 1.0, "V": 1.0}. @@ -63,7 +64,21 @@ def build_candle(request_json: list[list[int], list[int]]) -> tuple[dict[str, in else: closed_candle[label] = float(request_json[0][i]) current_candle[label] = float(request_json[1][i]) - return closed_candle, current_candle + return CandlePydantic( + open_time=closed_candle["T"], + open=closed_candle["O"], + high=closed_candle["H"], + low=closed_candle["L"], + close=closed_candle["C"], + volume=closed_candle["V"], + ), CandlePydantic( + open_time=current_candle["T"], + open=current_candle["O"], + high=current_candle["H"], + low=current_candle["L"], + close=current_candle["C"], + volume=current_candle["V"], + ) @staticmethod def request_get(pair: str, interval: str, api: str = "api") -> requests.Response: @@ -116,19 +131,18 @@ def get_candles(self, pair: str, interval: str) -> tuple[dict[str, int | float], error_msg = f"Impossible to get candles from binance's api (pair: {pair}, interval: {interval})" raise ValueError(error_msg) - def run(self) -> None: + def _run(self) -> None: """Run the task. Try to get the results of request of binance's api and send it to controller(s). If the request failed, the controller(s) is add to a list and controller(s) is this list try again (on all binance's backup api) at the end. """ - if not self.avoid_overlap(verbose=False): - return self.info("Running CandleCollectorTask") failed_controllers: list["Controller"] = [] failed_controllers_second_attempt: list["Controller"] = [] all_orders = [] + for controller in Controller.objects.all(): request = self.request_get(controller.pair, controller.interval, "api") success_code = 200 @@ -161,6 +175,9 @@ def run(self) -> None: error = f"{controller} failed on all apis" self.error(error) + if len(all_orders) > 0: + self.info(f"Sent {len(all_orders)} orders") + CandleCollectorTask().delete_task() CandleCollectorTask().register_task() diff --git a/django_napse/core/tasks/controller_update.py b/django_napse/core/tasks/controller_update.py index de53b0df..56c5e3cc 100644 --- a/django_napse/core/tasks/controller_update.py +++ b/django_napse/core/tasks/controller_update.py @@ -6,14 +6,12 @@ class ControllerUpdateTask(BaseTask): """Task to update all controllers.""" name = "controller_update" - interval_time = 45 + interval_time = 15 time_limit = 60 soft_time_limit = 60 - def run(self) -> None: + def _run(self) -> None: """Run a task to update all controllers.""" - if not self.avoid_overlap(verbose=False): - return self.info("Running ControllerUpdateTask") for controller in Controller.objects.all(): controller.update_variables_always() diff --git a/django_napse/core/tasks/history_update.py b/django_napse/core/tasks/history_update.py index e9ebdc5c..3ff2a867 100644 --- a/django_napse/core/tasks/history_update.py +++ b/django_napse/core/tasks/history_update.py @@ -10,10 +10,8 @@ class HistoryUpdateTask(BaseTask): time_limit = 60 soft_time_limit = 60 - def run(self) -> None: + def _run(self) -> None: """Run a task to update all controllers.""" - if not self.avoid_overlap(verbose=False): - return self.info("Running HistoryUpdateTask") for history in History.objects.all(): history.find().generate_data_point() diff --git a/django_napse/core/tasks/order_process_executor.py b/django_napse/core/tasks/order_process_executor.py index 8ac7308c..a7c210c9 100644 --- a/django_napse/core/tasks/order_process_executor.py +++ b/django_napse/core/tasks/order_process_executor.py @@ -1,18 +1,27 @@ -# from django_napse.core.models import Order +from django_napse.core.models.bots.controller import Controller from django_napse.core.tasks.base_task import BaseTask class OrderProcessExecutorTask(BaseTask): + """Task to process all pending orders.""" + name = "order_process_executor" - interval_time = 1 # Impossible to make dynamic modification because of celery + interval_time = 5 # Impossible to make dynamic modification because of celery - def run(self) -> None: + def _run(self) -> None: """Run a task to process all pending orders.""" - if not self.avoid_overlap(verbose=False): - return + self.info("Running OrderProcessExecutorTask") processed = 0 - # for order in Order.objects.filter(status="pending", completed=True): - # processed += 1 + for controller in Controller.objects.all(): + orders, batches = controller.process_orders__no_db(testing=True) + processed += len(orders) + controller.apply_batches(batches) + controller.apply_orders(orders) + for order in orders: + order.apply_modifications() + order.process_payout() + order.info() + if processed > 0: self.info(f"Processed {processed} orders") diff --git a/django_napse/simulations/models/simulations/simulation_queue.py b/django_napse/simulations/models/simulations/simulation_queue.py index 8dc25330..263e8a04 100644 --- a/django_napse/simulations/models/simulations/simulation_queue.py +++ b/django_napse/simulations/models/simulations/simulation_queue.py @@ -11,7 +11,7 @@ from django_napse.core.models.modifications import ArchitectureModification, ConnectionModification, StrategyModification from django_napse.core.models.orders.order import Order, OrderBatch from django_napse.core.models.transactions.credit import Credit -from django_napse.core.models.wallets.currency import CurrencyPydantic +from django_napse.core.pydantic.currency import CurrencyPydantic from django_napse.simulations.models.datasets.dataset import Candle, DataSet from django_napse.simulations.models.simulations.managers import SimulationQueueManager from django_napse.utils.constants import EXCHANGE_INTERVALS, ORDER_LEEWAY_PERCENTAGE, ORDER_STATUS, SIDES, SIMULATION_STATUS @@ -234,7 +234,7 @@ def quick_simulation(self, bot, no_db_data, verbose=True): candle_data=candle_data, min_interval=min_interval, ) - orders = bot._get_orders(data=processed_data, no_db_data=no_db_data) + orders = bot.get_orders__no_db(data=processed_data, no_db_data=no_db_data) batches = {} for order in orders: debited_amount = order["asked_for_amount"] * (1 + ORDER_LEEWAY_PERCENTAGE / 100) @@ -267,7 +267,7 @@ def quick_simulation(self, bot, no_db_data, verbose=True): for modification in architecture_modifications: all_modifications.append(ArchitectureModification(order=order, **modification)) - orders = controller.process_orders( + orders, _ = controller.process_orders__no_db( no_db_data={ "buy_orders": [order for order in order_objects if order.side == SIDES.BUY], "sell_orders": [order for order in order_objects if order.side == SIDES.SELL], @@ -363,7 +363,7 @@ def irl_simulation(self, bot, no_db_data, verbose=True): all_orders = [] for controller, batch in batches.items(): - orders = controller.process_orders( + orders, batch_list = controller.process_orders__no_db( no_db_data={ "buy_orders": [order for order in orders if order.side == SIDES.BUY], "sell_orders": [order for order in orders if order.side == SIDES.SELL], @@ -376,6 +376,7 @@ def irl_simulation(self, bot, no_db_data, verbose=True): testing=True, ) + controller.apply_batches(batch_list) controller.apply_orders(orders) for order in orders: order.apply_modifications() diff --git a/django_napse/simulations/tasks/dataset_queue.py b/django_napse/simulations/tasks/dataset_queue.py index 2c05d1bc..ac76f8dc 100644 --- a/django_napse/simulations/tasks/dataset_queue.py +++ b/django_napse/simulations/tasks/dataset_queue.py @@ -36,4 +36,4 @@ def run(self) -> None: DataSetQueueTask().delete_task() -DataSetQueueTask().register_task() +# DataSetQueueTask().register_task() diff --git a/django_napse/simulations/tasks/simulation_queue.py b/django_napse/simulations/tasks/simulation_queue.py index ff331866..9235b532 100644 --- a/django_napse/simulations/tasks/simulation_queue.py +++ b/django_napse/simulations/tasks/simulation_queue.py @@ -50,4 +50,4 @@ def run(self) -> None: SimulationQueueTask().delete_task() -SimulationQueueTask().register_task() +# SimulationQueueTask().register_task() diff --git a/django_napse/utils/usefull_functions.py b/django_napse/utils/usefull_functions.py index 2255457c..de79c884 100644 --- a/django_napse/utils/usefull_functions.py +++ b/django_napse/utils/usefull_functions.py @@ -3,10 +3,10 @@ from pytz import UTC +from django_napse.core.pydantic.currency import CurrencyPydantic -def calculate_mbp(value: str, current_value: float, order, currencies: dict) -> float: - from django_napse.core.models.wallets.currency import CurrencyPydantic +def calculate_mbp(value: str, current_value: float, order, currencies: dict) -> float: ticker, price = value.split("|") price = float(price) From 2e795f588e5cac8894c51852d0bbe8ec05920f00 Mon Sep 17 00:00:00 2001 From: Tom JEANNESSON Date: Wed, 13 Mar 2024 19:40:19 +0100 Subject: [PATCH 2/2] tmp --- .../core/management/commands/create_dca.py | 4 ++-- .../bots/implementations/empty/strategy.py | 2 +- .../bots/implementations/turbo_dca/strategy.py | 18 +++++++++--------- django_napse/core/models/orders/order.py | 3 +-- .../core/tasks/order_process_executor.py | 1 - .../models/simulations/simulation_queue.py | 11 ++++++----- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/django_napse/core/management/commands/create_dca.py b/django_napse/core/management/commands/create_dca.py index 19d65277..aae3bf50 100644 --- a/django_napse/core/management/commands/create_dca.py +++ b/django_napse/core/management/commands/create_dca.py @@ -14,12 +14,12 @@ def add_arguments(self, parser): # noqa def handle(self, *args, **options): # noqa exchange_account = ExchangeAccount.objects.first() space = Space.objects.first() - config = DCABotConfig.objects.create(space=space, settings={"timeframe": timedelta(minutes=5)}) + config = DCABotConfig.objects.create(space=space, settings={"timeframe": timedelta(hours=1)}) controller = Controller.get( exchange_account=exchange_account, base="BTC", quote="USDT", - interval="1m", + interval="15m", ) architecture = SinglePairArchitecture.objects.create(constants={"controller": controller}) strategy = DCAStrategy.objects.create(config=config, architecture=architecture) diff --git a/django_napse/core/models/bots/implementations/empty/strategy.py b/django_napse/core/models/bots/implementations/empty/strategy.py index 3eb5c058..7ce02f10 100644 --- a/django_napse/core/models/bots/implementations/empty/strategy.py +++ b/django_napse/core/models/bots/implementations/empty/strategy.py @@ -38,7 +38,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": 0, "asked_for_ticker": controller.quote, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.KEEP, }, ] diff --git a/django_napse/core/models/bots/implementations/turbo_dca/strategy.py b/django_napse/core/models/bots/implementations/turbo_dca/strategy.py index c7c1b147..5b74f169 100644 --- a/django_napse/core/models/bots/implementations/turbo_dca/strategy.py +++ b/django_napse/core/models/bots/implementations/turbo_dca/strategy.py @@ -42,7 +42,7 @@ def give_order(self, data: dict) -> list[dict]: controller = data["controllers"]["main"] if ( self.variable_last_buy_date is None - or data["candles"][controller]["current"]["open_time"] - self.variable_last_buy_date >= data["config"]["timeframe"] + or data["candles"][controller]["current"].open_time - self.variable_last_buy_date >= data["config"]["timeframe"] ): mbp = data["connection_data"][data["connection"]]["connection_specific_args"]["mbp"].get_value() lbo = data["connection_data"][data["connection"]]["connection_specific_args"]["lbo"].get_value() @@ -59,7 +59,7 @@ def give_order(self, data: dict) -> list[dict]: ) mbp = mbp if mbp is not None else math.inf sbv = sbv if sbv is not None else available_quote - current_price = data["candles"][controller]["latest"]["close"] + current_price = data["candles"][controller]["latest"].close amount = data["config"]["percentage"] * sbv / 100 if lbo == 0 or current_price < mbp: return [ @@ -69,7 +69,7 @@ def give_order(self, data: dict) -> list[dict]: "StrategyModifications": [ { "key": "last_buy_date", - "value": str(data["candles"][controller]["current"]["open_time"]), + "value": str(data["candles"][controller]["current"].open_time), "target_type": "datetime", "ignore_failed_order": False, }, @@ -79,7 +79,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": amount, "asked_for_ticker": controller.quote, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.BUY, }, ] @@ -91,7 +91,7 @@ def give_order(self, data: dict) -> list[dict]: "StrategyModifications": [ { "key": "last_buy_date", - "value": str(data["candles"][controller]["current"]["open_time"]), + "value": str(data["candles"][controller]["current"].open_time), "target_type": "datetime", "ignore_failed_order": False, }, @@ -101,7 +101,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": available_base, "asked_for_ticker": controller.base, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.SELL, }, ] @@ -112,7 +112,7 @@ def give_order(self, data: dict) -> list[dict]: "StrategyModifications": [ { "key": "last_buy_date", - "value": str(data["candles"][controller]["current"]["open_time"]), + "value": str(data["candles"][controller]["current"].open_time), "target_type": "datetime", "ignore_failed_order": False, }, @@ -122,7 +122,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": 0, "asked_for_ticker": controller.quote, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.KEEP, }, ] @@ -136,7 +136,7 @@ def give_order(self, data: dict) -> list[dict]: "asked_for_amount": 0, "asked_for_ticker": controller.quote, "pair": controller.pair, - "price": data["candles"][controller]["latest"]["close"], + "price": data["candles"][controller]["latest"].close, "side": SIDES.KEEP, }, ] diff --git a/django_napse/core/models/orders/order.py b/django_napse/core/models/orders/order.py index bc2125da..0d56ee92 100644 --- a/django_napse/core/models/orders/order.py +++ b/django_napse/core/models/orders/order.py @@ -46,7 +46,7 @@ def set_status_post_process__no_db(self, receipt: dict) -> None: buy_failed = True if "error" in receipt[SIDES.SELL]: sell_failed = True - print(receipt, buy_failed, sell_failed) + if buy_failed and sell_failed: self.status = ORDER_STATUS.FAILED elif buy_failed: @@ -267,7 +267,6 @@ def apply_modifications(self) -> list["Modification"]: def apply_swap(self) -> None: """Swap quote into base (BUY) or base into quote (SELL).""" - self.info() if self.side == SIDES.BUY: Debit.objects.create( wallet=self.wallet, diff --git a/django_napse/core/tasks/order_process_executor.py b/django_napse/core/tasks/order_process_executor.py index a7c210c9..a15f9c80 100644 --- a/django_napse/core/tasks/order_process_executor.py +++ b/django_napse/core/tasks/order_process_executor.py @@ -20,7 +20,6 @@ def _run(self) -> None: for order in orders: order.apply_modifications() order.process_payout() - order.info() if processed > 0: self.info(f"Processed {processed} orders") diff --git a/django_napse/simulations/models/simulations/simulation_queue.py b/django_napse/simulations/models/simulations/simulation_queue.py index 263e8a04..b545641c 100644 --- a/django_napse/simulations/models/simulations/simulation_queue.py +++ b/django_napse/simulations/models/simulations/simulation_queue.py @@ -11,6 +11,7 @@ from django_napse.core.models.modifications import ArchitectureModification, ConnectionModification, StrategyModification from django_napse.core.models.orders.order import Order, OrderBatch from django_napse.core.models.transactions.credit import Credit +from django_napse.core.pydantic.candle import CandlePydantic from django_napse.core.pydantic.currency import CurrencyPydantic from django_napse.simulations.models.datasets.dataset import Candle, DataSet from django_napse.simulations.models.simulations.managers import SimulationQueueManager @@ -153,7 +154,7 @@ def process_candle_data(self, candle_data, min_interval): processed_data = {"candles": {}, "extras": {}} current_prices = {} for controller, candle in candle_data.items(): - processed_data["candles"][controller] = {"current": candle, "latest": candle} + processed_data["candles"][controller] = {"current": CandlePydantic(**candle), "latest": CandlePydantic(**candle)} if controller.quote == "USDT" and controller.interval == min_interval: price = candle["close"] current_prices[f"{controller.base}_price"] = price @@ -274,8 +275,8 @@ def quick_simulation(self, bot, no_db_data, verbose=True): "keep_orders": [order for order in order_objects if order.side == SIDES.KEEP], "batches": [batch], "exchange_controller": exchange_controllers[controller], - "min_trade": controller.min_notional / processed_data["candles"][controller]["latest"]["close"], - "price": processed_data["candles"][controller]["latest"]["close"], + "min_trade": controller.min_notional / processed_data["candles"][controller]["latest"].close, + "price": processed_data["candles"][controller]["latest"].close, }, testing=True, ) @@ -370,8 +371,8 @@ def irl_simulation(self, bot, no_db_data, verbose=True): "keep_orders": [order for order in orders if order.side == SIDES.KEEP], "batches": [batch], "exchange_controller": exchange_controllers[controller], - "min_trade": controller.min_notional / processed_data["candles"][controller]["latest"]["close"], - "price": processed_data["candles"][controller]["latest"]["close"], + "min_trade": controller.min_notional / processed_data["candles"][controller]["latest"].close, + "price": processed_data["candles"][controller]["latest"].close, }, testing=True, )