Implement profit view and fix auto refresh

This commit is contained in:
Mark Veidemanis 2022-11-29 07:20:39 +00:00
parent 2b13802009
commit a39a5c3857
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
10 changed files with 192 additions and 84 deletions

View File

@ -28,6 +28,7 @@ from core.views import (
hooks, hooks,
limits, limits,
positions, positions,
profit,
signals, signals,
strategies, strategies,
trades, trades,
@ -137,6 +138,7 @@ urlpatterns = [
trades.TradeDeleteAll.as_view(), trades.TradeDeleteAll.as_view(),
name="trade_delete_all", name="trade_delete_all",
), ),
path("profit/<str:type>/", profit.Profit.as_view(), name="profit"),
path("positions/<str:type>/", positions.Positions.as_view(), name="positions"), path("positions/<str:type>/", positions.Positions.as_view(), name="positions"),
path( path(
"positions/<str:type>/<str:account_id>/", "positions/<str:type>/<str:account_id>/",

View File

@ -202,6 +202,18 @@
Home Home
</a> </a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
View
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'profit' type='page' %}">
Profit by account
</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
Manage Manage
@ -215,8 +227,6 @@
</a> </a>
</div> </div>
</div> </div>
{% endif %}
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
Setup Setup

View File

@ -4,7 +4,7 @@
{% block outer_content %} {% block outer_content %}
<div class="grid-stack" id="grid-stack-main"> <div class="grid-stack" id="grid-stack-main">
<div class="grid-stack-item" gs-w="5" gs-h="14" gs-y="0" gs-x="1"> <!-- <div class="grid-stack-item" gs-w="5" gs-h="14" gs-y="0" gs-x="1">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
@ -16,7 +16,7 @@
</article> </article>
</nav> </nav>
</div> </div>
</div> </div> -->
<div class="grid-stack-item" gs-w="4" gs-h="25" gs-y="0" gs-x="6"> <div class="grid-stack-item" gs-w="4" gs-h="25" gs-y="0" gs-x="6">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
@ -31,86 +31,72 @@
</div> </div>
</div> </div>
<!-- <div class="grid-stack-item" gs-w="5" gs-h="14" gs-y="14" gs-x="1"> <script>
<div class="grid-stack-item-content"> var grid = GridStack.init({
<nav class="panel"> cellHeight: 20,
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> cellWidth: 50,
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i> cellHeightUnit: 'px',
Offset auto: true,
</p> float: true,
<article class="panel-block is-active"> draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
{# include 'window-content/offset.html' #} removable: false,
</article> animate: true,
</nav> });
</div> // GridStack.init();
</div> -->
</div>
// a widget is ready to be loaded
document.addEventListener('load-widget', function(event) {
let container = htmx.find('#widget');
// get the scripts, they won't be run on the new element so we need to eval them
var scripts = htmx.findAll(container, "script");
let widgetelement = container.firstElementChild.cloneNode(true);
var new_id = widgetelement.id;
<script> // check if there's an existing element like the one we want to swap
var grid = GridStack.init({ let grid_element = htmx.find('#grid-stack-main');
cellHeight: 20, let existing_widget = htmx.find(grid_element, "#"+new_id);
cellWidth: 50,
cellHeightUnit: 'px',
auto: true,
float: true,
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
removable: false,
animate: true,
});
// GridStack.init();
// a widget is ready to be loaded // get the size and position attributes
document.addEventListener('load-widget', function(event) { if (existing_widget) {
let container = htmx.find('#widget'); let attrs = existing_widget.getAttributeNames();
// get the scripts, they won't be run on the new element so we need to eval them for (let i = 0, len = attrs.length; i < len; i++) {
var scripts = htmx.findAll(container, "script"); if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
let widgetelement = container.firstElementChild.cloneNode(true); widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
var new_id = widgetelement.id; }
// check if there's an existing element like the one we want to swap
let grid_element = htmx.find('#grid-stack-main');
let existing_widget = htmx.find(grid_element, "#"+new_id);
// get the size and position attributes
if (existing_widget) {
let attrs = existing_widget.getAttributeNames();
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
} }
} }
} // clear the queue element
// clear the queue element container.outerHTML = "";
container.outerHTML = ""; grid.addWidget(widgetelement);
grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid // re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement); htmx.process(widgetelement);
// update the size of the widget according to its content // update the size of the widget according to its content
var added_widget = htmx.find(grid_element, "#"+new_id); var added_widget = htmx.find(grid_element, "#"+new_id);
var itemContent = htmx.find(added_widget, ".control"); var itemContent = htmx.find(added_widget, ".control");
var scrollheight = itemContent.scrollHeight+80; var scrollheight = itemContent.scrollHeight+80;
var verticalmargin = 0; var verticalmargin = 0;
var cellheight = grid.opts.cellHeight; var cellheight = grid.opts.cellHeight;
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin)); var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
var opts = { var opts = {
h: height, h: height,
} }
grid.update( grid.update(
added_widget, added_widget,
opts opts
); );
// run the JS scripts inside the added element again // run the JS scripts inside the added element again
for (var i = 0; i < scripts.length; i++) { for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML); eval(scripts[i].innerHTML);
} }
}); });
</script> </script>
<script> <div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
</script> hx-get="{% url 'profit' type='widget' %}"
hx-target="#widgets-here"
hx-trigger="load"></div>
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body" hx-trigger="{{ context_object_name_singular }}Event from:body, every 5s"
hx-get="{{ list_url }}"> hx-get="{{ list_url }}">
<thead> <thead>
<th>account</th> <th>account</th>

View File

@ -0,0 +1,30 @@
{% include 'partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body, every 3s"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>name</th>
<th>P/L</th>
<th>balance</th>
<th>currency</th>
</thead>
{% for item in object_list %}
<tr class="
{% if item.pl > 0 %}has-background-success-light
{% elif item.pl < 0 %}has-background-danger-light
{% endif %}">
<td>{{ item.account.id }}</td>
<td>{{ item.account.name }}</td>
<td>{{ item.pl }}</td>
<td>{{ item.balance }}</td>
<td>{{ item.currency }}</td>
</tr>
{% endfor %}
</table>

View File

@ -1,5 +1,5 @@
<div id="widget"> <div id="widget">
<div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}gs-w="10" gs-h="1" gs-y="10" gs-x="1"{% endblock %}> <div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}gs-w="5" gs-h="1" gs-y="0" gs-x="1"{% endblock %}>
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">

View File

@ -153,7 +153,9 @@ class ObjectList(RestrictedViewMixin, ObjectNameMixin, ListView):
# Return partials for HTMX # Return partials for HTMX
if self.request.htmx: if self.request.htmx:
if orig_type == "page": if request.headers["HX-Target"] == self.context_object_name + "-table":
self.template_name = self.list_template
elif orig_type == "page":
self.template_name = self.list_template self.template_name = self.list_template
else: else:
context["window_content"] = self.list_template context["window_content"] = self.list_template

View File

@ -22,7 +22,7 @@ class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
"api_key", "api_key",
"sandbox", "sandbox",
"supported_symbols", "supported_symbols",
"instruments", # "instruments",
] ]
allowed_types = ["modal", "widget", "window", "page"] allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/account-info.html" window_content = "window-content/account-info.html"

View File

@ -61,6 +61,8 @@ class Positions(LoginRequiredMixin, OTPRequiredMixin, View):
list_template = "partials/position-list.html" list_template = "partials/position-list.html"
page_title = "Live positions from all exchanges" page_title = "Live positions from all exchanges"
page_subtitle = "Manual trades are editable under 'Bot Trades' tab." page_subtitle = "Manual trades are editable under 'Bot Trades' tab."
context_object_name_singular = "position"
context_object_name = "positions"
def get(self, request, type, account_id=None): def get(self, request, type, account_id=None):
if type not in self.allowed_types: if type not in self.allowed_types:
@ -89,12 +91,14 @@ class Positions(LoginRequiredMixin, OTPRequiredMixin, View):
"page_title": self.page_title, "page_title": self.page_title,
"page_subtitle": self.page_subtitle, "page_subtitle": self.page_subtitle,
"list_url": list_url, "list_url": list_url,
"context_object_name_singular": "position", "context_object_name_singular": self.context_object_name_singular,
"context_object_name": "positions", "context_object_name": self.context_object_name,
} }
# Return partials for HTMX # Return partials for HTMX
if self.request.htmx: if self.request.htmx:
if orig_type == "page": if request.headers["HX-Target"] == self.context_object_name + "-table":
self.template_name = self.list_template
elif orig_type == "page":
self.template_name = self.list_template self.template_name = self.list_template
else: else:
context["window_content"] = self.list_template context["window_content"] = self.list_template

74
core/views/profit.py Normal file
View File

@ -0,0 +1,74 @@
import uuid
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest
from django.shortcuts import render
from django.urls import reverse
from django.views import View
from rest_framework.parsers import FormParser
from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError
from core.models import Account, Trade
from core.util import logs
log = logs.get_logger(__name__)
def get_profit(user):
items = []
accounts = Account.objects.filter(user=user)
for account in accounts:
try:
details = account.client.get_account()
pl = details["pl"]
item = {"account": account, "pl": float(pl), "balance": details["balance"], "currency": details["currency"]}
items.append(item)
except GenericAPIError:
continue
return items
class Profit(LoginRequiredMixin, OTPRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/objects.html"
list_template = "partials/profit-list.html"
page_title = "Profit by account"
page_subtitle = None
context_object_name_singular = "profit"
context_object_name = "profit"
def get(self, request, type):
if type not in self.allowed_types:
return HttpResponseBadRequest
self.template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
items = get_profit(request.user)
orig_type = type
if type == "page":
type = "modal"
cast = {
"type": orig_type,
}
list_url = reverse("profit", kwargs={**cast})
context = {
"title": f"Profit ({type})",
"unique": unique,
"window_content": self.window_content,
"list_template": self.list_template,
"object_list": items,
"type": type,
"page_title": self.page_title,
"page_subtitle": self.page_subtitle,
"list_url": list_url,
"context_object_name_singular": self.context_object_name_singular,
"context_object_name": self.context_object_name,
}
# Return partials for HTMX
if self.request.htmx:
if request.headers["HX-Target"] == self.context_object_name+"-table":
self.template_name = self.list_template
elif orig_type == "page":
self.template_name = self.list_template
else:
context["window_content"] = self.list_template
return render(request, self.template_name, context)