Implement strategies and posting trades

This commit is contained in:
Mark Veidemanis 2022-10-27 18:08:40 +01:00
parent 7e4f3f52d1
commit 061c6f6ca7
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
32 changed files with 1060 additions and 178 deletions

View File

@ -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, positions, trades
from core.views import accounts, base, callbacks, hooks, positions, strategies, trades
from core.views.stripe_callbacks import Callback
urlpatterns = [
@ -127,6 +127,11 @@ urlpatterns = [
positions.Positions.as_view(),
name="positions",
),
path(
"positions/<str:type>/<str:account_id>/<str:asset_id>/",
positions.PositionAction.as_view(),
name="position_action",
),
# path(
# "trades/<str:type>/add/<str:name>/",
# trades.TradeAction.as_view(),
@ -142,4 +147,25 @@ urlpatterns = [
# trades.TradeAction.as_view(),
# name="trade_action",
# ),
path("strategies/<str:type>/", strategies.Strategies.as_view(), name="strategies"),
path(
"strategies/<str:type>/add/",
strategies.StrategiesAction.as_view(),
name="strategies_action",
),
path(
"strategies/<str:type>/add/<str:name>/",
strategies.StrategiesAction.as_view(),
name="strategies_action",
),
path(
"strategies/<str:type>/del/<str:strategy_id>/",
strategies.StrategiesAction.as_view(),
name="strategies_action",
),
path(
"strategies/<str:type>/edit/<str:strategy_id>/",
strategies.StrategiesAction.as_view(),
name="strategies_action",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.forms import ModelForm
from .models import Account, Hook, Trade, User
from .models import Account, Hook, Strategy, Trade, User
# Create your forms here.
@ -41,6 +41,7 @@ class HookForm(ModelForm):
fields = (
"name",
"hook",
"direction",
)
@ -56,6 +57,26 @@ class AccountForm(ModelForm):
)
class StrategyForm(ModelForm):
class Meta:
model = Strategy
fields = (
"name",
"description",
"account",
"hooks",
"enabled",
"take_profit_percent",
"stop_loss_percent",
"price_slippage_percent",
"trade_size_percent",
)
hooks = forms.ModelMultipleChoiceField(
queryset=Hook.objects.all(), widget=forms.CheckboxSelectMultiple
)
class TradeForm(ModelForm):
class Meta:
model = Trade

115
core/lib/market.py Normal file
View File

@ -0,0 +1,115 @@
from alpaca.common.exceptions import APIError
from core.models import Strategy, Trade
from core.util import logs
log = logs.get_logger(__name__)
def get_balance(account):
account_info = account.client.get_account()
cash = account_info["equity"]
try:
return float(cash)
except ValueError:
return False
def get_market_value(account, symbol):
try:
position = account.client.get_position(symbol)
return float(position["market_value"])
except APIError:
return False
def execute_strategy(callback, strategy):
cash_balance = get_balance(strategy.account)
log.debug(f"Cash balance: {cash_balance}")
if not cash_balance:
return None
user = strategy.user
account = strategy.account
hook = callback.hook
base = callback.base
quote = callback.quote
direction = hook.direction
if quote not in ["usd", "usdt", "usdc", "busd"]:
log.error(f"Quote not compatible with Dollar: {quote}")
return False
quote = "usd" # TODO: MASSIVE HACK
symbol = f"{base.upper()}/{quote.upper()}"
if symbol not in account.supported_assets:
log.error(f"Symbol not supported by account: {symbol}")
return False
print(f"Identified pair from callback {symbol}")
# market_from_alpaca = get_market_value(account, symbol)
# change_percent = abs(((float(market_from_alpaca)-price)/price)*100)
# if change_percent > strategy.price_slippage_percent:
# log.error(f"Price slippage too high: {change_percent}")
# return False
# type = "limit"
type = "market"
trade_size_as_ratio = strategy.trade_size_percent / 100
log.debug(f"Trade size as ratio: {trade_size_as_ratio}")
amount_usd = trade_size_as_ratio * cash_balance
log.debug(f"Trade size: {amount_usd}")
price = callback.price
if not price:
return
log.debug(f"Extracted price of quote: {price}")
# We can do this because the quote IS in $ or equivalent
trade_size_in_quote = amount_usd / price
log.debug(f"Trade size in quote: {trade_size_in_quote}")
# calculate sl/tp
stop_loss_as_ratio = strategy.stop_loss_percent / 100
take_profit_as_ratio = strategy.take_profit_percent / 100
log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}")
log.debug(f"Take profit as ratio: {take_profit_as_ratio}")
stop_loss_subtract = price * stop_loss_as_ratio
take_profit_add = price * take_profit_as_ratio
log.debug(f"Stop loss subtract: {stop_loss_subtract}")
log.debug(f"Take profit add: {take_profit_add}")
stop_loss = price - stop_loss_subtract
take_profit = price + take_profit_add
log.debug(f"Stop loss: {stop_loss}")
log.debug(f"Take profit: {take_profit}")
new_trade = Trade.objects.create(
user=user,
account=account,
hook=hook,
symbol=symbol,
type=type,
# amount_usd=amount_usd,
amount=trade_size_in_quote,
# price=price,
stop_loss=stop_loss,
take_profit=take_profit,
direction=direction,
)
new_trade.save()
posted, info = new_trade.post()
log.debug(f"Posted trade: {posted} - {info}")
def process_callback(callback):
log.info(f"Received callback for {callback.hook}")
strategies = Strategy.objects.filter(hooks=callback.hook, enabled=True)
log.debug(f"Matched strategies: {strategies}")
for strategy in strategies:
log.debug(f"Executing strategy {strategy}")
if callback.hook.user != strategy.user:
log.error("Ownership differs between callback and strategy.")
return
execute_strategy(callback, strategy)

View File

@ -1,5 +1,83 @@
# Trade handling
from alpaca.common.exceptions import APIError
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import LimitOrderRequest, MarketOrderRequest
from core.util import logs
log = logs.get_logger(__name__)
def sync_trades_with_db(user):
pass
def post_trade(trade):
# the trade is not placed yet
trading_client = trade.account.get_client()
if trade.direction == "buy":
direction = OrderSide.BUY
elif trade.direction == "sell":
direction = OrderSide.SELL
else:
raise Exception("Unknown direction")
cast = {"symbol": trade.symbol, "side": direction, "time_in_force": TimeInForce.IOC}
if trade.amount is not None:
cast["qty"] = trade.amount
if trade.amount_usd is not None:
cast["notional"] = trade.amount_usd
if not trade.amount and not trade.amount_usd:
return (False, "No amount specified")
if trade.take_profit:
cast["take_profit"] = {"limit_price": trade.take_profit}
if trade.stop_loss:
stop_limit_price = trade.stop_loss - (trade.stop_loss * 0.005)
cast["stop_loss"] = {
"stop_price": trade.stop_loss,
"limit_price": stop_limit_price,
}
if trade.type == "market":
market_order_data = MarketOrderRequest(**cast)
try:
order = trading_client.submit_order(order_data=market_order_data)
except APIError as e:
log.error(f"Error placing market order: {e}")
return (False, e)
elif trade.type == "limit":
if not trade.price:
return (False, "Limit order with no price")
cast["limit_price"] = trade.price
limit_order_data = LimitOrderRequest(**cast)
try:
order = trading_client.submit_order(order_data=limit_order_data)
except APIError as e:
log.error(f"Error placing limit order: {e}")
return (False, e)
print("ORDER", order)
else:
raise Exception("Unknown trade type")
trade.response = order
trade.status = "posted"
trade.order_id = order["id"]
trade.client_order_id = order["client_order_id"]
trade.save()
return (True, order)
def update_trade(self):
pass
def close_trade(trade):
pass
def get_position_info(account, asset_id):
trading_client = account.get_client()
try:
position = trading_client.get_open_position(asset_id)
except APIError as e:
return (False, e)
return (True, position)

View File

@ -0,0 +1,27 @@
# Generated by Django 4.1.2 on 2022-10-25 21:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_alter_account_exchange'),
]
operations = [
migrations.CreateModel(
name='Strategy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('enabled', models.BooleanField(default=False)),
('take_profit_percent', models.FloatField(default=300.0)),
('stop_loss_percent', models.FloatField(default=100.0)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
('hooks', models.ManyToManyField(to='core.hook')),
],
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.1.2 on 2022-10-25 21:26
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0015_strategy'),
]
operations = [
migrations.AddField(
model_name='strategy',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AddField(
model_name='trade',
name='user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 4.1.2 on 2022-10-26 09:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0016_strategy_user_trade_user'),
]
operations = [
migrations.AddField(
model_name='hook',
name='direction',
field=models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], default='buy', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='strategy',
name='price_slippage_percent',
field=models.FloatField(default=2.5),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.1.2 on 2022-10-26 09:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0017_hook_direction_strategy_price_slippage_percent'),
]
operations = [
migrations.AddField(
model_name='strategy',
name='trade_size_percent',
field=models.FloatField(default=2.5),
),
migrations.AddField(
model_name='trade',
name='amount_usd',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='trade',
name='amount',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.1.2 on 2022-10-27 16:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0018_strategy_trade_size_percent_trade_amount_usd_and_more'),
]
operations = [
migrations.AddField(
model_name='account',
name='supported_symbols',
field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='strategy',
name='stop_loss_percent',
field=models.FloatField(default=1.0),
),
migrations.AlterField(
model_name='strategy',
name='take_profit_percent',
field=models.FloatField(default=3.0),
),
migrations.AlterField(
model_name='trade',
name='symbol',
field=models.CharField(max_length=255),
),
]

View File

@ -0,0 +1,54 @@
# Generated by Django 4.1.2 on 2022-10-27 16:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0019_account_supported_symbols_and_more'),
]
operations = [
migrations.RenameField(
model_name='callback',
old_name='market_item',
new_name='base',
),
migrations.RenameField(
model_name='callback',
old_name='market_contract',
new_name='contract',
),
migrations.RenameField(
model_name='callback',
old_name='market_exchange',
new_name='exchange',
),
migrations.RenameField(
model_name='callback',
old_name='market_currency',
new_name='quote',
),
migrations.RenameField(
model_name='callback',
old_name='timestamp_sent',
new_name='sent',
),
migrations.RenameField(
model_name='callback',
old_name='timestamp_trade',
new_name='trade',
),
migrations.AddField(
model_name='callback',
name='price',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='callback',
name='symbol',
field=models.CharField(default='NUL/NUL', max_length=255),
preserve_default=False,
),
]

View File

@ -1,14 +1,13 @@
import stripe
from alpaca.common.exceptions import APIError
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import LimitOrderRequest, MarketOrderRequest
from alpaca.trading.requests import GetAssetsRequest
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from serde import ValidationError
from core.lib import trades
from core.lib.customers import get_or_create, update_customer_fields
from core.lib.serde import ccxt_s
from core.util import logs
log = logs.get_logger(__name__)
@ -78,12 +77,46 @@ class Account(models.Model):
api_key = models.CharField(max_length=255)
api_secret = models.CharField(max_length=255)
sandbox = models.BooleanField(default=False)
supported_symbols = models.JSONField(default=list)
def get_account(self):
def __str__(self):
name = f"{self.name} ({self.exchange})"
if self.sandbox:
name += " (sandbox)"
return name
def save(self, *args, **kwargs):
"""
Override the save function to update supported symbols.
"""
try:
request = GetAssetsRequest(status="active", asset_class="crypto")
assets = self.client.get_all_assets(filter=request)
except APIError as e:
log.error(f"Could not get asset list: {e}")
return False
asset_list = [x["symbol"] for x in assets if "symbol" in x]
self.supported_symbols = asset_list
print("Supported symbols", self.supported_symbols)
super().save(*args, **kwargs)
def get_client(self):
trading_client = TradingClient(
self.api_key, self.api_secret, paper=self.sandbox
self.api_key, self.api_secret, paper=self.sandbox, raw_data=True
)
return trading_client.get_account()
return trading_client
@property
def client(self):
"""
Convenience property for one-off API calls.
"""
return self.get_client()
@classmethod
def get_by_id(cls, account_id, user):
return cls.objects.get(id=account_id, user=user)
class Session(models.Model):
@ -95,17 +128,21 @@ class Session(models.Model):
class Hook(models.Model):
DIRECTION_CHOICES = (
("buy", "Buy"),
("sell", "Sell"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=1024, null=True, blank=True, unique=True)
hook = models.CharField(max_length=255, unique=True)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
received = models.IntegerField(default=0)
def __str__(self):
return f"{self.name} ({self.hook})"
class Trade(models.Model):
SYMBOL_CHOICES = (
("BTC/USD", "Bitcoin/US Dollar"),
("LTC/USD", "Litecoin/US Dollar"),
)
TYPE_CHOICES = (
("market", "Market"),
("limit", "Limit"),
@ -114,11 +151,13 @@ class Trade(models.Model):
("buy", "Buy"),
("sell", "Sell"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
symbol = models.CharField(choices=SYMBOL_CHOICES, max_length=255)
symbol = models.CharField(max_length=255)
type = models.CharField(choices=TYPE_CHOICES, max_length=255)
amount = models.FloatField()
amount = models.FloatField(null=True, blank=True)
amount_usd = models.FloatField(null=True, blank=True)
price = models.FloatField(null=True, blank=True)
stop_loss = models.FloatField(null=True, blank=True)
take_profit = models.FloatField(null=True, blank=True)
@ -134,56 +173,8 @@ class Trade(models.Model):
super().__init__(*args, **kwargs)
self._original = self
def save(self, *args, **kwargs):
"""
Override the save function to place the trade.
"""
if self.response is None:
# the trade is not placed yet
if self.account.exchange == "alpaca":
trading_client = TradingClient(
self.account.api_key,
self.account.api_secret,
paper=self.account.sandbox,
)
if self.direction == "buy":
direction = OrderSide.BUY
elif self.direction == "sell":
direction = OrderSide.SELL
else:
raise Exception("Unknown direction")
if self.type == "market":
market_order_data = MarketOrderRequest(
symbol=self.symbol,
qty=self.amount,
side=OrderSide.BUY,
time_in_force=TimeInForce.IOC,
)
order = trading_client.submit_order(order_data=market_order_data)
elif self.type == "limit":
limit_order_data = LimitOrderRequest(
symbol=self.symbol,
limit_price=self.price,
qty=self.amount,
side=direction,
time_in_force=TimeInForce.IOC,
)
order = trading_client.submit_order(order_data=limit_order_data)
else:
raise Exception("Unknown order type")
print("ORDER", order)
# self.status = parsed.status
# self.response = order
else:
# there is a trade open
# get trade
# update trade
pass
super().save(*args, **kwargs)
def post(self):
return trades.post_trade(self)
def delete(self, *args, **kwargs):
# close the trade
@ -195,12 +186,30 @@ class Callback(models.Model):
title = models.CharField(max_length=1024, null=True, blank=True)
message = models.CharField(max_length=1024, null=True, blank=True)
period = models.CharField(max_length=255, null=True, blank=True)
timestamp_sent = models.BigIntegerField(null=True, blank=True)
timestamp_trade = models.BigIntegerField(null=True, blank=True)
market_exchange = models.CharField(max_length=255, null=True, blank=True)
market_item = models.CharField(max_length=255, null=True, blank=True)
market_currency = models.CharField(max_length=255, null=True, blank=True)
market_contract = models.CharField(max_length=255, null=True, blank=True)
sent = models.BigIntegerField(null=True, blank=True)
trade = models.BigIntegerField(null=True, blank=True)
exchange = models.CharField(max_length=255, null=True, blank=True)
base = models.CharField(max_length=255, null=True, blank=True)
quote = models.CharField(max_length=255, null=True, blank=True)
contract = models.CharField(max_length=255, null=True, blank=True)
price = models.FloatField(null=True, blank=True)
symbol = models.CharField(max_length=255)
class Strategy(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
hooks = models.ManyToManyField(Hook)
enabled = models.BooleanField(default=False)
take_profit_percent = models.FloatField(default=3.0)
stop_loss_percent = models.FloatField(default=1.0)
price_slippage_percent = models.FloatField(default=2.5)
trade_size_percent = models.FloatField(default=2.5)
def __str__(self):
return self.name
# class Perms(models.Model):

View File

@ -202,29 +202,38 @@
Home
</a>
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'positions' type='page' %}">
Positions
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Manage
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'positions' type='page' %}">
Positions
</a>
<a class="navbar-item" href="{% url 'trades' type='page' %}">
Bot Trades
</a>
</div>
</div>
{% endif %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts
</a>
{% endif %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'trades' type='page' %}">
Bot Trades
</a>
{% endif %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
Hooks
</a>
{% endif %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'callbacks' type='page' %}">
Callbacks
</a>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Setup
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
Hooks
</a>
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts
</a>
<a class="navbar-item" href="{% url 'strategies' type='page' %}">
Strategies
</a>
</div>
</div>
{% endif %}
{% if settings.STRIPE_ENABLED %}
{% if user.is_authenticated %}
@ -233,22 +242,6 @@
</a>
{% endif %}
{% endif %}
{% if user.is_superuser %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Admin
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="#">
Admin1
</a>
<a class="navbar-item" href="#">
Admin2
</a>
</div>
</div>
{% endif %}
<a class="navbar-item add-button">
Install
</a>

View File

@ -6,6 +6,7 @@
<th>user</th>
<th>name</th>
<th>hook</th>
<th>direction</th>
<th>received hooks</th>
<th>actions</th>
</thead>
@ -15,6 +16,7 @@
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td><code>{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/</code></td>
<td>{{ item.direction }}</td>
<td>{{ item.received }}</td>
<td>
<div class="buttons">
@ -43,7 +45,7 @@
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'callbacks' type=type hook_id=item.id %}"><button
<a href="{% url 'callbacks' type='page' hook_id=item.id %}"><button
class="button is-success">
<span class="icon-text">
<span class="icon">

View File

@ -27,7 +27,7 @@
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#trade-edit"
hx-get="#"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-info">
@ -50,7 +50,7 @@
</span>
</button>
{% if type == 'page' %}
<a href="#trade-info">
<a href="{% url 'position_action' type=type account_id=item.account_id asset_id=item.asset_id %}">
<button
class="button is-success">
<span class="icon-text">
@ -63,7 +63,7 @@
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#trade-info"
hx-get="{% url 'position_action' type=type account_id=item.account_id asset_id=item.asset_id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-success">

View File

@ -0,0 +1,88 @@
{% include 'partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="strategies-table">
<thead>
<th>id</th>
<th>name</th>
<th>description</th>
<th>account</th>
<th>enabled</th>
<th>TP</th>
<th>SL</th>
<th>actions</th>
</thead>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.account }}</td>
<td>
{% if item.enabled %}
<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>{{ item.take_profit_percent }}</td>
<td>{{ item.stop_loss_percent }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'strategies_action' type=type strategy_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 'strategies_action' type=type strategy_id=item.id %}"
hx-trigger="click"
hx-target="#strategies-table"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'strategies_action' type=type strategy_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 'strategies_action' type=type strategy_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

@ -3,6 +3,7 @@
<table class="table is-fullwidth is-hoverable" id="trades-table">
<thead>
<th>id</th>
<th>status</th>
<th>account id</th>
<th>symbol</th>
<th>type</th>
@ -15,6 +16,7 @@
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.status }}</td>
<td>{{ item.account.id }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.type }}</td>

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% block content %}
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="box">
<p class="has-text-danger">Registration closed.</p>
</div>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -1,3 +1,6 @@
{% include 'partials/notify.html' %}
<h1 class="title is-4">List of accounts</h1>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
@ -14,6 +17,5 @@
</button>
</div>
{% include 'partials/notify.html' %}
{% include 'partials/account-list.html' %}

View File

@ -0,0 +1,31 @@
{% include 'partials/notify.html' %}
{% load crispy_forms_tags %}
{% load crispy_forms_bulma_field %}
<form
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
{% if strategy_id is not None %}
hx-put="{% url 'strategies_action' type=type strategy_id=strategy_id %}"
{% else %}
hx-put="{% url 'strategies_action' type=type %}"
{% endif %}
hx-target="#strategies-table"
hx-swap="outerHTML">
{% csrf_token %}
{{ form|crispy }}
<button
type="button"
class="button is-light modal-close-button">
Cancel
</button>
<button type="submit" class="button is-info modal-close-button">Submit</button>
</form>

View File

@ -1,3 +1,6 @@
<h1 class="title is-5">Add or edit a trade</h1>
<h1 class="subtitle">Updates will be posted to exchange!</h1>
{% include 'partials/notify.html' %}
{% load crispy_forms_tags %}

View File

@ -1,4 +1,5 @@
{% include 'partials/notify.html' %}
<h1 class="title is-4">List of received callbacks</h1>
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
<thead>
@ -11,8 +12,8 @@
<th>sent</th>
<th>trade</th>
<th>exchange</th>
<th>item</th>
<th>currency</th>
<th>symbol</th>
<th>price</th>
<th>contract</th>
<th>actions</th>
</thead>
@ -31,12 +32,12 @@
<td>{{ item.title }}</td>
<td>{{ item.message }}</td>
<td>{{ item.period }}</td>
<td>{{ item.timestamp_sent }}</td>
<td>{{ item.timestamp_trade }}</td>
<td>{{ item.market_exchange }}</td>
<td>{{ item.market_item }}</td>
<td>{{ item.market_currency }}</td>
<td>{{ item.market_contract }}</td>
<td>{{ item.sent }}</td>
<td>{{ item.trade }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.price }}</td>
<td>{{ item.contract }}</td>
<td>
<div class="buttons">

View File

@ -1,3 +1,8 @@
{% include 'partials/notify.html' %}
<h1 class="title is-4">List of active URL endpoints for receiving hooks.</h1>
<h1 class="subtitle">Add URLs here to receive Drakdoo callbacks. Make then unique!</h1>
<h1 class="subtitle">Warning: hooks can only trade in one direction.</h1>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
@ -14,6 +19,5 @@
</button>
</div>
{% include 'partials/notify.html' %}
{% include 'partials/hook-list.html' %}

View File

@ -1,19 +1,6 @@
<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>
<h1 class="title is-4">Live positions from all exchanges</h1>
<h1 class="subtitle">Manual trades are editable under "Bot Trades" tab.</h1>
{% include 'partials/notify.html' %}
{% include 'partials/position-list.html' %}
{% include 'partials/position-list.html' %}

View File

@ -0,0 +1,21 @@
{% include 'partials/notify.html' %}
<h1 class="title is-4">List of strategies</h1>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'strategies_action' type=type %}"
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>Strategy</span>
</span>
</button>
</div>
{% include 'partials/strategy-list.html' %}

View File

@ -15,5 +15,9 @@
</div>
{% include 'partials/notify.html' %}
<h1 class="title is-4">List of bot and manual trades. This may not reflect actual live trades.</h1>
<h1 class="subtitle">Trades deleted here will not be closed on the exchange.</h1>
{% include 'partials/trade-list.html' %}

View File

@ -0,0 +1,20 @@
{% include 'partials/notify.html' %}
<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 items.items %}
<tr>
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -34,7 +34,7 @@ class AccountInfo(LoginRequiredMixin, View):
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
try:
account = Account.objects.get(id=account_id, user=request.user)
account = Account.get_by_id(account_id, request.user)
except Account.DoesNotExist:
message = "Account does not exist"
message_class = "danger"
@ -45,7 +45,7 @@ class AccountInfo(LoginRequiredMixin, View):
}
return render(request, template_name, context)
live_info = dict(account.get_account())
live_info = dict(account.client.get_account())
account_info = account.__dict__
account_info = {
k: v for k, v in account_info.items() if k in self.VIEWABLE_FIELDS_MODEL
@ -141,7 +141,8 @@ class AccountAction(LoginRequiredMixin, APIView):
if account_id:
try:
form = AccountForm(
request.data, instance=Account.objects.get(id=account_id)
request.data,
instance=Account.objects.get(id=account_id, user=request.user),
)
except Account.DoesNotExist:
message = "Account does not exist"

View File

@ -91,6 +91,11 @@ class Signup(CreateView):
success_url = reverse_lazy("login")
template_name = "registration/signup.html"
def get(self, request, *args, **kwargs):
if not settings.REGISTRATION_OPEN:
return render(request, "registration/registration_closed.html")
super().get(request, *args, **kwargs)
class Portal(LoginRequiredMixin, View):
async def get(self, request):

View File

@ -1,6 +1,8 @@
import re
import uuid
import orjson
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render
@ -10,6 +12,7 @@ from rest_framework.views import APIView
from serde import ValidationError
from core.forms import HookForm
from core.lib import market
from core.lib.serde import drakdoo_s
from core.models import Callback, Hook
from core.util import logs
@ -22,6 +25,18 @@ def get_hooks(user):
return hooks
def extract_price(message):
result = re.findall("\d+\.\d+", message) # noqa
if len(result) != 1:
log.error(f"Could not extract price from message: {message}")
return False
try:
log.debug(f"Extracted {result[0]} from '{message}'")
return float(result[0])
except ValueError:
return False
class HookAPI(APIView):
parser_classes = [JSONParser]
@ -41,17 +56,31 @@ class HookAPI(APIView):
log.error(f"HookAPI POST: {e}")
return HttpResponseBadRequest(e)
price = extract_price(hook_resp.message)
if not price:
log.debug(f"Could not extract price from message: {hook_resp.message}")
return HttpResponseBadRequest("Could not extract price from message")
base = hook_resp.market.item
quote = hook_resp.market.currency
symbol = f"{base.upper()}/{quote.upper()}"
if symbol not in settings.ASSET_FILTER:
log.debug(f"Skipping {symbol} because it is not in the asset filter")
return HttpResponseBadRequest("Invalid symbol")
data = {
"title": hook_resp.title,
"message": hook_resp.message,
"period": hook_resp.period,
"timestamp_sent": hook_resp.timestamp.sent,
"sent": hook_resp.timestamp.sent,
"timestamp_trade": hook_resp.timestamp.trade,
"market_exchange": hook_resp.market.exchange,
"market_item": hook_resp.market.item,
"market_currency": hook_resp.market.currency,
"market_contract": hook_resp.market.contract,
"trade": hook_resp.market.exchange,
"base": hook_resp.market.item,
"quote": hook_resp.market.currency,
"symbol": symbol,
"contract": hook_resp.market.contract,
"price": price,
}
log.debug("HookAPI callback: data: %s", data)
# Try getting the hook
@ -63,6 +92,7 @@ class HookAPI(APIView):
# Create the callback object
callback = Callback.objects.create(hook=hook, **data)
callback.save()
market.process_callback(callback)
# Bump received count
hook.received = hook.received + 1
hook.save()

View File

@ -1,18 +1,13 @@
import uuid
import orjson
from alpaca.trading.client import TradingClient
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.views import APIView
from serde import ValidationError
from rest_framework.parsers import FormParser
from core.forms import HookForm
from core.lib.serde import drakdoo_s
from core.models import Account, Callback, Hook
from core.lib import trades
from core.models import Account
from core.util import logs
log = logs.get_logger(__name__)
@ -22,25 +17,21 @@ def get_positions(user, account_id=None):
items = []
accounts = Account.objects.filter(user=user)
for account in accounts:
if account.exchange == "alpaca":
trading_client = TradingClient(
account.api_key, account.api_secret, paper=account.sandbox
)
positions = trading_client.get_all_positions()
print("POSITIONS", positions)
positions = account.client.get_all_positions()
print("POSITIONS", positions)
for item in positions:
item = dict(item)
item["account_id"] = account.id
item["unrealized_pl"] = float(item["unrealized_pl"])
items.append(item)
# 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
for item in positions:
item = dict(item)
item["account_id"] = account.id
item["unrealized_pl"] = float(item["unrealized_pl"])
items.append(item)
# 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
return items
@ -64,3 +55,42 @@ class Positions(LoginRequiredMixin, View):
"type": type,
}
return render(request, template_name, context)
class PositionAction(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/view-position.html"
parser_classes = [FormParser]
async def get(self, request, type, account_id, asset_id):
"""
Get live information for a trade.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
account = Account.get_by_id(account_id, request.user)
success, info = trades.get_position_info(account, asset_id)
print("INFO", info)
if not success:
message = "Position does not exist"
message_class = "danger"
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,
}
if success:
context["items"] = info
else:
context["message"] = message
context["class"] = message_class
return render(request, template_name, context)

188
core/views/strategies.py Normal file
View File

@ -0,0 +1,188 @@
import uuid
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest
from django.shortcuts import render
from django.views import View # , CreateView, UpdateView, DeleteView
from rest_framework.parsers import FormParser
from rest_framework.views import APIView
from core.forms import StrategyForm
from core.models import Strategy
from core.util import logs
# from django.urls import reverse
log = logs.get_logger(__name__)
def get_strategies(user):
strategies = Strategy.objects.filter(user=user)
return strategies
class Strategies(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/strategies.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]
strategies = get_strategies(request.user)
if type == "page":
type = "modal"
context = {
"title": f"Accounts ({type})",
"unique": unique,
"window_content": self.window_content,
"items": strategies,
"type": type,
}
return render(request, template_name, context)
# class AddStrategy(CreateView):
# model = Strategy
# form_class = StrategyForm
# template_name = "window-content/add-strategy.html"
# success_url = reverse("strategies")
# def form_valid(self, form):
# form.instance.user = self.request.user
# return super().form_valid(form)
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context["title"] = "Add Strategy"
# context["window_content"] = "window-content/add_strategy.html"
# return context
class StrategiesAction(LoginRequiredMixin, APIView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/add-strategy.html"
parser_classes = [FormParser]
def get(self, request, type, strategy_id=None):
"""
Get the form for adding or editing a strategy.
:param strategy_id: The id of the strategy to edit. Optional.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if strategy_id:
try:
account = Strategy.objects.get(id=strategy_id)
form = StrategyForm(instance=account)
except Strategy.DoesNotExist:
message = "Strategy 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 = StrategyForm()
if type == "page":
type = "modal"
context = {
"form": form,
"strategy_id": strategy_id,
"type": type,
"unique": unique,
"window_content": self.window_content,
}
return render(request, template_name, context)
def put(self, request, type, strategy_id=None):
"""
Add or edit a account.
:param account_id: The id of the strategy to edit. Optional.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
message = None
message_class = "success"
if strategy_id:
try:
form = StrategyForm(
request.data,
instance=Strategy.objects.get(
id=strategy_id, account__user=request.user
),
)
except Strategy.DoesNotExist:
message = "Strategy does not exist"
message_class = "danger"
context = {
"message": message,
"class": message_class,
}
return render(request, self.template_name, context)
else:
form = StrategyForm(request.data)
if form.is_valid():
hooks = list(form.cleaned_data.get("hooks"))
strategy = form.save(commit=False)
strategy.user = request.user
strategy.hooks.set(hooks)
strategy.save()
print("HOOKS SET", strategy.hooks)
if strategy_id:
message = f"Strategy {strategy_id} edited successfully"
else:
message = f"Strategy {strategy.id} added successfully"
else:
message = "Error adding strategy"
message_class = "danger"
accounts = get_strategies(request.user)
context = {
"items": accounts,
"type": type,
}
if message:
context["message"] = message
context["class"] = message_class
template_name = "partials/strategy-list.html"
return render(request, template_name, context)
def delete(self, request, type, strategy_id):
"""
Delete a strategy.
:param strategy_id: The id of the strategy to delete.
"""
if type not in self.allowed_types:
return HttpResponseBadRequest
message = None
message_class = "success"
try:
strategy = Strategy.objects.get(id=strategy_id, user=request.user)
strategy.delete()
message = "Strategy deleted successfully"
except Strategy.DoesNotExist:
message = "Error deleting strategy"
message_class = "danger"
strategies = get_strategies(request.user)
context = {
"items": strategies,
"type": strategies,
}
if message:
context["message"] = message
context["class"] = message_class
template_name = "partials/strategy-list.html"
return render(request, template_name, context)

View File

@ -111,7 +111,10 @@ class TradeAction(LoginRequiredMixin, APIView):
if trade_id:
try:
form = TradeForm(request.data, instance=Trade.objects.get(id=trade_id))
form = TradeForm(
request.data,
instance=Trade.objects.get(id=trade_id, account__user=request.user),
)
except Trade.DoesNotExist:
message = "Trade does not exist"
message_class = "danger"
@ -125,12 +128,18 @@ class TradeAction(LoginRequiredMixin, APIView):
if form.is_valid():
trade = form.save(commit=False)
print("PRESAVE TRADE", trade)
trade.user = request.user
trade.save()
print("SAVED TRADE", trade)
if trade_id:
message = f"Trade {trade_id} edited successfully"
success, returned = trade.post()
if success:
print("SAVED TRADE", trade)
if trade_id:
message = f"Trade {trade_id} edited successfully"
else:
message = f"Trade {trade.id} added successfully"
else:
message = f"Trade {trade.id} added successfully"
message = f"Error adding trade: {returned}"
message_class = "danger"
else:
message = "Error adding trade"
message_class = "danger"