0

I have several functions in my utils.py file that my views use to calculate trade stats. Below I added just 3 but there are 8 and soon there will be several more. The way I currently have it set up works but it's very inefficient. You can see each function starts from the beginning and calls other functions that set similar variables.

What is the proper way to turn this into a class?

Is this even the right approach?


utils.py | multiple functions

def max_amount_function (pk):
    trade = get_object_or_404(Trade, pk=pk)
    entries_buy = Entry.objects.filter(trade=trade, entry_type="entry")
    max_amount_function = entries_buy.aggregate(max_amount_function=Sum('amount'))['max_amount_function']
    if max_amount_function is None:
        return 0
    else:
        return max_amount_function

def fees_function (pk):
    trade = get_object_or_404(Trade, pk=pk)
    entries = Entry.objects.filter(trade=trade)
    fees_function = entries.aggregate(fees_function=Sum('fee'))['fees_function']
    return fees_function

def entry_cpu_function (pk):
    if max_amount_function(pk) > 0:
        trade = get_object_or_404(Trade, pk=pk)
        entries_buy = Entry.objects.filter(trade=trade, entry_type="entry")
        entry_cpu_function = entries_buy.annotate(
            s=F('amount') * F('price')
        ).aggregate(
            entry_cpu_function=ExpressionWrapper(
                Sum(
                    Cast('s', output_field=models.FloatField())
                ) / Sum('amount'),
                output_field=models.FloatField()
            )
        )['entry_cpu_function']
        return entry_cpu_function
    else:
        return 0

utils.py | one class

class TradeStats:
    trade = get_object_or_404(Trade)
    entries_buy = Entry.objects.filter(trade=trade, entry_type="entry")

    def __init__(self):
        pass

    def max_amount_functio(pk):
        pass

    def fees_function(pk):
        pass

    def entry_cpu_function(self):
        pass

Edit added models.py as it may be relevant for the entries since it's a different class.

models.py

