Begin implementing better payment simulation

This commit is contained in:
Mark Veidemanis 2023-05-05 13:41:00 +01:00
parent 64fd072f2f
commit 35607898f0
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
6 changed files with 200 additions and 52 deletions

View File

@ -267,4 +267,9 @@ urlpatterns = [
linkgroups.LinkGroupInfo.as_view(), linkgroups.LinkGroupInfo.as_view(),
name="linkgroup_info", name="linkgroup_info",
), ),
path(
"links/<str:type>/simulate/<str:pk>/",
linkgroups.LinkGroupSimulation.as_view(),
name="linkgroup_simulate",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -47,6 +47,7 @@ class Money(object):
""" """
Run all the balance checks that output into ES in another thread. Run all the balance checks that output into ES in another thread.
""" """
# TODO: pass link group instead
if not all([user, nordigen, agora]): if not all([user, nordigen, agora]):
raise Exception raise Exception
@ -559,9 +560,15 @@ class Money(object):
# Add the platform payment # Add the platform payment
for platform in platforms: for platform in platforms:
# Get ratio of platform.throughput to the total platform throughput # Get ratio of platform.throughput to the total platform throughput
if total_throughput_platform == 0:
ratio = 0
else:
ratio = platform.throughput / total_throughput_platform ratio = platform.throughput / total_throughput_platform
platform_payment = cut_platform * ratio platform_payment = cut_platform * ratio
payees_length = len(platform.payees.all()) payees_length = len(platform.payees.all())
if payees_length == 0:
payment_per_payee = 0
else:
payment_per_payee = platform_payment / payees_length payment_per_payee = platform_payment / payees_length
for wallet in platform.payees.all(): for wallet in platform.payees.all():
if wallet not in pay_list: if wallet not in pay_list:
@ -575,9 +582,15 @@ class Money(object):
# Add the requisition payment # Add the requisition payment
for requisition in requisitions: for requisition in requisitions:
# Get ratio of requisition.throughput to the requisition cut # Get ratio of requisition.throughput to the requisition cut
if total_throughput_requisition == 0:
ratio = 0
else:
ratio = requisition.throughput / total_throughput_requisition ratio = requisition.throughput / total_throughput_requisition
req_payment = cut_req * ratio req_payment = cut_req * ratio
payees_length = len(requisition.payees.all()) payees_length = len(requisition.payees.all())
if payees_length == 0:
payment_per_payee = 0
else:
payment_per_payee = req_payment / payees_length payment_per_payee = req_payment / payees_length
for wallet in requisition.payees.all(): for wallet in requisition.payees.all():
if wallet not in pay_list: if wallet not in pay_list:
@ -590,5 +603,15 @@ class Money(object):
return pay_list return pay_list
def collapse_pay_list(self, pay_list):
"""
Collapse the pay list into a single dict of wallet: amount.
"""
collapsed = {}
for wallet, payments in pay_list.items():
collapsed[wallet] = sum([x[0] for x in payments])
return collapsed
money = Money() money = Money()

View File

@ -0,0 +1,45 @@
<h1 class="title">Simulation for $1000</h1>
{{ object }}
<p>
Assuming equal throughput for platforms and requisitions.
<strong>Note that this is just a simulation, equal throughput is highly unlikely.</strong>
</p>
<div class="columns">
<div class="column">
<div class="content">
<ul>
{% for key, list in simulation.items %}
<li>
{{ key.0 }}: ${{ key.1 }}
<ul>
{% for item in list %}
<li>${{ item.amount }} to {{ item.name }} at <code>{{ item.address }}</code></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
<h1 class="title is-4">Total for wallets</h1>
<div class="box">
{% for wallet, pay_list in pay_list.items %}
{{ wallet }}: ${{ pay_list.amount }}
<progress class="progress" value="{{ pay_list.amount }}" max="1000"></progress>
{% endfor %}
</div>
</div>
<div class="column">
<div class="box">
{% for key, list in simulation.items %}
<strong>{{ key.0 }}: ${{ key.1 }}</strong>
<progress class="progress" value="{{ key.1 }}" max="1000"></progress>
{% for item in list %}
<em>{{ item.name }}: ${{ item.amount }}</em><progress class="progress" value="{{ item.amount }}" max="{{ item.max }}"></progress>
{% endfor %}
{% endfor %}
</div>
</div>
</div>

View File

@ -89,47 +89,4 @@
</div> </div>
</div> </div>
<h1 class="title">Simulation for $1000</h1> {% include 'partials/linkgroup-info-sim.html' %}
<p>
Assuming equal throughput for platforms and requisitions.
<strong>Note that this is just a simulation, equal throughput is highly unlikely.</strong>
</p>
<div class="columns">
<div class="column">
<div class="content">
<ul>
{% for key, list in simulation.items %}
<li>
{{ key.0 }}: ${{ key.1 }}
<ul>
{% for item in list %}
<li>${{ item.amount }} to {{ item.name }} at <code>{{ item.address }}</code></li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
<h1 class="title is-4">Total for wallets</h1>
<div class="box">
{% for wallet, pay_list in pay_list.items %}
{{ wallet }}: ${{ pay_list.amount }}
<progress class="progress" value="{{ pay_list.amount }}" max="1000"></progress>
{% endfor %}
</div>
</div>
<div class="column">
<div class="box">
{% for key, list in simulation.items %}
<strong>{{ key.0 }}: ${{ key.1 }}</strong>
<progress class="progress" value="{{ key.1 }}" max="1000"></progress>
{% for item in list %}
<em>{{ item.name }}: ${{ item.amount }}</em><progress class="progress" value="{{ item.amount }}" max="{{ item.max }}"></progress>
{% endfor %}
{% endfor %}
</div>
</div>
</div>

View File

@ -237,3 +237,20 @@ class TestMoney(AggregatorPlatformMixin, TransactionTestCase):
amount, expected_requisition[wallets_requisition.index(wallet)] amount, expected_requisition[wallets_requisition.index(wallet)]
) )
self.assertIn("Requisition", detail) self.assertIn("Requisition", detail)
def test_collapse_pay_list(self):
self.create_wallets()
test_data = {
"WALLET1": [
(500.0, "Operator cut for 2 of 1 operators, total 1000.0"),
(500.0, "Operator cut for 1 of 2 operators, total 1000.0"),
],
"WALLET2": [(0.0, "Platform Live cut for 1 of 1 payees, total 500.0")],
}
expected = {
"WALLET1": 1000.0,
"WALLET2": 0.0,
}
collapsed = money.collapse_pay_list(test_data)
self.assertDictEqual(collapsed, expected)

