Implement viewing live and DB information from account

This commit is contained in:
Mark Veidemanis 2022-10-21 23:57:32 +01:00
parent 1bdd49ee6a
commit 572b839c2c
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
10 changed files with 125 additions and 20 deletions

View File

@ -93,6 +93,11 @@ urlpatterns = [
accounts.AccountAction.as_view(), accounts.AccountAction.as_view(),
name="account_action", name="account_action",
), ),
path(
"accounts/<str:type>/info/<str:account_id>/",
accounts.AccountInfo.as_view(),
name="account_info",
),
path("trades/<str:type>/", trades.Trades.as_view(), name="trades"), path("trades/<str:type>/", trades.Trades.as_view(), name="trades"),
path("trades/<str:type>/add/", trades.TradeAction.as_view(), name="trade_action"), path("trades/<str:type>/add/", trades.TradeAction.as_view(), name="trade_action"),
path( path(

View File

@ -48,6 +48,7 @@ class AccountForm(ModelForm):
class Meta: class Meta:
model = Account model = Account
fields = ( fields = (
"name",
"exchange", "exchange",
"api_key", "api_key",
"api_secret", "api_secret",

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-10-21 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_alter_trade_direction_alter_trade_price'),
]
operations = [
migrations.AlterField(
model_name='account',
name='exchange',
field=models.CharField(choices=[('alpaca', 'Alpaca')], max_length=255),
),
]

View File

@ -1,4 +1,4 @@
import ccxt from alpaca.trading.client import TradingClient
import stripe import stripe
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -70,7 +70,6 @@ class User(AbstractUser):
class Account(models.Model): class Account(models.Model):
EXCHANGE_CHOICES = ( EXCHANGE_CHOICES = (
("binance", "Binance"),
("alpaca", "Alpaca"), ("alpaca", "Alpaca"),
) )
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
@ -80,6 +79,10 @@ class Account(models.Model):
api_secret = models.CharField(max_length=255) api_secret = models.CharField(max_length=255)
sandbox = models.BooleanField(default=False) sandbox = models.BooleanField(default=False)
def get_account(self):
trading_client = TradingClient(self.api_key, self.api_secret, paper=self.sandbox)
return trading_client.get_account()
class Session(models.Model): class Session(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
@ -138,11 +141,7 @@ class Trade(models.Model):
if self.response is None: if self.response is None:
# the trade is not placed yet # the trade is not placed yet
if self.account.exchange == "alpaca": if self.account.exchange == "alpaca":
account = ccxt.alpaca( trading_client = TradingClient(self.account.api_key, self.account.api_secret, paper=self.sandbox)
{"apiKey": self.account.api_key, "secret": self.account.api_secret}
)
if self.account.sandbox:
account.set_sandbox_mode(True)
if self.type == "market": if self.type == "market":
order = account.create_order( order = account.create_order(
self.symbol, self.type, self.direction, self.amount self.symbol, self.type, self.direction, self.amount

View File

@ -55,7 +55,7 @@
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="{% url 'trades' type=type account_id=item.id %}"><button <a href="{% url 'account_info' type=type account_id=item.id %}"><button
class="button is-success"> class="button is-success">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
@ -67,7 +67,7 @@
{% else %} {% else %}
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trades' type=type account_id=item.id %}" hx-get="{% url 'account_info' type=type account_id=item.id %}"
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
class="button is-success"> class="button is-success">

View File

@ -0,0 +1,39 @@
<h1 class="title">Live information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in live_info.items %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h1 class="title">Stored information</h1>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>attribute</th>
<th>value</th>
</thead>
<tbody>
{% for key, item in db_info.items %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -18,6 +18,48 @@ def get_accounts(user):
accounts = Account.objects.filter(user=user) accounts = Account.objects.filter(user=user)
return accounts return accounts
class AccountInfo(LoginRequiredMixin, View):
VIEWABLE_FIELDS_MODEL = ["name", "exchange", "api_key", "sandbox"]
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/account-info.html"
def get(self, request, type, account_id):
"""
Get the account details.
:param account_id: The id of the account.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
try:
account = Account.objects.get(id=account_id, user=request.user)
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)
live_info = dict(account.get_account())
account_info = account.__dict__
account_info = {k:v for k,v in account_info.items() if k in self.VIEWABLE_FIELDS_MODEL}
if type == "page":
type = "modal"
context = {
"db_info": account_info,
"live_info": live_info,
"account_id": account_id,
"type": type,
"unique": unique,
"window_content": self.window_content,
}
return render(request, template_name, context)
class Accounts(LoginRequiredMixin, View): class Accounts(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"] allowed_types = ["modal", "widget", "window", "page"]

View File

@ -8,13 +8,13 @@ from django.views import View
from rest_framework.parsers import FormParser, JSONParser from rest_framework.parsers import FormParser, JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
from serde import ValidationError from serde import ValidationError
from alpaca.trading.client import TradingClient
from core.forms import HookForm from core.forms import HookForm
from core.lib.serde import drakdoo_s from core.lib.serde import drakdoo_s
from core.models import Callback, Hook, Account from core.models import Callback, Hook, Account
from core.util import logs from core.util import logs
import ccxt
from ccxt.base.errors import NotSupported
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
@ -22,14 +22,10 @@ def get_positions(user, account_id=None):
items = [] items = []
accounts = Account.objects.filter(user=user) accounts = Account.objects.filter(user=user)
for account in accounts: for account in accounts:
if hasattr(ccxt, account.exchange): if account.exchange == "alpaca":
instance = getattr(ccxt, account.exchange)({"apiKey": account.api_key, "secret": account.api_secret}) cast = {"api-key": account.api_key, "secret-key": account.api_secret, "paper": account.sandbox}
if account.sandbox: trading_client = TradingClient(**cast)
instance.set_sandbox_mode(True) positions = trading_client.get_all_positions()
try:
positions = instance.fetch_positions()
except NotSupported:
positions = [{"account": account.exchange, "error": "Not supported"}]
print("POSITIONS", positions) print("POSITIONS", positions)
# try: # try:
# parsed = ccxt_s.CCXTRoot.from_dict(order) # parsed = ccxt_s.CCXTRoot.from_dict(order)

View File

@ -3,6 +3,7 @@ version: "2.2"
services: services:
app: app:
image: xf/fisk:prod image: xf/fisk:prod
container_name: fisk
build: ${PORTAINER_GIT_DIR}/docker/prod build: ${PORTAINER_GIT_DIR}/docker/prod
volumes: volumes:
- ${PORTAINER_GIT_DIR}:/code - ${PORTAINER_GIT_DIR}:/code
@ -24,6 +25,7 @@ services:
migration: migration:
image: xf/fisk:prod image: xf/fisk:prod
container_name: migration_fisk
build: ./docker/prod build: ./docker/prod
command: sh -c '. /venv/bin/activate && python manage.py migrate --noinput' command: sh -c '. /venv/bin/activate && python manage.py migrate --noinput'
volumes: volumes:
@ -35,6 +37,7 @@ services:
collectstatic: collectstatic:
image: xf/fisk:prod image: xf/fisk:prod
container_name: collectstatic_fisk
build: ./docker/prod build: ./docker/prod
command: sh -c '. /venv/bin/activate && python manage.py collectstatic --noinput' command: sh -c '. /venv/bin/activate && python manage.py collectstatic --noinput'
volumes: volumes:
@ -46,6 +49,7 @@ services:
nginx: nginx:
image: nginx:latest image: nginx:latest
container_name: nginx_fisk
ports: ports:
- ${APP_PORT}:9999 - ${APP_PORT}:9999
ulimits: ulimits:
@ -72,6 +76,7 @@ services:
tmp: tmp:
image: busybox image: busybox
container_name: tmp_fisk
command: chmod -R 777 /var/run/socks command: chmod -R 777 /var/run/socks
volumes: volumes:
- /var/run/socks - /var/run/socks

View File

@ -16,4 +16,4 @@ orjson
django-otp django-otp
qrcode qrcode
serde[ext] serde[ext]
ccxt alpaca-py