class Trade(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
    status = models.CharField(max_length=2, choices=STATUS_CHOICES, default='op')
    broker = models.ForeignKey(Broker, on_delete=models.CASCADE, blank=True, null=True)
    asset = models.ForeignKey(Asset, on_delete=models.CASCADE, null=True)
    ...

class Entry(models.Model):

    ENTRY = 'entry'
    EXIT = 'exit'
    ENTRY_TYPE_CHOICES = [
        (ENTRY, 'Entry'),
        (EXIT, 'Exit'),

    trade = models.ForeignKey(Trade, on_delete=models.CASCADE)
    amount = models.FloatField(null=True)
    price = models.FloatField(null=True, blank=True)
    entry_type = models.CharField(max_length=5, choices=ENTRY_TYPE_CHOICES, default=ENTRY)
    ...
Alex Winkler
  • 469
  • 4
  • 25

2 Answers2

2

You can move all of these functions under the model or custom QuerySet.

If you move to the model you can do something similar. (Code might not work, just given for your reference)

# MODEL
class Trade(models.Model):

    # Model fields here

    def get_entries(self):
        # NB: assuming you didn't specify a related_name
        # in the `Entry.trade` ForeignKey field. Else,
        # replace `entry_set` with said related_name.
        return self.entry_set.filter(entry_type="entry")


    def max_amount_function(self):
        max_amount_res = self.get_entries().aggregate(max_amount_function=Sum('amount'))['max_amount_function']
        if max_amount_res is None:
            return 0
        else:
            return max_amount_res

    def fees_function(self):
        return self.get_entries().aggregate(fees_function=Sum('fee'))['fees_function']

    def entry_cpu_function(self, ):

        if self.max_amount_function() <= 0:
            return 0

        entry_cpu_function = self.get_entries().annotate(
            s=F('amount') * F('price')
        ).aggregate(
            entry_cpu_function=ExpressionWrapper(
                Sum(
                    Cast('s', output_field=models.FloatField())
                ) / Sum('amount'),
                output_field=models.FloatField()
            )
        )['entry_cpu_function']
        return entry_cpu_function

Inside views.py, you can refer this functions like below -

trade = get_object_or_404(Trade, pk=pk)
trade.max_amount_function()
trade.fees_function()
trade.entry_cpu_function()
bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
RockStar
  • 1,304
  • 2
  • 13
  • 35
  • 1
    `self.trades.filter` (in `get_entries`) is very certainly wrong, unless the OP defined the "related_name" in `Entry.trade` as "trades" (which would be a wrong naming to start with). Since we don't have the `Trade` model definition, better to assume the "related_name" is not set and use the default one, so this should be `self.entry_set.filter`. – bruno desthuilliers Feb 17 '20 at 11:49
  • Also, while not technically wrong, the comma after `self` in the methods definitions is useless, probably misleading for newcomers, and totally unpythonic. – bruno desthuilliers Feb 17 '20 at 11:50
  • Agreed to both points, Bruno. I have answered by considering the related_name set. Thanks for the edit. – RockStar Feb 17 '20 at 14:17
  • Thanks for the big help so far. Really cool approach. I added the models.py to my question to hopefully address the 'entry_set' comment. Still working on the implementation. – Alex Winkler Feb 18 '20 at 08:30
  • Thanks Alex! Do not forget to accept answer if it helped you. – RockStar Feb 18 '20 at 08:32
  • @RockStar will do! Just working on the solution a bit more in case I run into some issues. Little side note is that to run through all the stats this page used to load 7+ seconds and already that time is much less. Thanks again! – Alex Winkler Feb 18 '20 at 09:03
2

Rockstar already posted the best solution in this context - you're working on a Django model and all your code depends on a specific model instance, so adding those features as methods in your Trade model is the very obvuous solution.

This being said, the question by itself (how do I turn a bunch of function relying on the same variables into a class) can also do with a more generic answer that apply to other contexts as well:

First, you want to fix this class definition so trades and entries_buy are instance attributes, not class attributes - else they will be initialised only once at module import time and you will end up with stale data. For this to work you will also need to add the pk param to your class initializer:

class TradeStats:
    def __init__(self, pk):
        self.trade = get_object_or_404(Trade, pk=pk)
        self.entries_buy = Entry.objects.filter(trade=self.trade, entry_type="entry")

Then you want to add self as first param to all your methods (else the current instance can't be passed - and remove the now useless 'pk' param:

def max_amount_function(self):
    pass

def fees_function(self):
    pass

def entry_cpu_function(self):
    pass

Now you can backport your methods implementations, replacing the relevant parts with accesses to self.trade and self.entries_buy, ie:

def max_amount_function(self):
    result = self.entries_buy.aggregate(max_amount_function=Sum('amount'))['max_amount_function']
    return 0 if result is None else result

etc...

Also, "xxx_yyy_function" is a very poor naming. As a general rule, you want to use nouns for objects (variables, values, class names, etc), and verbs for functions or methods, so one would expect something like get_max_amount, get_fees etc.

Finally, given your question (and what you second snippet looks like), I strongly suggest you spend some time on the official Python tutorial - the part about classes of course but you may learn a lot of otherwise useful things doing the whole tutorial.

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
  • Thanks for that detailed answer! I know I've got lots to work on and I've been reading the Python tutorial about classes but was still difficult to imagine it in my situation. I'm having a hard time deciding if the solution would be a standalone class like your example or inside my models (that's something I never even considered) such as Rockstar's example which you seem to also agree with is the best approach. In the future, a lot of statistics will be compiled from these functions so the most efficient would be ideal. – Alex Winkler Feb 18 '20 at 08:23
  • 1
    @AlexWinkler models are not only about data access, they are also about domain logic so going for "smart models" (instead of having this logic scattered in views / utility functions / etc) makes sense. For the example you gave (per-instance stats), the model is the most obvious "host". For "per-subset" stats (stats on a set of Trade objects, ie per year etc), the ModelManager would be the obvious host. Now if you have a lot of complex stats based on half a dozen models etc, you may indeed want to consider distinct dedicated classes. – bruno desthuilliers Feb 18 '20 at 08:44
  • 1
    wrt/ efficiency - for analytical computations on whole datasets at least - you probably want to avoid loading model instances at all and leverage your database computing power instead. Actually, you may even want an alternate representation of your data, cf [OLAP vs OLTP](https://stackoverflow.com/questions/21900185) – bruno desthuilliers Feb 18 '20 at 08:53
  • Looks like I got my reading material for the next few weekends haha thanks Bruno for all the help. I really appreciate it. (I've also updated the function names ) – Alex Winkler Feb 18 '20 at 09:01