diff --git a/django_napse/core/migrations/0001_initial.py b/django_napse/core/migrations/0001_initial.py index 183c5a7f..d14c6b5d 100644 --- a/django_napse/core/migrations/0001_initial.py +++ b/django_napse/core/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-08-14 16:31 +# Generated by Django 4.1.7 on 2023-08-17 11:29 import datetime from django.db import migrations, models @@ -83,6 +83,17 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("share", models.FloatField()), + ("breakpoint", models.FloatField()), + ("autoscale", models.BooleanField()), + ( + "bot", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="cluster", + to="django_napse_core.bot", + ), + ), ], ), migrations.CreateModel( @@ -132,20 +143,6 @@ class Migration(migrations.Migration): ("last_settings_update", models.DateTimeField(null=True)), ], ), - migrations.CreateModel( - name="DefaultFleetOperator", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ], - ), migrations.CreateModel( name="Exchange", fields=[ @@ -405,23 +402,6 @@ class Migration(migrations.Migration): ], bases=("django_napse_core.strategy",), ), - migrations.CreateModel( - name="EquilibriumFleetOperator", - fields=[ - ( - "defaultfleetoperator_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="django_napse_core.defaultfleetoperator", - ), - ), - ], - bases=("django_napse_core.defaultfleetoperator",), - ), migrations.CreateModel( name="LBOPlugin", fields=[ @@ -473,23 +453,6 @@ class Migration(migrations.Migration): ], bases=("django_napse_core.plugin",), ), - migrations.CreateModel( - name="SpecificSharesFleetOperator", - fields=[ - ( - "defaultfleetoperator_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="django_napse_core.defaultfleetoperator", - ), - ), - ], - bases=("django_napse_core.defaultfleetoperator",), - ), migrations.CreateModel( name="StrategyModification", fields=[ @@ -712,6 +675,7 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("importance", models.FloatField()), ( "bot", models.OneToOneField( @@ -795,15 +759,6 @@ class Migration(migrations.Migration): ), ], ), - migrations.AddField( - model_name="defaultfleetoperator", - name="fleet", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="operator", - to="django_napse_core.fleet", - ), - ), migrations.CreateModel( name="Debit", fields=[ @@ -899,108 +854,6 @@ class Migration(migrations.Migration): to="django_napse_core.strategy", ), ), - migrations.CreateModel( - name="SpecificShare", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("share", models.FloatField()), - ( - "cluster", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="specific_shares", - to="django_napse_core.cluster", - ), - ), - ( - "operator", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="specific_shares", - to="django_napse_core.defaultfleetoperator", - ), - ), - ], - options={ - "unique_together": {("cluster", "operator")}, - }, - ), - migrations.CreateModel( - name="SpecificBreakPoint", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("scale_up_breakpoint", models.FloatField()), - ( - "cluster", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="specific_breakpoints", - to="django_napse_core.cluster", - ), - ), - ( - "operator", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="specific_breakpoints", - to="django_napse_core.defaultfleetoperator", - ), - ), - ], - options={ - "unique_together": {("cluster", "operator")}, - }, - ), - migrations.CreateModel( - name="SpecificAutoscale", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("autoscale", models.BooleanField(default=True)), - ( - "cluster", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="specific_autoscales", - to="django_napse_core.cluster", - ), - ), - ( - "operator", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="specific_autoscales", - to="django_napse_core.defaultfleetoperator", - ), - ), - ], - options={ - "unique_together": {("cluster", "operator")}, - }, - ), migrations.CreateModel( name="SpaceWallet", fields=[ diff --git a/django_napse/core/models/bots/architecture.py b/django_napse/core/models/bots/architecture.py index 17ec7752..1a840ee0 100644 --- a/django_napse/core/models/bots/architecture.py +++ b/django_napse/core/models/bots/architecture.py @@ -49,6 +49,22 @@ def controllers_dict(self): # pragma: no cover error_msg = f"controllers_dict not implemented for the Architecture base class, please implement it in the {self.__class__} class." raise NotImplementedError(error_msg) + def accepted_tickers(self): # pragma: no cover + if self.__class__ == Architecture: + error_msg = "accepted_tickers not implemented for the Architecture base class, please implement it in a subclass." + else: + error_msg = f"accepted_tickers not implemented for the Architecture base class, please implement it in the {self.__class__} class." + raise NotImplementedError(error_msg) + + def accepted_investment_tickers(self): # pragma: no cover + if self.__class__ == Architecture: + error_msg = "accepted_investment_tickers not implemented for the Architecture base class, please implement it in a subclass." + else: + error_msg = ( + f"accepted_investment_tickers not implemented for the Architecture base class, please implement it in the {self.__class__} class." + ) + raise NotImplementedError(error_msg) + def get_extras(self): return {} diff --git a/django_napse/core/models/bots/architectures/single_pair.py b/django_napse/core/models/bots/architectures/single_pair.py index d6524f4c..fe682017 100644 --- a/django_napse/core/models/bots/architectures/single_pair.py +++ b/django_napse/core/models/bots/architectures/single_pair.py @@ -46,3 +46,9 @@ def architecture_modifications(self, order: dict, data: dict): "ignore_failed_order": True, }, ] + + def accepted_tickers(self): + return [self.controller.base, self.controller.quote] + + def accepted_investment_tickers(self): + return [self.controller.quote] diff --git a/django_napse/core/models/bots/plugins/mbp.py b/django_napse/core/models/bots/plugins/mbp.py index e94ebf57..fd217a88 100644 --- a/django_napse/core/models/bots/plugins/mbp.py +++ b/django_napse/core/models/bots/plugins/mbp.py @@ -11,7 +11,7 @@ def plugin_category(cls): def _apply(self, data: dict) -> dict: order = data["order"] if order["side"] == SIDES.BUY: - new_mbp = f"{order['controller'].base}|(a * b + c) / (c / {order['price']} + a)" + new_mbp = f"{order['controller'].base}|{order['price']}" order["ConnectionModifications"] += [ { "key": "mbp", diff --git a/django_napse/core/models/bots/strategy.py b/django_napse/core/models/bots/strategy.py index a1e29fcc..e5ae0d11 100644 --- a/django_napse/core/models/bots/strategy.py +++ b/django_napse/core/models/bots/strategy.py @@ -58,6 +58,6 @@ def connect(self, connection): def copy(self): return self.find().__class__.objects.create( - config=self.config.duplicate_immutable(), - architecture=self.architecture.copy(), + config=self.config.find().duplicate_immutable(), + architecture=self.architecture.find().copy(), ) diff --git a/django_napse/core/models/connections/connection.py b/django_napse/core/models/connections/connection.py index 76ecacf5..302802e4 100644 --- a/django_napse/core/models/connections/connection.py +++ b/django_napse/core/models/connections/connection.py @@ -30,7 +30,7 @@ def info(self, verbose=True, beacon=""): string += f"{beacon}Wallet:\n" new_beacon = beacon + "\t" - string += f"{beacon}\t{self.wallet.info(verbose=False, beacon=new_beacon)}\n" + string += f"{self.wallet.info(verbose=False, beacon=new_beacon)}\n" string += f"{beacon}ConnectionSpecificArgs:\n" query = self.specific_args.all() @@ -38,7 +38,7 @@ def info(self, verbose=True, beacon=""): string += f"{beacon}\tNone\n" else: for connection_specific_arg in query: - string += f"{beacon}\t{connection_specific_arg.info(verbose=False, beacon=new_beacon)}\n" + string += f"{connection_specific_arg.info(verbose=False, beacon=new_beacon)}\n" if verbose: # pragma: no cover print(string) diff --git a/django_napse/core/models/fleets/cluster.py b/django_napse/core/models/fleets/cluster.py index 70b8fd81..7db43a6e 100644 --- a/django_napse/core/models/fleets/cluster.py +++ b/django_napse/core/models/fleets/cluster.py @@ -1,24 +1,91 @@ from django.db import models -from django_napse.utils.errors import ClusterError +from django_napse.core.models.connections.connection import Connection +from django_napse.core.models.fleets.link import Link +from django_napse.core.models.transactions.transaction import Transaction +from django_napse.utils.constants import TRANSACTION_TYPES +from django_napse.utils.errors import BotError, ClusterError class Cluster(models.Model): fleet = models.ForeignKey("Fleet", on_delete=models.CASCADE, related_name="clusters") + bot = models.OneToOneField("Bot", on_delete=models.CASCADE, related_name="cluster") + share = models.FloatField() + breakpoint = models.FloatField() + autoscale = models.BooleanField() def __str__(self): return f"Cluster: {self.fleet}" def save(self, *args, **kwargs): - configs = [link.bot.strategy.config for link in self.links.all()] - if len(set(configs)) != 1: - error_msg = "All bots in a cluster must have the same config." - raise ClusterError.MultipleConfigs(error_msg) - if not configs[0].immutable: - error_msg = "The config must be immutable." + if not self.config.immutable: + error_msg = "In a fleet, the config must be immutable." raise ClusterError.MutableBotConfig(error_msg) return super().save(*args, **kwargs) + def info(self, verbose=True, beacon=""): + string = "" + string += f"{beacon}Cluster {self.pk}:\n" + string += f"{beacon}Args:\n" + string += f"{beacon}\t{self.fleet=}\n" + string += f"{beacon}\t{self.bot=}\n" + string += f"{beacon}\t{self.share=}\n" + string += f"{beacon}\t{self.breakpoint=}\n" + string += f"{beacon}\t{self.autoscale=}\n" + + new_beacon = beacon + "\t" + string += f"{beacon}Links:\n" + for link in self.links.all(): + string += f"{link.info(verbose=False, beacon=new_beacon)}\n" + + string += f"{beacon}Connections:\n" + connections = [] + for link in self.links.all(): + connections += list(link.bot.connections.all()) + for connection in connections: + string += f"{connection.info(verbose=False, beacon=new_beacon)}\n" + + if verbose: # pragma: no cover + print(string) + return string + @property def config(self): - return self.links.first().bot.strategy.config + return self.bot.strategy.config + + def invest(self, space, amount, ticker): + all_connections = [] + bots = [link.bot for link in self.links.all().order_by("importance")] + if len(bots) == 0: + new_bot = self.bot.copy() + Link.objects.create(bot=new_bot, cluster=self, importance=1) + connection = Connection.objects.create(bot=new_bot, owner=space.wallet) + Transaction.objects.create( + from_wallet=space.wallet, + to_wallet=connection.wallet, + amount=amount, + ticker=ticker, + transaction_type=TRANSACTION_TYPES.CONNECTION_DEPOSIT, + ) + all_connections.append(connection) + elif len(bots) == 1: + bot = bots[0] + if ticker not in bot.architecture.accepted_investment_tickers() or ticker not in bot.architecture.accepted_tickers(): + error_msg = f"Bot {bot} does not accept ticker {ticker}." + raise BotError.InvalidTicker(error_msg) + try: + connection = Connection.objects.get(bot=bot, owner=space.wallet) + except Connection.DoesNotExist: + connection = Connection.objects.create(bot=bot, owner=space.wallet) + Transaction.objects.create( + from_wallet=space.wallet, + to_wallet=connection.wallet, + amount=amount, + ticker=ticker, + transaction_type=TRANSACTION_TYPES.CONNECTION_DEPOSIT, + ) + all_connections.append(connection) + else: + error_msg = "Autoscale not implemented yet." + raise NotImplementedError(error_msg) + return all_connections diff --git a/django_napse/core/models/fleets/fleet.py b/django_napse/core/models/fleets/fleet.py index 97a24b31..64951d8c 100644 --- a/django_napse/core/models/fleets/fleet.py +++ b/django_napse/core/models/fleets/fleet.py @@ -19,6 +19,24 @@ class Fleet(models.Model): def __str__(self): return f"FLEET: {self.pk=}, name={self.name}" + def info(self, verbose=True, beacon=""): + string = "" + string += f"{beacon}Fleet {self.pk}:\n" + string += f"{beacon}Args:\n" + string += f"{beacon}\t{self.name=}\n" + string += f"{beacon}\t{self.exchange_account=}\n" + string += f"{beacon}\t{self.running=}\n" + string += f"{beacon}\t{self.setup_finished=}\n" + + string += f"{beacon}Clusters:\n" + new_beacon = beacon + "\t" + for cluster in self.clusters.all(): + string += f"{beacon}{cluster.info(verbose=False, beacon=new_beacon)}\n" + + if verbose: # pragma: no cover + print(string) + return string + @property def testing(self): return self.exchange_account.testing @@ -33,53 +51,8 @@ def bot_clusters(self): bot_clusters.append(Bot.objects.filter(link__cluster=cluster)) return bot_clusters - -class DefaultFleetOperator(models.Model): - fleet = models.OneToOneField("Fleet", on_delete=models.CASCADE, related_name="operator") - - def __str__(self) -> str: - return f"DEFAULT_FLEET_OPERATOR: {self.pk=}, fleet__name={self.fleet.name}" - - -class EquilibriumFleetOperator(DefaultFleetOperator): - pass - - -class SpecificSharesFleetOperator(DefaultFleetOperator): - pass - - -class SpecificShare(models.Model): - cluster = models.OneToOneField("Cluster", on_delete=models.CASCADE, related_name="specific_shares") - operator = models.ForeignKey(DefaultFleetOperator, on_delete=models.CASCADE, related_name="specific_shares") - share = models.FloatField() - - class Meta: - unique_together = ("cluster", "operator") - - def __str__(self) -> str: - return f"SPECIFIC_SHARE: {self.pk=}, operator={self.operator}, share={self.share}" - - -class SpecificBreakPoint(models.Model): - cluster = models.OneToOneField("Cluster", on_delete=models.CASCADE, related_name="specific_breakpoints") - operator = models.ForeignKey(DefaultFleetOperator, on_delete=models.CASCADE, related_name="specific_breakpoints") - scale_up_breakpoint = models.FloatField() - - class Meta: - unique_together = ("cluster", "operator") - - def __str__(self) -> str: - return f"SPECIFIC_BREAKPOINT: {self.pk=}, operator={self.operator}, scale_up_breakpoint={self.scale_up_breakpoint}" - - -class SpecificAutoscale(models.Model): - cluster = models.OneToOneField("Cluster", on_delete=models.CASCADE, related_name="specific_autoscales") - operator = models.ForeignKey(DefaultFleetOperator, on_delete=models.CASCADE, related_name="specific_autoscales") - autoscale = models.BooleanField(default=True) - - class Meta: - unique_together = ("cluster", "operator") - - def __str__(self) -> str: - return f"SPECIFIC_AUTOSCALE: {self.pk=}, operator={self.operator}, autoscale={self.autoscale}" + def invest(self, space, amount, ticker): + connections = [] + for cluster in self.clusters.all(): + connections += cluster.invest(space, amount * cluster.share, ticker) + return connections diff --git a/django_napse/core/models/fleets/link.py b/django_napse/core/models/fleets/link.py index ff9d3ab2..406fbc72 100644 --- a/django_napse/core/models/fleets/link.py +++ b/django_napse/core/models/fleets/link.py @@ -4,6 +4,19 @@ class Link(models.Model): bot = models.OneToOneField("Bot", on_delete=models.CASCADE, related_name="link") cluster = models.ForeignKey("Cluster", on_delete=models.CASCADE, related_name="links") + importance = models.FloatField() def __str__(self): return f"LINK: {self.bot} {self.fleet}" + + def info(self, verbose=True, beacon=""): + string = "" + string += f"{beacon}Link {self.pk}:\n" + string += f"{beacon}Args:\n" + string += f"{beacon}\t{self.bot=}\n" + string += f"{beacon}\t{self.cluster=}\n" + string += f"{beacon}\t{self.importance=}\n" + + if verbose: # pragma: no cover + print(string) + return string diff --git a/django_napse/core/models/fleets/managers/cluster.py b/django_napse/core/models/fleets/managers/cluster.py new file mode 100644 index 00000000..da869eac --- /dev/null +++ b/django_napse/core/models/fleets/managers/cluster.py @@ -0,0 +1,15 @@ +from django.apps import apps +from django.db import models + + +class ClusterManager(models.Manager): + def create(self, fleet, share, breakpoint, autoscale): # noqa A002 + SpecificShare = apps.get_model("django_napse_core", "SpecificShare") + SpecificBreakPoint = apps.get_model("django_napse_core", "SpecificBreakPoint") + SpecificAutoscale = apps.get_model("django_napse_core", "SpecificAutoscale") + + cluster = self.model(fleet=fleet) + cluster.save() + SpecificShare.objects.create(cluster=cluster, share=share) + SpecificBreakPoint.objects.create(cluster=cluster, breakpoint=breakpoint) + SpecificAutoscale.objects.create(cluster=cluster, autoscale=autoscale) diff --git a/django_napse/core/models/fleets/managers/fleet.py b/django_napse/core/models/fleets/managers/fleet.py index d13befb5..76ca93ab 100644 --- a/django_napse/core/models/fleets/managers/fleet.py +++ b/django_napse/core/models/fleets/managers/fleet.py @@ -1,8 +1,6 @@ -from django.apps import apps from django.db import models -from django_napse.core.models.fleets.link import Link -from django_napse.utils.constants import OPERATORS +from django_napse.core.models.fleets.cluster import Cluster from django_napse.utils.errors import FleetError @@ -11,51 +9,29 @@ def create( self, name, exchange_account, - operator: str = "EQUILIBRIUM", - operator_args=None, - bots=None, + clusters=None, ): - """Create a fleet. - - Args: - ---- - name (str): The name of the fleet - exchange_account(str): The exchange account of the fleet - operator (str): The operator of the fleet (default: "EQUILIBRIUM") - operator_args (dict): The arguments to pass down to the operator (default: {}) - bots (list): The list of bots to use (default: None) - - Raises: - ------ - ValueError: If the exchange or operator are invalid - - Returns: - ------- - Fleet: The created fleet - """ - operator_args = operator_args or {} - bots = bots or [] + clusters = clusters or [] + + if sum(cluster["share"] for cluster in clusters) != 1: + error_message = "The sum of all shares must be 1." + raise FleetError.InvalidShares(error_message) + + if len(clusters) == 0: + error_msg = "A fleet must have at least one cluster." + raise FleetError.InvalidClusters(error_msg) fleet = self.model( name=name, exchange_account=exchange_account, ) - if operator not in OPERATORS: - error_message = f"Invalid operator: {operator}." - raise FleetError.InvalidOperator(error_message) - fleet.save() - match operator: - case "EQUILIBRIUM": - EquilibriumFleetOperator = apps.get_model("django_napse_core", "EquilibriumFleetOperator") - EquilibriumFleetOperator.objects.create(fleet=fleet, **operator_args) - case "SPECIFIC_SHARES": - SpecificSharesFleetOperator = apps.get_model("django_napse_core", "SpecificSharesFleetOperator") - SpecificSharesFleetOperator.objects.create(fleet=fleet, **operator_args) - - for bot in bots: - Link.objects.create(bot=bot, fleet=fleet) + for cluster in clusters: + cluster["bot"] = cluster["bot"].copy() + Cluster.objects.create(fleet=fleet, **cluster) + fleet.setup_finished = True + fleet.save() return fleet diff --git a/django_napse/simulations/migrations/0001_initial.py b/django_napse/simulations/migrations/0001_initial.py index b6530056..a5117092 100644 --- a/django_napse/simulations/migrations/0001_initial.py +++ b/django_napse/simulations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-08-14 16:31 +# Generated by Django 4.1.7 on 2023-08-17 11:29 from django.db import migrations, models import django.db.models.deletion diff --git a/django_napse/utils/constants.py b/django_napse/utils/constants.py index 63b15830..4c94ee3b 100644 --- a/django_napse/utils/constants.py +++ b/django_napse/utils/constants.py @@ -19,13 +19,6 @@ def __str__(cls) -> str: return f"{[cls._member_map_[name].value for name in cls._member_names_]}" -class OPERATORS(StrEnum, metaclass=CustomEnumMeta): - """The operator for a fleet.""" - - EQUILIBRIUM = "EQUILIBRIUM" - SPECIFIC_SHARES = "SPECIFIC_SHARES" - - class EXCHANGES(StrEnum, metaclass=CustomEnumMeta): """The exchange for a fleet or a bot.""" diff --git a/django_napse/utils/errors/bots.py b/django_napse/utils/errors/bots.py index fb7a8c72..8253cb79 100644 --- a/django_napse/utils/errors/bots.py +++ b/django_napse/utils/errors/bots.py @@ -10,6 +10,9 @@ class NoSpace(Exception): class BuildNotPossible(Exception): """Raised when a bot cannot be built due to bad parameters.""" + class InvalidTicker(Exception): + """Raised when a ticker is invalid.""" + class BotConfigError: """Base class for bot config errors.""" diff --git a/django_napse/utils/errors/fleets.py b/django_napse/utils/errors/fleets.py index 09cd116f..af7acb58 100644 --- a/django_napse/utils/errors/fleets.py +++ b/django_napse/utils/errors/fleets.py @@ -6,3 +6,9 @@ class FleetNotSetupError(Exception): class InvalidOperator(Exception): """Raised when the operator is invalid.""" + + class InvalidShares(Exception): + """Raised when the sum of all shares is not 1.""" + + class InvalidClusters(Exception): + """Raised when the fleet has no clusters.""" diff --git a/django_napse/utils/trading/__init__.py b/django_napse/utils/trading/__init__.py index 67550fb5..715c2de8 100644 --- a/django_napse/utils/trading/__init__.py +++ b/django_napse/utils/trading/__init__.py @@ -1,3 +1 @@ from .binance_controller import BinanceController -from .processor import OrderProcessor -from .trading_utils import EXCHANGE_TOOLS, BinanceExchangeTools diff --git a/django_napse/utils/trading/processor.py b/django_napse/utils/trading/processor.py deleted file mode 100644 index dea2dd2d..00000000 --- a/django_napse/utils/trading/processor.py +++ /dev/null @@ -1,2 +0,0 @@ -class OrderProcessor: - pass diff --git a/django_napse/utils/trading/trading_utils.py b/django_napse/utils/trading/trading_utils.py deleted file mode 100644 index 39149e56..00000000 --- a/django_napse/utils/trading/trading_utils.py +++ /dev/null @@ -1,315 +0,0 @@ -from time import sleep - -from django_napse.core.models.bots.controller import Controller -from django_napse.utils.constants import DEFAULT_TAX -from django_napse.utils.trading.binance_controller import BinanceController -from django_napse.utils.usefull_functions import round_down - - -class BinanceExchangeTools: - """Class used to interact with Binance.""" - - def submit_order( - self, - order, - min_trade, - exchange_controller, - controller, - receipt, - executed_amounts_buy, - executed_amounts_sell, - ) -> tuple[dict, dict, dict]: - """Send the order to the binance exchange. - - Args: - ---- - order (Order): The Order to send. - min_trade (float): The minimum amount binance will accept for this pair. - exchange_controller (BinanceController): Either the NAPSE Binance account controller or the user's controller. - controller (Controller): The controller of the pair. - receipt (dict): Normally an empty dict. Used to store the receipt of the order. - executed_amounts_buy (dict): Normally an empty dict. Used to store the executed amounts of the buy order. - executed_amounts_sell (dict): Normally an empty dict. Used to store the executed amounts of the sell order. - - Returns: - ------- - receipt (dict): The receipt of the order. - executed_amounts_buy (dict): The executed amounts of the buy order. - executed_amounts_sell (dict): The executed amounts of the sell order. - - Steps: - ----- - 1. Send the buy component of the order. - 2. Send the sell component of the order. - """ - wallet = order.wallet if order.pk else None - receipt["BUY"], executed_amounts_buy = self.send_order_to_exchange( - side="BUY", - amount=order.buy_amount, - controller=controller, - min_trade=min_trade, - wallet=wallet, - price=order.price, - ) - receipt["SELL"], executed_amounts_sell = self.send_order_to_exchange( - side="SELL", - amount=order.sell_amount, - controller=controller, - min_trade=min_trade, - wallet=wallet, - price=order.price, - ) - return receipt, executed_amounts_buy, executed_amounts_sell - - def send_order_to_exchange( - self, - side, - price, - amount, - controller, - min_trade, - wallet=None, - ) -> tuple[dict, dict]: - """Place an order on the exchange. - - Args: - ---- - side (str): BUY or SELL - price (float): The price to buy or sell at (only used in testing environments). - amount (float): The amount to buy or sell. - controller (Controller): The controller of the pair. - min_trade (float): The minimum amount binance will accept for this pair. - wallet (Wallet, optional): The wallet to use for the order. Defaults to None. - - Returns: - ------- - receipt: The receipt of the order (given by binance, or calculated by us). - executed_amounts: The executed amounts of the order (how much the balance of the account has changed). - - Raises: - ------ - NotImplementedError: Real orders are not implemented yet. - """ - executed_amounts = {} - testing = wallet.testing if wallet else True - amount = round_down(amount, controller.lot_size) - if testing: - if side == "BUY": - if amount > min_trade: - receipt = self.test_order(amount, "BUY", price, quote=controller.quote, base=controller.base) - exec_quote = -float(receipt["cummulativeQuoteQty"]) - exec_base = 0 - for elem in receipt["fills"]: - exec_base += float(elem["qty"]) - float(elem["commission"]) - - executed_amounts[controller.quote] = exec_quote - executed_amounts[controller.base] = exec_base - else: - receipt = {"error": "Amount too low"} - executed_amounts = {} - - elif side == "SELL": - if amount > min_trade: - receipt = self.test_order(amount, "SELL", price, quote=controller.quote, base=controller.base) - exec_quote = float(receipt["cummulativeQuoteQty"]) - exec_base = -float(receipt["origQty"]) - for elem in receipt["fills"]: - exec_quote -= float(elem["commission"]) - executed_amounts[controller.quote] = exec_quote - executed_amounts[controller.base] = exec_base - else: - receipt = {"error": "Amount too low"} - executed_amounts = {} - else: - # TODO: implement real orders and public exchange GIL - error_msg = "IRL orders are not implemented yet. (failsafe to prevent accidental irl orders)." - raise NotImplementedError(error_msg) - - if wallet: - for ticker, amount in executed_amounts.items(): - if amount < 0: - wallet.spend(-amount, ticker) - else: - wallet.top_up(amount, ticker) - return receipt, executed_amounts - - def swap(self, wallet, amount, from_ticker, to_ticker, pair, price=None): - """Swap from one asset to another. - - amount is always the amount of the from_ticker asset, in the from_ticker currency. - - Args: - ---- - wallet (Wallet): The wallet to use for the swap. - amount (float): How much of the from_ticker asset to swap. - from_ticker (str): The ticker of the asset to swap from. - to_ticker (str): The ticker of the asset to swap to. - pair (str): The pair to use for the swap. - price (float, optional): Only used for testing. Defaults to None. - - Raises: - ------ - ValueError: If price is not specified in testing mode. - ValueError: If pair is not valid for from_ticker and to_ticker. - - Returns: - ------- - receipt (dict): The receipt of the swap. - executed_amounts (dict): A dictionnary that summarises the changes of your wallet. - """ - testing = wallet.testing - if testing and price is None: - error_msg = "Price must be specified in testing mode." - raise ValueError(error_msg) - if from_ticker + to_ticker == pair: - side = "SELL" - base = from_ticker - quote = to_ticker - elif to_ticker + from_ticker == pair: - side = "BUY" - base = to_ticker - quote = from_ticker - if not testing: - price = Controller.get_asset_price(base, quote) - amount /= price - else: - error_msg = f"Pair {pair} is not valid for {from_ticker} and {to_ticker}" - raise ValueError(error_msg) - - controller = Controller.get(base=base, quote=quote, exchange="BINANCE", interval="1m") - receipt = {} - executed_amounts = {} - - min_trade = controller.min_trade - receipt, executed_amounts = self.send_order_to_exchange( - side=side, - price=price, - amount=amount, - controller=controller, - min_trade=min_trade, - wallet=wallet, - ) - return receipt, executed_amounts - - @staticmethod - def current_free_assets(controller: BinanceController) -> dict: - """Get the current free assets of the account. - - Args: - ---- - controller (BinanceController): Used to communicate with Binance - - Returns: - ------- - dict: Free assets. Shape: {"asset": amount} - """ - assets = controller.get_info()["balances"] - current = {} - for elem in assets: - if float(elem["free"]) > 0: - current[elem.get("asset")] = float(elem.get("free")) - return current - - @staticmethod - def test_order(amount: float, side: str, price: float, base: str, quote: str) -> dict: - """Create a fictional receipt for a test order. - - Args: - ---- - amount (float): amount to buy or sell - side (str): BUY or SELL - price (float): current price - base (str): base ticker - quote (str): quote ticker - - Raises: - ------ - ValueError: If side is not BUY or SELL - - Returns: - ------- - dict: The fake receipt. - """ - if side not in ("BUY", "SELL"): - error_msg = f"Side must be BUY or SELL. Got {side}" - raise ValueError(error_msg) - - pair = base + quote - side_buy = side == "BUY" - - executed_qty = amount - cummulative_quote_qty = amount * price - commission = executed_qty * DEFAULT_TAX["BINANCE"] / 100 if side_buy else cummulative_quote_qty * DEFAULT_TAX["BINANCE"] / 100 - commission_asset = base if side_buy else quote - return { - "symbol": pair, - "orderId": None, - "orderListId": None, - "clientOrderId": None, - "transactTime": None, - "price": price, - "origQty": amount, - "executedQty": executed_qty, - "cummulativeQuoteQty": cummulative_quote_qty, - "status": "FILLED", - "timeInForce": "GTC", - "type": "MARKET", - "side": side, - "fills": [ - { - "price": price, - "qty": amount, - "commission": commission, - "commissionAsset": commission_asset, - "tradeId": None, - }, - ], - } - - def executed_amounts(self, receipt: dict, current_free_assets_dict: dict, controller, testing: bool) -> dict: - """Calculate the difference between the current free assets and the free assets before the order. - - Returns the executed amounts of said order. - - Args: - ---- - receipt (dict): The receipt of the order. - current_free_assets_dict (dict): The free assets before the order. - controller (BinanceController): Used to communicate with Binance - testing (bool): If the order is a test order. - - Returns: - ------- - dict: The executed amounts. Shape: {"asset": amount} - """ - executed = {} - commission = 0 - if receipt == {} or receipt is None: - return {} - if testing: - for fill in receipt.get("fills"): - commission += float(fill.get("commission")) - if receipt.get("side") == "BUY": - base = receipt.get("fills")[0].get("commissionAsset") - quote = receipt.get("symbol").replace(base, "") - executed[base] = float(receipt.get("executedQty")) * (1 - DEFAULT_TAX["BINANCE"] / 100) - executed[quote] = -float(receipt.get("cummulativeQuoteQty")) - elif receipt.get("side") == "SELL": - quote = receipt.get("fills")[0].get("commissionAsset") - base = receipt.get("symbol").replace(quote, "") - executed[base] = -float(receipt.get("executedQty")) - executed[quote] = float(receipt.get("cummulativeQuoteQty")) * (1 - DEFAULT_TAX["BINANCE"] / 100) - else: - while self.current_free_assets(controller) == current_free_assets_dict: - print("Waiting for order to be executed...") - sleep(0.01) - new_free_assets = self.current_free_assets(controller) - for asset, amount in new_free_assets.items(): - if amount != current_free_assets_dict.get(asset): - executed[asset] = amount - current_free_assets_dict.get(asset) - return executed - - -EXCHANGE_TOOLS = { - "BINANCE": BinanceExchangeTools, -} diff --git a/django_napse/utils/usefull_functions.py b/django_napse/utils/usefull_functions.py index c6f8f3fc..0f408083 100644 --- a/django_napse/utils/usefull_functions.py +++ b/django_napse/utils/usefull_functions.py @@ -5,11 +5,14 @@ def calculate_mbp(value: str, current_value: float, order, currencies: dict) -> float: - ticker, value = value.split("|") - value = value.replace("c", str(order.debited_amount - order.exit_amount_quote)) - value = value.replace("b", str(current_value if current_value is not None else 0)) - value = value.replace("a", str(currencies.get(ticker, {"amount": 0})["amount"])) - return eval(value) # noqa S307 + ticker, price = value.split("|") + price = float(price) + + current_amount = currencies.get(ticker, {"amount": 0})["amount"] + current_value = current_value if current_value is not None else 0 + received_quote = order.debited_amount - order.exit_amount_quote + print(current_amount, current_value, received_quote, price) + return (current_amount * current_value + received_quote) / (received_quote / price + current_amount) def process_value_from_type(value, target_type, **kwargs): diff --git a/pyproject.toml b/pyproject.toml index 6fcfbc0f..4999f81d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[tool.isort] +py_version=311 + + [tool.ruff] # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. diff --git a/requirements/core.txt b/requirements/core.txt index f1c8e5d5..63a2764c 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -4,7 +4,7 @@ django-celery-beat==2.5.0 # https://github.com/celery/django-celery-beat psycopg2-binary==2.9.7 # https://github.com/psycopg/psycopg2 celery==5.3.1 -redis==4.6.0 +redis==5.0.0 python-binance==1.0.19 # https://github.com/sammchardy/python-binance shortuuid==1.0.11 # https://github.com/skorokithakis/shortuuid diff --git a/test/django_tests/fleets/__init__.py b/test/django_tests/fleets/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/test/django_tests/fleets/__init__.py @@ -0,0 +1 @@ +from . import * diff --git a/test/django_tests/fleets/test_fleet.py b/test/django_tests/fleets/test_fleet.py new file mode 100644 index 00000000..07c04a8d --- /dev/null +++ b/test/django_tests/fleets/test_fleet.py @@ -0,0 +1,61 @@ +from django_napse.core.models import Bot, Controller, Credit, EmptyBotConfig, EmptyStrategy, Fleet, SinglePairArchitecture +from django_napse.utils.model_test_case import ModelTestCase + +""" +python test/test_app/manage.py test test.django_tests.fleets.test_fleet -v2 --keepdb --parallel +""" + + +class FleetTestCase: + model = Fleet + + def simple_create(self): + config = EmptyBotConfig.objects.create(space=self.space, settings={"empty": True}) + controller = Controller.get( + exchange_account=self.exchange_account, + base="BTC", + quote="USDT", + interval="1m", + ) + architecture = SinglePairArchitecture.objects.create(constants={"controller": controller}) + strategy = EmptyStrategy.objects.create(config=config, architecture=architecture) + bot = Bot.objects.create(name="Test Bot", strategy=strategy) + return self.model.objects.create( + name="Test Fleet", + exchange_account=self.exchange_account, + clusters=[ + { + "bot": bot, + "share": 0.7, + "breakpoint": 1000, + "autoscale": False, + }, + { + "bot": bot, + "share": 0.3, + "breakpoint": 1000, + "autoscale": True, + }, + ], + ) + + def test_invest(self): + Credit.objects.create(wallet=self.space.wallet, amount=1000, ticker="USDT") + fleet = self.simple_create() + connections = fleet.invest(self.space, 1000, "USDT") + self.assertEqual(self.space.wallet.get_amount("USDT"), 0) + self.assertEqual(connections[0].wallet.get_amount("USDT"), 700) + self.assertEqual(connections[1].wallet.get_amount("USDT"), 300) + + def test_invest_twice(self): + Credit.objects.create(wallet=self.space.wallet, amount=2000, ticker="USDT") + fleet = self.simple_create() + fleet.invest(self.space, 1000, "USDT") + connections = fleet.invest(self.space, 1000, "USDT") + self.assertEqual(self.space.wallet.get_amount("USDT"), 0) + self.assertEqual(connections[0].wallet.get_amount("USDT"), 1400) + self.assertEqual(connections[1].wallet.get_amount("USDT"), 600) + + +class FleetBINANCETestCase(FleetTestCase, ModelTestCase): + exchange = "BINANCE" diff --git a/test/django_tests/orders/test_orders.py b/test/django_tests/orders/test_order.py similarity index 97% rename from test/django_tests/orders/test_orders.py rename to test/django_tests/orders/test_order.py index ab6fecf1..2797e6d8 100644 --- a/test/django_tests/orders/test_orders.py +++ b/test/django_tests/orders/test_order.py @@ -3,7 +3,7 @@ from django_napse.utils.model_test_case import ModelTestCase """ -python test/test_app/manage.py test test.django_tests.orders.test_orders -v2 --keepdb --parallel +python test/test_app/manage.py test test.django_tests.orders.test_order -v2 --keepdb --parallel """ diff --git a/test/django_tests/simulations/test_simulation_queue.py b/test/django_tests/simulations/test_simulation_queue.py index 65aaeb4c..ae702809 100644 --- a/test/django_tests/simulations/test_simulation_queue.py +++ b/test/django_tests/simulations/test_simulation_queue.py @@ -43,9 +43,9 @@ def test_quick_simulation(self): simulation_queue = self.simple_create() simulation_queue.run_quick_simulation().info() - # def test_irl_simulation(self): - # simulation_queue = self.simple_create() - # simulation_queue.run_irl_simulation().info() + def test_irl_simulation(self): + simulation_queue = self.simple_create() + simulation_queue.run_irl_simulation().info() class SimulationQueueBINANCETestCase(SimulationQueueTestCase, ModelTestCase):