diff --git a/app/urls.py b/app/urls.py index 583eb68..5fbe919 100644 --- a/app/urls.py +++ b/app/urls.py @@ -21,7 +21,7 @@ from django.urls import include, path from django.views.generic import TemplateView from django_otp.forms import OTPAuthenticationForm -from core.views import base, callbacks, hooks +from core.views import accounts, base, callbacks, hooks, trades from core.views.stripe_callbacks import Callback urlpatterns = [ @@ -72,4 +72,47 @@ urlpatterns = [ name="callbacks", ), path("callbacks//", callbacks.Callbacks.as_view(), name="callbacks"), + path("accounts//", accounts.Accounts.as_view(), name="accounts"), + path( + "accounts//add/", + accounts.AccountAction.as_view(), + name="account_action", + ), + path( + "accounts//add//", + accounts.AccountAction.as_view(), + name="account_action", + ), + path( + "accounts//del//", + accounts.AccountAction.as_view(), + name="account_action", + ), + path( + "accounts//edit//", + accounts.AccountAction.as_view(), + name="account_action", + ), + path("trades//", trades.Trades.as_view(), name="trades"), + path("trades//add/", trades.TradeAction.as_view(), name="trade_action"), + path( + "trades///", + trades.Trades.as_view(), + name="trades", + ), + path( + "trades//add//", + trades.TradeAction.as_view(), + name="trade_action", + ), + path( + "trades//del//", + trades.TradeAction.as_view(), + name="trade_action", + ), + path( + "trades//edit//", + trades.TradeAction.as_view(), + name="trade_action", + ), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/forms.py b/core/forms.py index f6fde84..b832d7b 100644 --- a/core/forms.py +++ b/core/forms.py @@ -2,7 +2,7 @@ from django import forms from django.contrib.auth.forms import UserCreationForm from django.forms import ModelForm -from .models import Hook, User +from .models import Account, Hook, Trade, User # Create your forms here. @@ -42,3 +42,27 @@ class HookForm(ModelForm): "name", "hook", ) + + +class AccountForm(ModelForm): + class Meta: + model = Account + fields = ( + "exchange", + "api_key", + "api_secret", + ) + + +class TradeForm(ModelForm): + class Meta: + model = Trade + fields = ( + "account", + "symbol", + "type", + "amount", + "price", + "stop_loss", + "take_profit", + ) diff --git a/core/migrations/0007_account.py b/core/migrations/0007_account.py new file mode 100644 index 0000000..f35cf47 --- /dev/null +++ b/core/migrations/0007_account.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.2 on 2022-10-17 17:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_remove_callback_market_alter_callback_timestamp_sent_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('exchange', models.CharField(max_length=255)), + ('api_key', models.CharField(max_length=255)), + ('api_secret', models.CharField(max_length=255)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/0008_trade.py b/core/migrations/0008_trade.py new file mode 100644 index 0000000..b758e95 --- /dev/null +++ b/core/migrations/0008_trade.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.2 on 2022-10-17 17:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_account'), + ] + + operations = [ + migrations.CreateModel( + name='Trade', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('symbol', models.CharField(max_length=255)), + ('type', models.CharField(max_length=255)), + ('amount', models.FloatField()), + ('price', models.FloatField()), + ('stop_loss', models.FloatField(blank=True, null=True)), + ('take_profit', models.FloatField(blank=True, null=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')), + ('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')), + ], + ), + ] diff --git a/core/models.py b/core/models.py index 0fafea4..800d718 100644 --- a/core/models.py +++ b/core/models.py @@ -66,6 +66,14 @@ class User(AbstractUser): return plan in plan_list +class Account(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + exchange = models.CharField(max_length=255) + api_key = models.CharField(max_length=255) + api_secret = models.CharField(max_length=255) + + class Session(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) request = models.CharField(max_length=255, null=True, blank=True) @@ -81,6 +89,17 @@ class Hook(models.Model): received = models.IntegerField(default=0) +class Trade(models.Model): + account = models.ForeignKey(Account, on_delete=models.CASCADE) + hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True) + symbol = models.CharField(max_length=255) + type = models.CharField(max_length=255) + amount = models.FloatField() + price = models.FloatField() + stop_loss = models.FloatField(null=True, blank=True) + take_profit = models.FloatField(null=True, blank=True) + + class Callback(models.Model): hook = models.ForeignKey(Hook, on_delete=models.CASCADE) title = models.CharField(max_length=1024, null=True, blank=True) diff --git a/core/templates/base.html b/core/templates/base.html index 086dc68..3e5b322 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -201,6 +201,16 @@ Home + {% if user.is_authenticated %} + + Accounts + + {% endif %} + {% if user.is_authenticated %} + + Trades + + {% endif %} {% if user.is_authenticated %} Hooks diff --git a/core/templates/index.html b/core/templates/index.html index be7701f..b7b1baf 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -4,7 +4,7 @@ {% block outer_content %}
-
+
- - + + Accounts + + + + + + +
+ diff --git a/core/templates/window-content/trades.html b/core/templates/window-content/trades.html new file mode 100644 index 0000000..c6f6574 --- /dev/null +++ b/core/templates/window-content/trades.html @@ -0,0 +1,20 @@ +
+ +
+ + {% include 'partials/notify.html' %} + {% include 'partials/trade-list.html' %} + + \ No newline at end of file diff --git a/core/views/accounts.py b/core/views/accounts.py new file mode 100644 index 0000000..b66f7cc --- /dev/null +++ b/core/views/accounts.py @@ -0,0 +1,163 @@ +import uuid + +import orjson +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse, HttpResponseBadRequest +from django.shortcuts import render +from django.views import View +from rest_framework.parsers import FormParser, JSONParser +from rest_framework.views import APIView +from serde import ValidationError + +from core.forms import AccountForm +from core.lib.serde import drakdoo +from core.models import Callback, Account +from core.util import logs + +log = logs.get_logger(__name__) + + +def get_accounts(user): + accounts = Account.objects.filter(user=user) + return accounts + + +class Accounts(LoginRequiredMixin, View): + allowed_types = ["modal", "widget", "window", "page"] + window_content = "window-content/accounts.html" + + async def get(self, request, type): + if type not in self.allowed_types: + return HttpResponseBadRequest + template_name = f"wm/{type}.html" + unique = str(uuid.uuid4())[:8] + accounts = get_accounts(request.user) + if type == "page": + type = "modal" + context = { + "title": f"Accounts ({type})", + "unique": unique, + "window_content": self.window_content, + "items": accounts, + "type": type, + } + return render(request, template_name, context) + + +class AccountAction(LoginRequiredMixin, APIView): + allowed_types = ["modal", "widget", "window", "page"] + window_content = "window-content/add-account.html" + parser_classes = [FormParser] + + def get(self, request, type, account_id=None): + """ + Get the form for adding or editing a account. + :param account_id: The id of the account to edit. Optional. + """ + if type not in self.allowed_types: + return HttpResponseBadRequest + template_name = f"wm/{type}.html" + unique = str(uuid.uuid4())[:8] + if account_id: + try: + account = Account.objects.get(id=account_id, user=request.user) + form = AccountForm(instance=account) + except Account.DoesNotExist: + message = "Account does not exist" + message_class = "danger" + context = { + "message": message, + "message_class": message_class, + "window_content": self.window_content, + } + return render(request, template_name, context) + else: + form = AccountForm() + if type == "page": + type = "modal" + context = { + "form": form, + "account_id": account_id, + "type": type, + "unique": unique, + "window_content": self.window_content, + } + + return render(request, template_name, context) + + def put(self, request, type, account_id=None): + """ + Add or edit a account. + :param account_id: The id of the account to edit. Optional. + """ + if type not in self.allowed_types: + return HttpResponseBadRequest + message = None + message_class = "success" + + if account_id: + try: + form = AccountForm(request.data, instance=account.objects.get(id=account_id)) + except account.DoesNotExist: + message = "Account does not exist" + message_class = "danger" + context = { + "message": message, + "class": message_class, + } + return render(request, self.template_name, context) + else: + form = AccountForm(request.data) + if form.is_valid(): + account = form.save(commit=False) + account.user = request.user + account.save() + if account_id: + message = f"Account {account_id} edited successfully" + else: + message = f"Account {account.id} added successfully" + else: + message = "Error adding account" + message_class = "danger" + + accounts = get_accounts(request.user) + + context = { + "items": accounts, + "type": type, + } + if message: + context["message"] = message + context["class"] = message_class + template_name = "partials/account-list.html" + return render(request, template_name, context) + + def delete(self, request, type, account_id): + """ + Delete a account. + :param account_id: The id of the account to delete. + """ + if type not in self.allowed_types: + return HttpResponseBadRequest + message = None + message_class = "success" + try: + account = Account.objects.get(id=account_id, user=request.user) + account.delete() + message = "Account deleted successfully" + except Account.DoesNotExist: + message = "Error deleting account" + message_class = "danger" + + accounts = get_accounts(request.user) + + context = { + "items": accounts, + "type": type, + } + if message: + context["message"] = message + context["class"] = message_class + + template_name = "partials/account-list.html" + return render(request, template_name, context) diff --git a/core/views/callbacks.py b/core/views/callbacks.py index 7cc5b6e..9bea58a 100644 --- a/core/views/callbacks.py +++ b/core/views/callbacks.py @@ -8,11 +8,11 @@ from django.views import View from core.models import Callback, Hook -def get_callbacks(hook=None, user=None): - if user: +def get_callbacks(user, hook=None): + if hook: + callbacks = Callback.objects.filter(hook=hook, hook__user=user) + else: callbacks = Callback.objects.filter(hook__user=user) - elif hook: - callbacks = Callback.objects.filter(hook=hook) return callbacks @@ -38,9 +38,9 @@ class Callbacks(LoginRequiredMixin, View): "type": type, } return render(request, template_name, context) - callbacks = get_callbacks(hook) + callbacks = get_callbacks(request.user, hook) else: - callbacks = get_callbacks(user=request.user) + callbacks = get_callbacks(request.user) if type == "page": type = "modal" diff --git a/core/views/trades.py b/core/views/trades.py new file mode 100644 index 0000000..ed865d7 --- /dev/null +++ b/core/views/trades.py @@ -0,0 +1,179 @@ +import uuid + +import orjson +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpResponse, HttpResponseBadRequest +from django.shortcuts import render +from django.views import View +from rest_framework.parsers import FormParser, JSONParser +from rest_framework.views import APIView +from serde import ValidationError + +from core.forms import TradeForm +from core.lib.serde import drakdoo +from core.models import Callback, Trade, Account +from core.util import logs + +log = logs.get_logger(__name__) + + +def get_trades(user, account=None): + if user: + trades = Trade.objects.filter(account__user=user) + elif account: + trades = Trade.objects.filter(account=account, account__user=user) + return trades + + +class Trades(LoginRequiredMixin, View): + allowed_types = ["modal", "widget", "window", "page"] + window_content = "window-content/trades.html" + + async def get(self, request, type, account_id=None): + if type not in self.allowed_types: + return HttpResponseBadRequest + template_name = f"wm/{type}.html" + unique = str(uuid.uuid4())[:8] + if account_id: + try: + trades = Account.objects.get(id=account_id, user=request.user) + except Account.DoesNotExist: + message = "Account does not exist." + message_class = "danger" + context = { + "message": message, + "class": message_class, + "type": type, + } + return render(request, template_name, context) + trades = get_trades(request.user, account_id) + else: + trades = get_trades(request.user) + if type == "page": + type = "modal" + context = { + "title": f"Trades ({type})", + "unique": unique, + "window_content": self.window_content, + "items": trades, + "type": type, + } + return render(request, template_name, context) + + +class TradeAction(LoginRequiredMixin, APIView): + allowed_types = ["modal", "widget", "window", "page"] + window_content = "window-content/add-trade.html" + parser_classes = [FormParser] + + def get(self, request, type, trade_id=None): + """ + Get the form for adding or editing a trade. + :param trade_id: The id of the trade to edit. Optional. + """ + if type not in self.allowed_types: + return HttpResponseBadRequest + template_name = f"wm/{type}.html" + unique = str(uuid.uuid4())[:8] + if trade_id: + try: + trade = Trade.objects.get(id=trade_id, account__user=request.user) + form = TradeForm(instance=trade) + except Trade.DoesNotExist: + message = "Trade does not exist" + message_class = "danger" + context = { + "message": message, + "message_class": message_class, + "window_content": self.window_content, + } + return render(request, template_name, context) + else: + form = TradeForm() + if type == "page": + type = "modal" + context = { + "form": form, + "trade_id": trade_id, + "type": type, + "unique": unique, + "window_content": self.window_content, + } + + return render(request, template_name, context) + + def put(self, request, type, trade_id=None): + """ + Add or edit a trade. + :param trade_id: The id of the trade to edit. Optional. + """ + if type not in self.allowed_types: + return HttpResponseBadRequest + message = None + message_class = "success" + + if trade_id: + try: + form = TradeForm(request.data, instance=Trade.objects.get(id=trade_id)) + except Trade.DoesNotExist: + message = "Trade does not exist" + message_class = "danger" + context = { + "message": message, + "class": message_class, + } + return render(request, self.template_name, context) + else: + form = TradeForm(request.data) + if form.is_valid(): + trade = form.save(commit=False) + trade.save() + if trade_id: + message = f"Trade {trade_id} edited successfully" + else: + message = f"Trade {trade.id} added successfully" + else: + message = "Error adding trade" + message_class = "danger" + + trades = get_trades(request.user) + + context = { + "items": trades, + "type": type, + } + if message: + context["message"] = message + context["class"] = message_class + template_name = "partials/trade-list.html" + return render(request, template_name, context) + + def delete(self, request, type, trade_id): + """ + Delete a trade. + :param trade_id: The id of the trade to delete. + """ + if type not in self.allowed_types: + return HttpResponseBadRequest + message = None + message_class = "success" + try: + trade = Trade.objects.get(id=trade_id, account__user=request.user) + trade.delete() + message = "trade deleted successfully" + except Trade.DoesNotExist: + message = "Error deleting trade" + message_class = "danger" + + trades = get_trades(request.user) + + context = { + "items": trades, + "type": type, + } + if message: + context["message"] = message + context["class"] = message_class + + template_name = "partials/trade-list.html" + return render(request, template_name, context) diff --git a/docker/prod/requirements.prod.txt b/docker/prod/requirements.prod.txt index 242d4e9..bf37ea0 100644 --- a/docker/prod/requirements.prod.txt +++ b/docker/prod/requirements.prod.txt @@ -16,3 +16,4 @@ orjson django-otp qrcode serde[ext] +ccxt