Begin implementing positions

master
Mark Veidemanis 2 years ago
parent 5279217324
commit 1bdd49ee6a
Signed by: m
GPG Key ID: 5ACFCEED46C0904F

@ -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 accounts, base, callbacks, hooks, trades
from core.views import accounts, base, callbacks, hooks, trades, positions
from core.views.stripe_callbacks import Callback
urlpatterns = [
@ -115,4 +115,26 @@ urlpatterns = [
trades.TradeAction.as_view(),
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)

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

@ -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())

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

@ -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),
),
]

@ -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),
),
]

@ -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),
),
]

@ -1,14 +1,15 @@
import logging
import ccxt
import stripe
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from serde import ValidationError
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):
@ -59,7 +60,7 @@ class User(AbstractUser):
if settings.STRIPE_ENABLED:
if 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)
def has_plan(self, plan):
@ -96,7 +97,10 @@ class Hook(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 = (
("market", "Market"),
("limit", "Limit"),
@ -110,15 +114,19 @@ class Trade(models.Model):
symbol = models.CharField(choices=SYMBOL_CHOICES, max_length=255)
type = models.CharField(choices=TYPE_CHOICES, max_length=255)
amount = models.FloatField()
price = models.FloatField()
price = models.FloatField(null=True, blank=True)
stop_loss = 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)
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):
super().__init__(*args, **kwargs)
self._original = self
@ -127,7 +135,7 @@ class Trade(models.Model):
"""
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
if self.account.exchange == "alpaca":
account = ccxt.alpaca(
@ -149,7 +157,15 @@ class Trade(models.Model):
self.price,
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:
# there is a trade open
# get trade

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

@ -7,6 +7,7 @@
<th>name</th>
<th>exchange</th>
<th>API key</th>
<th>sandbox</th>
<th>actions</th>
</thead>
{% for item in items %}
@ -15,7 +16,18 @@
<td>{{ item.user }}</td>
<td>{{ item.name }}</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>
<div class="buttons">
<button

@ -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>

@ -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' %}

@ -1,17 +1,14 @@
import uuid
import orjson
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.views import View
from rest_framework.parsers import FormParser, JSONParser
from rest_framework.parsers import FormParser
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 Account, Callback
from core.models import Account
from core.util import logs
log = logs.get_logger(__name__)
@ -100,7 +97,7 @@ class AccountAction(LoginRequiredMixin, APIView):
form = AccountForm(
request.data, instance=Account.objects.get(id=account_id)
)
except account.DoesNotExist:
except Account.DoesNotExist:
message = "Account does not exist"
message_class = "danger"
context = {

@ -10,7 +10,7 @@ from rest_framework.views import APIView
from serde import ValidationError
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.util import logs
@ -36,7 +36,7 @@ class HookAPI(APIView):
# Try validating the JSON
try:
hook_resp = drakdoo.BaseDrakdoo.from_dict(request.data)
hook_resp = drakdoo_s.BaseDrakdoo.from_dict(request.data)
except ValidationError as e:
log.error(f"HookAPI POST: {e}")
return HttpResponseBadRequest(e)

@ -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)

@ -1,17 +1,14 @@
import uuid
import orjson
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.views import View
from rest_framework.parsers import FormParser, JSONParser
from rest_framework.parsers import FormParser
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 Account, Callback, Trade
from core.models import Account, Trade
from core.util import logs
log = logs.get_logger(__name__)
@ -127,7 +124,9 @@ class TradeAction(LoginRequiredMixin, APIView):
form = TradeForm(request.data)
if form.is_valid():
trade = form.save(commit=False)
print("PRESAVE TRADE", trade)
trade.save()
print("SAVED TRADE", trade)
if trade_id:
message = f"Trade {trade_id} edited successfully"
else:

Loading…
Cancel
Save