View File

@ -1,5 +1,6 @@
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse
from mixins.views import ( from mixins.views import (
ObjectCreate, ObjectCreate,
ObjectDelete, ObjectDelete,
@ -10,8 +11,19 @@ from mixins.views import (
from rest_framework import status from rest_framework import status
from two_factor.views.mixins import OTPRequiredMixin from two_factor.views.mixins import OTPRequiredMixin
from core.clients.aggregators.nordigen import NordigenClient
from core.clients.platforms.agora import AgoraClient
from core.forms import LinkGroupForm from core.forms import LinkGroupForm
from core.models import Aggregator, LinkGroup, Platform, Requisition from core.lib.money import Money
from core.models import (
Aggregator,
LinkGroup,
OperatorWallets,
Platform,
Requisition,
User,
)
from core.views.helpers import synchronize_async_helper
class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead): class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
@ -32,6 +44,25 @@ class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
def get_context_data(self): def get_context_data(self):
context = super().get_context_data() context = super().get_context_data()
self.extra_buttons = [
{
"url": "#",
"action": "withdraw",
"method": "get",
"label": "Withdraw profit",
"icon": "fa-solid fa-money-bill-transfer",
},
{
"url": reverse(
"linkgroup_simulate", kwargs={"pk": self.object.id, "type": "modal"}
),
"action": "simulate",
"method": "get",
"label": "Simulate withdrawal",
"icon": "fa-solid fa-play",
},
]
aggregators = Aggregator.objects.filter( aggregators = Aggregator.objects.filter(
user=self.request.user, user=self.request.user,
link_group=self.object, link_group=self.object,
@ -50,8 +81,8 @@ class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
context["linkgroup"] = self.object context["linkgroup"] = self.object
payees = self.object.payees() payees = self.object.payees()
simulation = {} simulation = {}
profit = 1000 profit = 1000
profit_platform = profit * (self.object.platform_owner_cut_percentage / 100) profit_platform = profit * (self.object.platform_owner_cut_percentage / 100)
profit_requisition = profit * ( profit_requisition = profit * (
@ -95,12 +126,35 @@ class LinkGroupInfo(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
pay_list[payee] = dict(cast) pay_list[payee] = dict(cast)
requisition_pay_list.append(cast) requisition_pay_list.append(cast)
operator_pay_list = []
staff = User.objects.filter(
is_staff=True,
)
for user in staff:
wallets, _ = OperatorWallets.objects.get_or_create(user=user)
total_wallets = len(wallets.payees.all())
# Select all OperatorWallet instances with any distinct user attributes
for payee in wallets.payees.all():
cast = {
"name": payee.name,
"address": payee.address,
"amount": profit_operator / total_wallets,
"max": profit_operator,
}
print("CAST", cast)
if user not in pay_list:
pay_list[payee] = {}
if "amount" in pay_list[payee]:
pay_list[payee]["amount"] += cast["amount"]
else:
pay_list[payee] = dict(cast)
operator_pay_list.append(cast)
simulation[("Platform", profit_platform)] = platform_pay_list simulation[("Platform", profit_platform)] = platform_pay_list
simulation[("Requisition", profit_requisition)] = requisition_pay_list simulation[("Requisition", profit_requisition)] = requisition_pay_list
simulation[("Operator", profit_operator)] = [] simulation[("Operator", profit_operator)] = operator_pay_list
context["pay_list"] = pay_list context["pay_list"] = pay_list
context["simulation"] = simulation context["simulation"] = simulation
return context return context
@ -134,3 +188,50 @@ class LinkGroupUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
class LinkGroupDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete): class LinkGroupDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
model = LinkGroup model = LinkGroup
class LinkGroupSimulation(LoginRequiredMixin, OTPRequiredMixin, ObjectRead):
context_object_name_singular = "linkgroupsim"
context_object_name = "linkgroupsim"
detail_template = "partials/linkgroup-info-sim.html"
def get_object(self, **kwargs):
pk = self.kwargs.get("pk")
linkgroup = LinkGroup.objects.filter(
user=self.request.user,
id=pk,
).first()
if not linkgroup:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
money = Money()
checks = synchronize_async_helper(
money.check_all(
user=self.request.user, nordigen=NordigenClient, agora=AgoraClient
)
)
print("CHECKS", checks)
aggregators = Aggregator.objects.filter(
user=self.request.user,
link_group=self.object,
)
platforms = Platform.objects.filter(
user=self.request.user,
link_group=self.object,
)
requisitions = Requisition.objects.filter(
user=self.request.user,
aggregator__in=aggregators,
)
pay_list = money.get_pay_list(
linkgroup,
requisitions,
platforms,
self.request.user,
checks["total_profit"],
)
print("PAY LIST", pay_list)
collapsed = money.collapse_pay_list(pay_list)
print("COLLAPSED", collapsed)
return collapsed