Implement profit view and fix auto refresh
This commit is contained in:
parent
2b13802009
commit
a39a5c3857
|
@ -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>/",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue