Begin implementing positions

This commit is contained in:
Mark Veidemanis 2022-10-17 07:20:30 +01:00
parent 5279217324
commit 1bdd49ee6a
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
17 changed files with 452 additions and 28 deletions

View File

@ -21,7 +21,7 @@ from django.urls import include, path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp.forms import OTPAuthenticationForm from django_otp.forms import OTPAuthenticationForm
from core.views import accounts, base, callbacks, hooks, trades from core.views import accounts, base, callbacks, hooks, trades, positions
from core.views.stripe_callbacks import Callback from core.views.stripe_callbacks import Callback
urlpatterns = [ urlpatterns = [
@ -115,4 +115,26 @@ urlpatterns = [
trades.TradeAction.as_view(), trades.TradeAction.as_view(),
name="trade_action", name="trade_action",
), ),
path("positions/<str:type>/", positions.Positions.as_view(), name="positions"),
# path("trades/<str:type>/add/", trades.TradeAction.as_view(), name="trade_action"),
path(
"positions/<str:type>/<str:account_id>/",
positions.Positions.as_view(),
name="positions",
),
# path(
# "trades/<str:type>/add/<str:name>/",
# trades.TradeAction.as_view(),
# name="trade_action",
# ),
# path(
# "trades/<str:type>/del/<str:trade_id>/",
# trades.TradeAction.as_view(),
# name="trade_action",
# ),
# path(
# "trades/<str:type>/edit/<str:trade_id>/",
# trades.TradeAction.as_view(),
# name="trade_action",
# ),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -51,6 +51,7 @@ class AccountForm(ModelForm):
"exchange", "exchange",
"api_key", "api_key",
"api_secret", "api_secret",
"sandbox",
) )
@ -65,4 +66,5 @@ class TradeForm(ModelForm):
"price", "price",
"stop_loss", "stop_loss",
"take_profit", "take_profit",
"direction",
) )

124
core/lib/serde/ccxt_s.py Normal file
View File

@ -0,0 +1,124 @@
from serde import Model, fields
# {
# "id": "92f0b26b-4c98-4553-9c74-cdafc7e037db",
# "clientOrderId": "ccxt_26adcbf445674f01af38a66a15e6f5b5",
# "timestamp": 1666096856515,
# "datetime": "2022-10-18T12:40:56.515477181Z",
# "lastTradeTimeStamp": null,
# "status": "open",
# "symbol": "BTC/USD",
# "type": "market",
# "timeInForce": "gtc",
# "postOnly": null,
# "side": "buy",
# "price": null,
# "stopPrice": null,
# "cost": null,
# "average": null,
# "amount": 1.1,
# "filled": 0.0,
# "remaining": 1.1,
# "trades": [],
# "fee": null,
# "info": {
# "id": "92f0b26b-4c98-4553-9c74-cdafc7e037db",
# "client_order_id": "ccxt_26adcbf445674f01af38a66a15e6f5b5",
# "created_at": "2022-10-18T12:40:56.516095561Z",
# "updated_at": "2022-10-18T12:40:56.516173841Z",
# "submitted_at": "2022-10-18T12:40:56.515477181Z",
# "filled_at": null,
# "expired_at": null,
# "canceled_at": null,
# "failed_at": null,
# "replaced_at": null,
# "replaced_by": null,
# "replaces": null,
# "asset_id": "276e2673-764b-4ab6-a611-caf665ca6340",
# "symbol": "BTC/USD",
# "asset_class": "crypto",
# "notional": null,
# "qty": "1.1",
# "filled_qty": "0",
# "filled_avg_price": null,
# "order_class": "",
# "order_type": "market",
# "type": "market",
# "side": "buy",
# "time_in_force": "gtc",
# "limit_price": null,
# "stop_price": null,
# "status": "pending_new",
# "extended_hours": false,
# "legs": null,
# "trail_percent": null,
# "trail_price": null,
# "hwm": null,
# "subtag": null,
# "source": null
# },
# "fees": [],
# "lastTradeTimestamp": null
# }
class CCXTInfo(Model):
id = fields.Uuid()
client_order_id = fields.Str()
created_at = fields.Str()
updated_at = fields.Str()
submitted_at = fields.Str()
filled_at = fields.Optional(fields.Str())
expired_at = fields.Optional(fields.Str())
canceled_at = fields.Optional(fields.Str())
failed_at = fields.Optional(fields.Str())
replaced_at = fields.Optional(fields.Str())
replaced_by = fields.Optional(fields.Str())
replaces = fields.Optional(fields.Str())
asset_id = fields.Uuid()
symbol = fields.Str()
asset_class = fields.Str()
notional = fields.Optional(fields.Str())
qty = fields.Str()
filled_qty = fields.Str()
filled_avg_price = fields.Optional(fields.Str())
order_class = fields.Str()
order_type = fields.Str()
type = fields.Str()
side = fields.Str()
time_in_force = fields.Str()
limit_price = fields.Optional(fields.Str())
stop_price = fields.Optional(fields.Str())
status = fields.Str()
extended_hours = fields.Bool()
legs = fields.Optional(fields.List(fields.Nested("CCXTInfo")))
trail_percent = fields.Optional(fields.Str())
trail_price = fields.Optional(fields.Str())
hwm = fields.Optional(fields.Str())
subtag = fields.Optional(fields.Str())
source = fields.Optional(fields.Str())
class CCXTRoot(Model):
id = fields.Uuid()
clientOrderId = fields.Str()
timestamp = fields.Int()
datetime = fields.Str()
lastTradeTimeStamp = fields.Optional(fields.Str())
status = fields.Str()
symbol = fields.Str()
type = fields.Str()
timeInForce = fields.Str()
postOnly = fields.Optional(fields.Str())
side = fields.Str()
price = fields.Optional(fields.Float())
stopPrice = fields.Optional(fields.Float())
cost = fields.Optional(fields.Float())
average = fields.Optional(fields.Float())
amount = fields.Float()
filled = fields.Float()
remaining = fields.Float()
trades = fields.Optional(fields.List(fields.Dict()))
fee = fields.Optional(fields.Float())
info = fields.Nested(CCXTInfo)
fees = fields.Optional(fields.List(fields.Dict()))
lastTradeTimestamp = fields.Optional(fields.Str())

4
core/lib/trades.py Normal file
View File

@ -0,0 +1,4 @@
# Trade handling
def sync_trades_with_db(user):
pass

View File

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

View File

@ -0,0 +1,33 @@
# Generated by Django 4.1.2 on 2022-10-18 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0011_alter_account_exchange'),
]
operations = [
migrations.RenameField(
model_name='trade',
old_name='exchange_id',
new_name='client_order_id',
),
migrations.AddField(
model_name='trade',
name='order_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='trade',
name='response',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='trade',
name='symbol',
field=models.CharField(choices=[('BTC/USD', 'Bitcoin/US Dollar'), ('LTC/USD', 'Litecoin/US Dollar')], max_length=255),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.2 on 2022-10-18 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_rename_exchange_id_trade_client_order_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='trade',
name='direction',
field=models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], default='buy', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='trade',
name='price',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -1,14 +1,15 @@
import logging
import ccxt import ccxt
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
from django.db import models from django.db import models
from serde import ValidationError
from core.lib.customers import get_or_create, update_customer_fields from core.lib.customers import get_or_create, update_customer_fields
from core.lib.serde import ccxt_s
from core.util import logs
logger = logging.getLogger(__name__) log = logs.get_logger(__name__)
class Plan(models.Model): class Plan(models.Model):
@ -59,7 +60,7 @@ class User(AbstractUser):
if settings.STRIPE_ENABLED: if settings.STRIPE_ENABLED:
if self.stripe_id: if self.stripe_id:
stripe.Customer.delete(self.stripe_id) stripe.Customer.delete(self.stripe_id)
logger.info(f"Deleted Stripe customer {self.stripe_id}") log.info(f"Deleted Stripe customer {self.stripe_id}")
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
def has_plan(self, plan): def has_plan(self, plan):
@ -96,7 +97,10 @@ class Hook(models.Model):
class Trade(models.Model): class Trade(models.Model):
SYMBOL_CHOICES = (("BTCUSD", "Bitcoin/USD"),) SYMBOL_CHOICES = (
("BTC/USD", "Bitcoin/US Dollar"),
("LTC/USD", "Litecoin/US Dollar"),
)
TYPE_CHOICES = ( TYPE_CHOICES = (
("market", "Market"), ("market", "Market"),
("limit", "Limit"), ("limit", "Limit"),
@ -110,15 +114,19 @@ class Trade(models.Model):
symbol = models.CharField(choices=SYMBOL_CHOICES, max_length=255) symbol = models.CharField(choices=SYMBOL_CHOICES, max_length=255)
type = models.CharField(choices=TYPE_CHOICES, max_length=255) type = models.CharField(choices=TYPE_CHOICES, max_length=255)
amount = models.FloatField() amount = models.FloatField()
price = models.FloatField() price = models.FloatField(null=True, blank=True)
stop_loss = models.FloatField(null=True, blank=True) stop_loss = models.FloatField(null=True, blank=True)
take_profit = models.FloatField(null=True, blank=True) take_profit = models.FloatField(null=True, blank=True)
exchange_id = models.CharField(max_length=255, null=True, blank=True)
status = models.CharField(max_length=255, null=True, blank=True) status = models.CharField(max_length=255, null=True, blank=True)
direction = models.CharField( direction = models.CharField(
choices=DIRECTION_CHOICES, max_length=255, null=True, blank=True choices=DIRECTION_CHOICES, max_length=255
) )
# To populate from the trade
order_id = models.CharField(max_length=255, null=True, blank=True)
client_order_id = models.CharField(max_length=255, null=True, blank=True)
response = models.JSONField(null=True, blank=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._original = self self._original = self
@ -127,7 +135,7 @@ class Trade(models.Model):
""" """
Override the save function to place the trade. Override the save function to place the trade.
""" """
if self.exchange_id 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( account = ccxt.alpaca(
@ -149,7 +157,15 @@ class Trade(models.Model):
self.price, self.price,
params, params,
) )
self.status = "filled"
print("ORDER", order)
try:
parsed = ccxt_s.CCXTRoot.from_dict(order)
except ValidationError as e:
log.error(f"Error creating trade: {e}")
return False
self.status = parsed.status
self.response = order
else: else:
# there is a trade open # there is a trade open
# get trade # get trade

View File

@ -201,6 +201,11 @@
<a class="navbar-item" href="{% url 'home' %}"> <a class="navbar-item" href="{% url 'home' %}">
Home Home
</a> </a>
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'positions' type='page' %}">
Positions
</a>
{% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'accounts' type='page' %}"> <a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts Accounts
@ -208,7 +213,7 @@
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'trades' type='page' %}"> <a class="navbar-item" href="{% url 'trades' type='page' %}">
Trades Bot Trades
</a> </a>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}

View File

@ -7,6 +7,7 @@
<th>name</th> <th>name</th>
<th>exchange</th> <th>exchange</th>
<th>API key</th> <th>API key</th>
<th>sandbox</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in items %} {% for item in items %}
@ -15,7 +16,18 @@
<td>{{ item.user }}</td> <td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.exchange }}</td> <td>{{ item.exchange }}</td>
<td>{{ item.api_jey }}</td> <td>{{ item.api_key }}</td>
<td>
{% if item.sandbox %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td> <td>
<div class="buttons"> <div class="buttons">
<button <button

View File

@ -0,0 +1,86 @@
{% include 'partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="accounts-table">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>exchange</th>
<th>API key</th>
<th>sandbox</th>
<th>actions</th>
</thead>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.api_key }}</td>
<td>
{% if item.sandbox %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'position_action' type=type order_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'position_action' type=type order_id=item.id %}"
hx-trigger="click"
hx-target="#positions-table"
class="button is-danger">
<span class="icon-text">
<span class="icon" data-tooltip="Close">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'position_action' type=type account_id=item.id %}"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trades' type=type account_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,20 @@
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#"
hx-trigger="click"
hx-target="#modals-here"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-plus"></i>
</span>
<span>Trade</span>
</span>
</button>
</div>
{% include 'partials/notify.html' %}
{% include 'partials/position-list.html' %}

View File

@ -1,17 +1,14 @@
import uuid import uuid
import orjson
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from rest_framework.parsers import FormParser, JSONParser from rest_framework.parsers import FormParser
from rest_framework.views import APIView from rest_framework.views import APIView
from serde import ValidationError
from core.forms import AccountForm from core.forms import AccountForm
from core.lib.serde import drakdoo from core.models import Account
from core.models import Account, Callback
from core.util import logs from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
@ -100,7 +97,7 @@ class AccountAction(LoginRequiredMixin, APIView):
form = AccountForm( form = AccountForm(
request.data, instance=Account.objects.get(id=account_id) request.data, instance=Account.objects.get(id=account_id)
) )
except account.DoesNotExist: except Account.DoesNotExist:
message = "Account does not exist" message = "Account does not exist"
message_class = "danger" message_class = "danger"
context = { context = {

View File

@ -10,7 +10,7 @@ from rest_framework.views import APIView
from serde import ValidationError from serde import ValidationError
from core.forms import HookForm from core.forms import HookForm
from core.lib.serde import drakdoo from core.lib.serde import drakdoo_s
from core.models import Callback, Hook from core.models import Callback, Hook
from core.util import logs from core.util import logs
@ -36,7 +36,7 @@ class HookAPI(APIView):
# Try validating the JSON # Try validating the JSON
try: try:
hook_resp = drakdoo.BaseDrakdoo.from_dict(request.data) hook_resp = drakdoo_s.BaseDrakdoo.from_dict(request.data)
except ValidationError as e: except ValidationError as e:
log.error(f"HookAPI POST: {e}") log.error(f"HookAPI POST: {e}")
return HttpResponseBadRequest(e) return HttpResponseBadRequest(e)

62
core/views/positions.py Normal file
View File

@ -0,0 +1,62 @@
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 HookForm
from core.lib.serde import drakdoo_s
from core.models import Callback, Hook, Account
from core.util import logs
import ccxt
from ccxt.base.errors import NotSupported
log = logs.get_logger(__name__)
def get_positions(user, account_id=None):
items = []
accounts = Account.objects.filter(user=user)
for account in accounts:
if hasattr(ccxt, account.exchange):
instance = getattr(ccxt, account.exchange)({"apiKey": account.api_key, "secret": account.api_secret})
if account.sandbox:
instance.set_sandbox_mode(True)
try:
positions = instance.fetch_positions()
except NotSupported:
positions = [{"account": account.exchange, "error": "Not supported"}]
print("POSITIONS", positions)
# try:
# parsed = ccxt_s.CCXTRoot.from_dict(order)
# except ValidationError as e:
# log.error(f"Error creating trade: {e}")
# return False
# self.status = parsed.status
# self.response = order
class Positions(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/positions.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]
items = get_positions(request.user, account_id)
if type == "page":
type = "modal"
context = {
"title": f"Hooks ({type})",
"unique": unique,
"window_content": self.window_content,
"items": items,
"type": type,
}
return render(request, template_name, context)

View File

@ -1,17 +1,14 @@
import uuid import uuid
import orjson
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from rest_framework.parsers import FormParser, JSONParser from rest_framework.parsers import FormParser
from rest_framework.views import APIView from rest_framework.views import APIView
from serde import ValidationError
from core.forms import TradeForm from core.forms import TradeForm
from core.lib.serde import drakdoo from core.models import Account, Trade
from core.models import Account, Callback, Trade
from core.util import logs from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
@ -127,7 +124,9 @@ class TradeAction(LoginRequiredMixin, APIView):
form = TradeForm(request.data) form = TradeForm(request.data)
if form.is_valid(): if form.is_valid():
trade = form.save(commit=False) trade = form.save(commit=False)
print("PRESAVE TRADE", trade)
trade.save() trade.save()
print("SAVED TRADE", trade)
if trade_id: if trade_id:
message = f"Trade {trade_id} edited successfully" message = f"Trade {trade_id} edited successfully"
else: else: