Implement asset rules as Asset Group children objects
This commit is contained in:
parent
b48af50620
commit
6ff5f718ba
46
app/urls.py
46
app/urls.py
|
@ -263,41 +263,25 @@ urlpatterns = [
|
||||||
assets.AssetGroupDelete.as_view(),
|
assets.AssetGroupDelete.as_view(),
|
||||||
name="assetgroup_delete",
|
name="assetgroup_delete",
|
||||||
),
|
),
|
||||||
# Asset Restrictions
|
# Asset Rules
|
||||||
# path(
|
|
||||||
# "restriction/<str:type>/<str:group>/",
|
|
||||||
# assets.AssetRestrictionList.as_view(),
|
|
||||||
# name="assetrestrictions",
|
|
||||||
# ),
|
|
||||||
# path(
|
|
||||||
# "restriction/<str:type>/create/<str:group>/",
|
|
||||||
# assets.AssetRestrictionCreate.as_view(),
|
|
||||||
# name="assetrestriction_create",
|
|
||||||
# ),
|
|
||||||
# path(
|
|
||||||
# "restriction/<str:type>/update/<str:group>/<str:pk>/",
|
|
||||||
# assets.AssetRestrictionUpdate.as_view(),
|
|
||||||
# name="assetrestriction_update",
|
|
||||||
# ),
|
|
||||||
# path(
|
|
||||||
# "restriction/<str:type>/delete/<str:group>/<str:pk>/",
|
|
||||||
# assets.AssetRestrictionDelete.as_view(),
|
|
||||||
# name="assetrestriction_delete",
|
|
||||||
# ),
|
|
||||||
# Asset group filters
|
|
||||||
path(
|
path(
|
||||||
"assetfilter/<str:group_id>/flip/<str:symbol>/",
|
"assetrule/<str:type>/<str:group>/",
|
||||||
assets.AssetFilterFlip.as_view(),
|
assets.AssetRuleList.as_view(),
|
||||||
name="assetfilter_flip",
|
name="assetrules",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"assetfilter/<str:group_id>/delete/<str:symbol>/",
|
"assetrule/<str:type>/create/<str:group>/",
|
||||||
assets.AssetFilterDelete.as_view(),
|
assets.AssetRuleCreate.as_view(),
|
||||||
name="assetfilter_delete",
|
name="assetrule_create",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"assetfilter/<str:type>/view/<str:group_id>/",
|
"assetrule/<str:type>/update/<str:group>/<str:pk>/",
|
||||||
assets.AssetFilterList.as_view(),
|
assets.AssetRuleUpdate.as_view(),
|
||||||
name="assetfilters",
|
name="assetrule_update",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"assetrule/<str:type>/delete/<str:group>/<str:pk>/",
|
||||||
|
assets.AssetRuleDelete.as_view(),
|
||||||
|
name="assetrule_delete",
|
||||||
),
|
),
|
||||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
100
core/forms.py
100
core/forms.py
|
@ -7,6 +7,7 @@ from mixins.restrictions import RestrictedFormMixin
|
||||||
from .models import ( # AssetRestriction,
|
from .models import ( # AssetRestriction,
|
||||||
Account,
|
Account,
|
||||||
AssetGroup,
|
AssetGroup,
|
||||||
|
AssetRule,
|
||||||
Hook,
|
Hook,
|
||||||
NotificationSettings,
|
NotificationSettings,
|
||||||
RiskModel,
|
RiskModel,
|
||||||
|
@ -308,85 +309,38 @@ class AssetGroupForm(RestrictedFormMixin, ModelForm):
|
||||||
fields = (
|
fields = (
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
"aggregation",
|
"when_no_data",
|
||||||
"trigger_below",
|
"when_no_match",
|
||||||
"trigger_above",
|
"when_no_aggregation",
|
||||||
|
"when_not_in_bounds",
|
||||||
|
"when_bullish",
|
||||||
|
"when_bearish",
|
||||||
)
|
)
|
||||||
help_texts = {
|
help_texts = {
|
||||||
"name": "Name of the asset group. Informational only.",
|
"name": "Name of the asset group. Informational only.",
|
||||||
"description": "Description of the asset group. Informational only.",
|
"description": "Description of the asset group. Informational only.",
|
||||||
"aggregation": "The aggregation method to use for this asset group.",
|
"when_no_data": "The action to take when no webhooks have been received for an asset.",
|
||||||
"trigger_below": "Trigger when the aggregation is below this value.",
|
"when_no_match": "The action to take when there were no matches last callback for an asset.",
|
||||||
"trigger_above": "Trigger when the aggregation is above this value.",
|
"when_no_aggregation": "The action to take when there is no defined aggregations for the asset.",
|
||||||
|
"when_not_in_bounds": "The action to take when the aggregation is not breaching either bound.",
|
||||||
|
"when_bullish": "The action to take when the asset is bullish.",
|
||||||
|
"when_bearish": "The action to take when the asset is bearish.",
|
||||||
}
|
}
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super(AssetGroupForm, self).clean()
|
class AssetRuleForm(RestrictedFormMixin, ModelForm):
|
||||||
if "aggregation" in cleaned_data:
|
def __init__(self, *args, **kwargs):
|
||||||
if cleaned_data["aggregation"] == "none":
|
super(AssetRuleForm, self).__init__(*args, **kwargs)
|
||||||
if "trigger_below" in cleaned_data and cleaned_data["trigger_below"]:
|
self.fields["value"].disabled = True
|
||||||
self.add_error(
|
self.fields["aggregation"].disabled = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AssetRule
|
||||||
|
fields = (
|
||||||
|
"asset",
|
||||||
|
"aggregation",
|
||||||
|
"value",
|
||||||
|
"status",
|
||||||
"trigger_below",
|
"trigger_below",
|
||||||
"You cannot specify a trigger below value when aggregation is set to none.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if "trigger_above" in cleaned_data and cleaned_data["trigger_above"]:
|
|
||||||
self.add_error(
|
|
||||||
"trigger_above",
|
"trigger_above",
|
||||||
"You cannot specify a trigger above value when aggregation is set to none.",
|
|
||||||
)
|
)
|
||||||
return
|
|
||||||
else:
|
|
||||||
# Check if either trigger_below or trigger_above has been set
|
|
||||||
if not any(
|
|
||||||
[cleaned_data["trigger_below"], cleaned_data["trigger_above"]]
|
|
||||||
):
|
|
||||||
self.add_error(
|
|
||||||
"trigger_below",
|
|
||||||
"You must specify a trigger below and/or trigger above value when aggregation is set to anything other than none.",
|
|
||||||
)
|
|
||||||
self.add_error(
|
|
||||||
"trigger_above",
|
|
||||||
"You must specify a trigger below and/or trigger above value when aggregation is set to anything other than none.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestrictionForm(RestrictedFormMixin, ModelForm):
|
|
||||||
# class Meta:
|
|
||||||
# model = AssetRestriction
|
|
||||||
# fields = (
|
|
||||||
# "name",
|
|
||||||
# "description",
|
|
||||||
# "pairs",
|
|
||||||
# "pairs_parsed",
|
|
||||||
# )
|
|
||||||
# help_texts = {
|
|
||||||
# "name": "Name of the asset restriction group. Informational only.",
|
|
||||||
# "description": "Description of the asset restriction group. Informational only.",
|
|
||||||
# "pairs": "Comma-separated list of pairs to restrict when a webhook is received. This does nothing on its own.",
|
|
||||||
# }
|
|
||||||
|
|
||||||
# pairs_parsed = forms.BooleanField(widget=forms.HiddenInput, required=False)
|
|
||||||
|
|
||||||
# def clean(self):
|
|
||||||
# cleaned_data = super(AssetRestrictionForm, self).clean()
|
|
||||||
# if "pairs" in cleaned_data and cleaned_data["pairs"]:
|
|
||||||
# new_pairs = []
|
|
||||||
# pair_split = cleaned_data["pairs"].split(",")
|
|
||||||
# if not pair_split:
|
|
||||||
# self.add_error("pairs", "You must specify at least one pair.")
|
|
||||||
# return
|
|
||||||
# for pair in pair_split:
|
|
||||||
# if pair:
|
|
||||||
# new_pairs.append(pair.strip())
|
|
||||||
# else:
|
|
||||||
# self.add_error("pairs", f"You cannot have an empty pair: {pair}")
|
|
||||||
# return
|
|
||||||
|
|
||||||
# cleaned_data["pairs_parsed"] = new_pairs
|
|
||||||
# else:
|
|
||||||
# cleaned_data["pairs_parsed"] = {}
|
|
||||||
|
|
||||||
# return cleaned_data
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 4.1.6 on 2023-02-13 18:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0060_assetgroup_aggregation_assetgroup_trigger_above_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AssetRule',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('asset', models.CharField(max_length=64)),
|
||||||
|
('aggregation', models.CharField(choices=[('none', 'None'), ('avg_sentiment', 'Average sentiment')], default='none', max_length=255)),
|
||||||
|
('value', models.FloatField(blank=True, null=True)),
|
||||||
|
('status', models.FloatField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Positive'), (3, 'Negative')], default=0)),
|
||||||
|
('trigger_below', models.FloatField(blank=True, null=True)),
|
||||||
|
('trigger_above', models.FloatField(blank=True, null=True)),
|
||||||
|
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.assetgroup')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.6 on 2023-02-13 18:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0061_assetrule'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='assetrule',
|
||||||
|
name='status',
|
||||||
|
field=models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Positive'), (3, 'Negative')], default=0),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 4.1.6 on 2023-02-13 19:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0062_alter_assetrule_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='assetrule',
|
||||||
|
unique_together={('asset', 'group')},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 4.1.6 on 2023-02-13 19:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0063_alter_assetrule_unique_together'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='aggregation',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='allowed',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='trigger_above',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='trigger_below',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Generated by Django 4.1.6 on 2023-02-13 20:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0064_remove_assetgroup_aggregation_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='when_bearish',
|
||||||
|
field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='when_bullish',
|
||||||
|
field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='when_no_aggregation',
|
||||||
|
field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='when_no_data',
|
||||||
|
field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='when_no_match',
|
||||||
|
field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetgroup',
|
||||||
|
name='when_not_in_bounds',
|
||||||
|
field=models.IntegerField(choices=[(6, 'Ignore (no action)'), (-1, 'Default (no remapping)'), (2, 'Bullish'), (3, 'Bearish')], default=-1, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='assetrule',
|
||||||
|
name='original_status',
|
||||||
|
field=models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'No action')], default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='assetrule',
|
||||||
|
name='status',
|
||||||
|
field=models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'No action')], default=0),
|
||||||
|
),
|
||||||
|
]
|
|
@ -46,6 +46,24 @@ AGGREGATION_CHOICES = (
|
||||||
("avg_sentiment", "Average sentiment"),
|
("avg_sentiment", "Average sentiment"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
(0, "No data"),
|
||||||
|
(1, "No match"),
|
||||||
|
(2, "Bullish"),
|
||||||
|
(3, "Bearish"),
|
||||||
|
(4, "No aggregation"),
|
||||||
|
(5, "Not in bounds"),
|
||||||
|
(6, "Always allow"),
|
||||||
|
(7, "Always deny"),
|
||||||
|
)
|
||||||
|
|
||||||
|
MAPPING_CHOICES = (
|
||||||
|
(6, "Always allow"),
|
||||||
|
(7, "Always deny"),
|
||||||
|
(2, "Bullish"),
|
||||||
|
(3, "Bearish"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Plan(models.Model):
|
class Plan(models.Model):
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
@ -416,16 +434,17 @@ class AssetGroup(models.Model):
|
||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
# Dict like {"RUB": True, "USD": False}
|
# Dict like {"RUB": True, "USD": False}
|
||||||
allowed = models.JSONField(null=True, blank=True, default=dict)
|
# allowed = models.JSONField(null=True, blank=True, default=dict)
|
||||||
|
|
||||||
webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||||
|
|
||||||
aggregation = models.CharField(
|
when_no_data = models.IntegerField(choices=MAPPING_CHOICES, default=7)
|
||||||
choices=AGGREGATION_CHOICES, max_length=255, default="none"
|
when_no_match = models.IntegerField(choices=MAPPING_CHOICES, default=6)
|
||||||
)
|
when_no_aggregation = models.IntegerField(choices=MAPPING_CHOICES, default=6)
|
||||||
|
when_not_in_bounds = models.IntegerField(choices=MAPPING_CHOICES, default=6)
|
||||||
|
|
||||||
trigger_below = models.FloatField(null=True, blank=True)
|
when_bullish = models.IntegerField(choices=MAPPING_CHOICES, default=2)
|
||||||
trigger_above = models.FloatField(null=True, blank=True)
|
when_bearish = models.IntegerField(choices=MAPPING_CHOICES, default=3)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
@ -435,20 +454,24 @@ class AssetGroup(models.Model):
|
||||||
"""
|
"""
|
||||||
Get the total number of matches for this group.
|
Get the total number of matches for this group.
|
||||||
"""
|
"""
|
||||||
if isinstance(self.allowed, dict):
|
asset_rule_total = AssetRule.objects.filter(group=self).count()
|
||||||
truthy_values = [x for x in self.allowed.values() if x is True]
|
asset_rule_positive = AssetRule.objects.filter(group=self, status=2).count()
|
||||||
return f"{len(truthy_values)}/{len(self.allowed)}"
|
return f"{asset_rule_positive}/{asset_rule_total}"
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestriction(models.Model):
|
class AssetRule(models.Model):
|
||||||
# user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
# name = models.CharField(max_length=255)
|
asset = models.CharField(max_length=64)
|
||||||
# description = models.TextField(null=True, blank=True)
|
group = models.ForeignKey(AssetGroup, on_delete=models.CASCADE)
|
||||||
# pairs = models.CharField(max_length=4096, null=True, blank=True)
|
aggregation = models.CharField(
|
||||||
# pairs_parsed = models.JSONField(null=True, blank=True, default=list)
|
choices=AGGREGATION_CHOICES, max_length=255, default="none"
|
||||||
|
)
|
||||||
|
value = models.FloatField(null=True, blank=True)
|
||||||
|
original_status = models.IntegerField(choices=STATUS_CHOICES, default=0)
|
||||||
|
status = models.IntegerField(choices=STATUS_CHOICES, default=0)
|
||||||
|
trigger_below = models.FloatField(null=True, blank=True)
|
||||||
|
trigger_above = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
# Ensure that the asset is unique per group
|
||||||
|
class Meta:
|
||||||
# group = models.ForeignKey(
|
unique_together = ("asset", "group")
|
||||||
# AssetGroup, on_delete=models.CASCADE, null=True, blank=True
|
|
||||||
# )
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% load cache %}
|
{% load cache %}
|
||||||
{% load cachalot cache %}
|
{% load cachalot cache %}
|
||||||
{% get_last_invalidation 'core.AssetGroup' as last %}
|
{% get_last_invalidation 'core.AssetGroup' 'core.AssetRule' as last %}
|
||||||
{% include 'mixins/partials/notify.html' %}
|
{% include 'mixins/partials/notify.html' %}
|
||||||
{% cache 600 objects_assetgroups request.user.id object_list last %}
|
{% cache 600 objects_assetgroups request.user.id object_list last %}
|
||||||
<table
|
<table
|
||||||
|
@ -27,11 +27,7 @@
|
||||||
<td>{{ item.description }}</td>
|
<td>{{ item.description }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
href="{% url 'assetrules' type='page' group=item.id %}">
|
||||||
hx-get="{% url 'assetfilters' type=type group_id=item.id %}"
|
|
||||||
hx-trigger="click"
|
|
||||||
hx-target="#{{ type }}s-here"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
{{ item.matches }}
|
{{ item.matches }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% load cache %}
|
{% load cache %}
|
||||||
{% load cachalot cache %}
|
{% load cachalot cache %}
|
||||||
{% get_last_invalidation 'core.AssetRestriction' as last %}
|
{% get_last_invalidation 'core.AssetRule' as last %}
|
||||||
{% include 'mixins/partials/notify.html' %}
|
{% include 'mixins/partials/notify.html' %}
|
||||||
{% cache 600 objects_assetrestrictions request.user.id object_list last %}
|
{% cache 600 objects_assetrules request.user.id object_list last %}
|
||||||
<table
|
<table
|
||||||
class="table is-fullwidth is-hoverable"
|
class="table is-fullwidth is-hoverable"
|
||||||
hx-target="#{{ context_object_name }}-table"
|
hx-target="#{{ context_object_name }}-table"
|
||||||
|
@ -13,35 +13,37 @@
|
||||||
<thead>
|
<thead>
|
||||||
<th>id</th>
|
<th>id</th>
|
||||||
<th>user</th>
|
<th>user</th>
|
||||||
<th>name</th>
|
<th>asset</th>
|
||||||
<th>description</th>
|
|
||||||
<th>pairs</th>
|
|
||||||
<th>group</th>
|
<th>group</th>
|
||||||
<th>hook</th>
|
<th>aggregation</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>original status</th>
|
||||||
|
<th>status</th>
|
||||||
|
<th>trigger above</th>
|
||||||
|
<th>trigger below</th>
|
||||||
<th>actions</th>
|
<th>actions</th>
|
||||||
</thead>
|
</thead>
|
||||||
{% for item in object_list %}
|
{% for item in object_list %}
|
||||||
<tr>
|
<tr class="
|
||||||
|
{% if item.status == 2 %}has-background-success-light
|
||||||
|
{% elif item.status == 3 %}has-background-danger-light
|
||||||
|
{% elif item.status == 0 %}has-background-grey-light
|
||||||
|
{% endif %}">
|
||||||
<td>{{ item.id }}</td>
|
<td>{{ item.id }}</td>
|
||||||
<td>{{ item.user }}</td>
|
<td>{{ item.user }}</td>
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.asset }}</td>
|
||||||
<td>{{ item.description }}</td>
|
|
||||||
<td>{{ item.pairs_parsed|length }}</td>
|
|
||||||
<td>{{ item.group }}</td>
|
<td>{{ item.group }}</td>
|
||||||
<td>
|
<td>{{ item.get_aggregation_display }}</td>
|
||||||
<a
|
<td>{{ item.value }}</td>
|
||||||
class="has-text-grey"
|
<td>{{ item.get_original_status_display }}</td>
|
||||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{settings.URL}}/{{settings.ASSET_PATH}}/{{ item.webhook_id }}/');">
|
<td>{{ item.get_status_display }}</td>
|
||||||
<span class="icon" data-tooltip="Copy to clipboard">
|
<td>{{ item.trigger_above }}</td>
|
||||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
<td>{{ item.trigger_below }}</td>
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button
|
<button
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
hx-get="{% url 'assetrestriction_update' type=type group=item.group.id pk=item.id %}"
|
hx-get="{% url 'assetrule_update' type=type group=item.group.id pk=item.id %}"
|
||||||
hx-trigger="click"
|
hx-trigger="click"
|
||||||
hx-target="#{{ type }}s-here"
|
hx-target="#{{ type }}s-here"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
|
@ -54,7 +56,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
hx-delete="{% url 'assetrestriction_delete' type=type group=item.group.id pk=item.id %}"
|
hx-delete="{% url 'assetrule_delete' type=type group=item.group.id pk=item.id %}"
|
||||||
hx-trigger="click"
|
hx-trigger="click"
|
||||||
hx-target="#modals-here"
|
hx-target="#modals-here"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
|
@ -1,6 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from core.models import AssetGroup, User
|
from core.models import AssetGroup, AssetRule, User
|
||||||
from core.trading import assetfilter
|
from core.trading import assetfilter
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,13 +16,18 @@ class AssetfilterTestCase(TestCase):
|
||||||
description="Test group",
|
description="Test group",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_allowed_negative(self):
|
def test_get_allowed_prohibited(self):
|
||||||
"""
|
"""
|
||||||
Test that the asset filter works on negative aggregations.
|
Test that the asset filter works on negative aggregations.
|
||||||
"""
|
"""
|
||||||
# We have negative news about EUR
|
# We have negative news about EUR
|
||||||
self.group.allowed = {"EUR": False}
|
self.asset_rule = AssetRule.objects.create(
|
||||||
self.group.save()
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=3,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
# This means that:
|
# This means that:
|
||||||
# * base == EUR: long is not allowed, short is allowed
|
# * base == EUR: long is not allowed, short is allowed
|
||||||
|
@ -41,13 +46,18 @@ class AssetfilterTestCase(TestCase):
|
||||||
# Test that short on quote of EUR is not allowed
|
# Test that short on quote of EUR is not allowed
|
||||||
self.assertFalse(assetfilter.get_allowed(self.group, "USD", "EUR", "short"))
|
self.assertFalse(assetfilter.get_allowed(self.group, "USD", "EUR", "short"))
|
||||||
|
|
||||||
def test_get_allowed_positive(self):
|
def test_get_allowed_permitted(self):
|
||||||
"""
|
"""
|
||||||
Test that the asset filter works on positive aggregations.
|
Test that the asset filter works on positive aggregations.
|
||||||
"""
|
"""
|
||||||
# We have positive news about EUR
|
# We have positive news about EUR
|
||||||
self.group.allowed = {"EUR": True}
|
self.asset_rule = AssetRule.objects.create(
|
||||||
self.group.save()
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=2,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
# This means that:
|
# This means that:
|
||||||
# * base == EUR: long is allowed, short is not allowed
|
# * base == EUR: long is allowed, short is not allowed
|
||||||
|
@ -66,69 +76,188 @@ class AssetfilterTestCase(TestCase):
|
||||||
# Test that long on quote of EUR is not allowed
|
# Test that long on quote of EUR is not allowed
|
||||||
self.assertFalse(assetfilter.get_allowed(self.group, "USD", "EUR", "long"))
|
self.assertFalse(assetfilter.get_allowed(self.group, "USD", "EUR", "long"))
|
||||||
|
|
||||||
|
def test_get_allowed_no_data(self):
|
||||||
|
self.asset_rule = AssetRule.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=0,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
|
self.assertTrue(assetfilter.get_allowed(self.group, "EUR", "USD", "long"))
|
||||||
|
|
||||||
|
def test_get_allowed_no_match(self):
|
||||||
|
self.asset_rule = AssetRule.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=1,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
|
self.assertTrue(assetfilter.get_allowed(self.group, "EUR", "USD", "long"))
|
||||||
|
|
||||||
|
def test_get_allowed_no_aggregation(self):
|
||||||
|
self.asset_rule = AssetRule.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=4,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
|
self.assertTrue(assetfilter.get_allowed(self.group, "EUR", "USD", "long"))
|
||||||
|
|
||||||
|
def test_get_allowed_not_in_bounds(self):
|
||||||
|
self.asset_rule = AssetRule.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=5,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
|
def test_get_allowed_always_allow(self):
|
||||||
|
self.asset_rule = AssetRule.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=6,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
|
self.assertTrue(assetfilter.get_allowed(self.group, "EUR", "USD", "long"))
|
||||||
|
|
||||||
|
def test_get_allowed_always_deny(self):
|
||||||
|
self.asset_rule = AssetRule.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
group=self.group,
|
||||||
|
asset="EUR",
|
||||||
|
status=7,
|
||||||
|
)
|
||||||
|
self.asset_rule.save()
|
||||||
|
|
||||||
|
self.assertFalse(assetfilter.get_allowed(self.group, "EUR", "USD", "long"))
|
||||||
|
|
||||||
def test_check_asset_aggregation(self):
|
def test_check_asset_aggregation(self):
|
||||||
"""
|
"""
|
||||||
Test that the asset aggregation works.
|
Test that the asset aggregation works.
|
||||||
"""
|
"""
|
||||||
# Test within lower bound
|
# Test within lower bound
|
||||||
self.assertTrue(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=None, trigger_below=2.0
|
1.0, trigger_above=None, trigger_below=2.0
|
||||||
)
|
),
|
||||||
|
3, # Prohibited
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test within upper bound
|
# Test within upper bound
|
||||||
self.assertTrue(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=0.0, trigger_below=None
|
1.0, trigger_above=0.0, trigger_below=None
|
||||||
)
|
),
|
||||||
|
2, # Permitted
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test within bounds
|
# Test within bounds
|
||||||
self.assertTrue(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=0.0, trigger_below=2.0
|
1.0, trigger_above=0.0, trigger_below=2.0
|
||||||
)
|
),
|
||||||
)
|
3, # Prohibited
|
||||||
|
|
||||||
# Test outside bounds
|
|
||||||
self.assertFalse(
|
|
||||||
assetfilter.check_asset_aggregation(
|
|
||||||
1.0, trigger_above=2.0, trigger_below=3.0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test outside lower bound
|
# Test outside lower bound
|
||||||
self.assertFalse(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=None, trigger_below=0.0
|
1.0, trigger_above=None, trigger_below=0.0
|
||||||
)
|
),
|
||||||
|
5, # Not in bounds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test outside upper bound
|
# Test outside upper bound
|
||||||
self.assertFalse(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=2.0, trigger_below=None
|
1.0, trigger_above=2.0, trigger_below=None
|
||||||
)
|
),
|
||||||
|
5, # Not in bounds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test no bounds, just to be sure
|
# Test no bounds, just to be sure
|
||||||
self.assertFalse(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=None, trigger_below=None
|
1.0, trigger_above=None, trigger_below=None
|
||||||
)
|
),
|
||||||
|
4, # No aggregation
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test both bounds, but inverted
|
# Test both bounds, but inverted
|
||||||
self.assertFalse(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
1.0, trigger_above=2.0, trigger_below=0.0
|
1.0, trigger_above=2.0, trigger_below=0.0
|
||||||
)
|
),
|
||||||
|
5, # Not in bounds
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test within negative and positive bounds
|
# Test within negative and positive bounds
|
||||||
self.assertTrue(
|
self.assertEqual(
|
||||||
assetfilter.check_asset_aggregation(
|
assetfilter.check_asset_aggregation(
|
||||||
-1.0, trigger_above=-2.0, trigger_below=0.0
|
-1.0, trigger_above=-2.0, trigger_below=0.0
|
||||||
|
),
|
||||||
|
3, # Prohibited
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_update_status_from_mappings_when_no_data(self):
|
||||||
|
self.group.when_no_data = 2 # Bullish
|
||||||
|
self.group.save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
assetfilter.update_status_from_mappings(0, self.group),
|
||||||
|
2, # Bullish
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_status_from_mappings_when_no_match(self):
|
||||||
|
self.group.when_no_match = 2
|
||||||
|
self.group.save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
assetfilter.update_status_from_mappings(1, self.group),
|
||||||
|
2, # Bullish
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_status_from_mappings_when_no_aggregation(self):
|
||||||
|
self.group.when_no_aggregation = 2
|
||||||
|
self.group.save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
assetfilter.update_status_from_mappings(4, self.group),
|
||||||
|
2, # Bullish
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_status_from_mappings_when_not_in_bounds(self):
|
||||||
|
self.group.when_not_in_bounds = 2
|
||||||
|
self.group.save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
assetfilter.update_status_from_mappings(5, self.group),
|
||||||
|
2, # Bullish
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_status_from_mappings_when_bullish(self):
|
||||||
|
self.group.when_bullish = 3 # Bearish
|
||||||
|
self.group.save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
assetfilter.update_status_from_mappings(2, self.group),
|
||||||
|
3, # Bearish
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_update_status_from_mappings_when_bearish(self):
|
||||||
|
self.group.when_bearish = 2 # Bullish
|
||||||
|
self.group.save()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
assetfilter.update_status_from_mappings(3, self.group),
|
||||||
|
2, # Bullish
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
from core.models import AssetRule
|
||||||
|
|
||||||
|
|
||||||
def get_allowed(group, base, quote, direction):
|
def get_allowed(group, base, quote, direction):
|
||||||
"""
|
"""
|
||||||
Determine whether the trade is allowed according to the group.
|
Determine whether the trade is allowed according to the group.
|
||||||
|
@ -8,25 +11,35 @@ def get_allowed(group, base, quote, direction):
|
||||||
:param direction: The direction of the trade
|
:param direction: The direction of the trade
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allowed = group.allowed
|
|
||||||
if not isinstance(allowed, dict):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If our base has allowed == False, we can only short it, or long the quote
|
# If our base has allowed == False, we can only short it, or long the quote
|
||||||
if base in allowed:
|
base_rule = AssetRule.objects.filter(group=group, asset=base).first()
|
||||||
if not allowed[base]:
|
if base_rule:
|
||||||
|
if base_rule.status == 6:
|
||||||
|
# Always allow
|
||||||
|
return True
|
||||||
|
elif base_rule.status == 7:
|
||||||
|
# Always deny
|
||||||
|
return False
|
||||||
|
elif base_rule.status == 3:
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
return False
|
return False
|
||||||
else:
|
elif base_rule.status == 2:
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If our quote has allowed == False, we can only long it, or short the base
|
# If our quote has allowed == False, we can only long it, or short the base
|
||||||
if quote in allowed:
|
quote_rule = AssetRule.objects.filter(group=group, asset=quote).first()
|
||||||
if not allowed[quote]:
|
if quote_rule:
|
||||||
|
if quote_rule.status == 6:
|
||||||
|
# Always allow
|
||||||
|
return True
|
||||||
|
elif quote_rule.status == 7:
|
||||||
|
# Always deny
|
||||||
|
return False
|
||||||
|
elif quote_rule.status == 3:
|
||||||
if direction == "short":
|
if direction == "short":
|
||||||
return False
|
return False
|
||||||
else:
|
elif quote_rule.status == 2:
|
||||||
if direction == "long":
|
if direction == "long":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -40,16 +53,39 @@ def check_asset_aggregation(value, trigger_above, trigger_below):
|
||||||
:param trigger_above: Only trigger if the value is above this
|
:param trigger_above: Only trigger if the value is above this
|
||||||
:param trigger_below: Only trigger if the value is below this
|
:param trigger_below: Only trigger if the value is below this
|
||||||
"""
|
"""
|
||||||
# If both are defined
|
|
||||||
if trigger_above is not None and trigger_below is not None:
|
|
||||||
if value > trigger_above and value < trigger_below:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
if trigger_below is not None:
|
if trigger_below is not None:
|
||||||
if value < trigger_below:
|
if value < trigger_below:
|
||||||
# Value is less than lower bound, match
|
# Prohibited
|
||||||
return True
|
return 3
|
||||||
if trigger_above is not None:
|
if trigger_above is not None:
|
||||||
if value > trigger_above:
|
if value > trigger_above:
|
||||||
return True
|
# Permitted
|
||||||
return False
|
return 2
|
||||||
|
# Neither is defined, so we match
|
||||||
|
if not any(x is not None for x in [trigger_above, trigger_below]):
|
||||||
|
# No aggregation
|
||||||
|
return 4
|
||||||
|
# Not in bounds
|
||||||
|
return 5
|
||||||
|
|
||||||
|
|
||||||
|
def update_status_from_mappings(status, asset_group):
|
||||||
|
if status == 0:
|
||||||
|
if asset_group.when_no_data > -1:
|
||||||
|
return asset_group.when_no_data
|
||||||
|
elif status == 1:
|
||||||
|
if asset_group.when_no_match > -1:
|
||||||
|
return asset_group.when_no_match
|
||||||
|
elif status == 2:
|
||||||
|
if asset_group.when_bullish > -1:
|
||||||
|
return asset_group.when_bullish
|
||||||
|
elif status == 3:
|
||||||
|
if asset_group.when_bearish > -1:
|
||||||
|
return asset_group.when_bearish
|
||||||
|
elif status == 4:
|
||||||
|
if asset_group.when_no_aggregation > -1:
|
||||||
|
return asset_group.when_no_aggregation
|
||||||
|
elif status == 5:
|
||||||
|
if asset_group.when_not_in_bounds > -1:
|
||||||
|
return asset_group.when_not_in_bounds
|
||||||
|
return status
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render
|
from mixins.views import AbortSave, ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||||
from django.views import View
|
|
||||||
from mixins.views import (
|
|
||||||
ObjectCreate,
|
|
||||||
ObjectDelete,
|
|
||||||
ObjectList,
|
|
||||||
ObjectNameMixin,
|
|
||||||
ObjectUpdate,
|
|
||||||
)
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from core.forms import AssetGroupForm # , AssetRestrictionForm
|
from core.forms import AssetGroupForm, AssetRuleForm
|
||||||
from core.models import AssetGroup # , AssetRestriction
|
from core.models import AssetGroup, AssetRule
|
||||||
from core.trading import assetfilter
|
from core.trading import assetfilter
|
||||||
from core.util import logs
|
from core.util import logs
|
||||||
|
|
||||||
|
@ -55,73 +48,69 @@ class AssetGroupDelete(LoginRequiredMixin, ObjectDelete):
|
||||||
model = AssetGroup
|
model = AssetGroup
|
||||||
|
|
||||||
|
|
||||||
# Asset Restrictions
|
# Asset Rules
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestrictionsPermissionMixin:
|
class AssetRulesPermissionMixin:
|
||||||
# # Check the user has permission to view the asset group
|
# Check the user has permission to view the asset group
|
||||||
# # We have a user check on the AssetRestriction, but we need to check the
|
# We have a user check on the AssetRestriction, but we need to check the
|
||||||
# # AssetGroup as well
|
# AssetGroup as well
|
||||||
# def set_extra_args(self, user):
|
def set_extra_args(self, user):
|
||||||
# self.extra_permission_args = {
|
self.extra_permission_args = {
|
||||||
# "group__user": user,
|
"group__user": user,
|
||||||
# "group__pk": self.kwargs["group"],
|
"group__pk": self.kwargs["group"],
|
||||||
# }
|
}
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestrictionList(
|
class AssetRuleList(LoginRequiredMixin, AssetRulesPermissionMixin, ObjectList):
|
||||||
# LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectList
|
list_template = "partials/assetrule-list.html"
|
||||||
# ):
|
model = AssetRule
|
||||||
# list_template = "partials/assetrestriction-list.html"
|
page_title = "List of asset rules"
|
||||||
# model = AssetRestriction
|
|
||||||
# page_title = "List of asset restrictions. Linked to asset groups."
|
|
||||||
# page_subtitle = (
|
|
||||||
# "Allows API calls to permit or prohibit trading on defined currency pairs."
|
|
||||||
# )
|
|
||||||
|
|
||||||
# list_url_name = "assetrestrictions"
|
list_url_name = "assetrules"
|
||||||
# list_url_args = ["type", "group"]
|
list_url_args = ["type", "group"]
|
||||||
|
|
||||||
# submit_url_name = "assetrestriction_create"
|
submit_url_name = "assetrule_create"
|
||||||
# submit_url_args = ["type", "group"]
|
submit_url_args = ["type", "group"]
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestrictionCreate(
|
class AssetRuleCreate(LoginRequiredMixin, AssetRulesPermissionMixin, ObjectCreate):
|
||||||
# LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectCreate
|
model = AssetRule
|
||||||
# ):
|
form_class = AssetRuleForm
|
||||||
# model = AssetRestriction
|
|
||||||
# form_class = AssetRestrictionForm
|
|
||||||
|
|
||||||
# submit_url_name = "assetrestriction_create"
|
submit_url_name = "assetrule_create"
|
||||||
# submit_url_args = ["type", "group"]
|
submit_url_args = ["type", "group"]
|
||||||
|
|
||||||
# def form_invalid(self, form):
|
def form_valid(self, form):
|
||||||
# """If the form is invalid, render the invalid form."""
|
"""If the form is invalid, render the invalid form."""
|
||||||
# return self.get(self.request, **self.kwargs, form=form)
|
try:
|
||||||
|
return super().form_valid(form)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "UNIQUE constraint failed" in str(e):
|
||||||
|
form.add_error("asset", "Asset rule already exists")
|
||||||
|
return self.form_invalid(form)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
# def pre_save_mutate(self, user, obj):
|
def pre_save_mutate(self, user, obj):
|
||||||
# try:
|
try:
|
||||||
# assetgroup = AssetGroup.objects.get(pk=self.kwargs["group"], user=user)
|
assetgroup = AssetGroup.objects.get(pk=self.kwargs["group"], user=user)
|
||||||
# obj.group = assetgroup
|
obj.group = assetgroup
|
||||||
# except AssetGroup.DoesNotExist:
|
except AssetGroup.DoesNotExist:
|
||||||
# log.error(f"Asset Group {self.kwargs['group']} does not exist")
|
log.error(f"Asset Group {self.kwargs['group']} does not exist")
|
||||||
# raise AbortSave("asset group does not exist or you don't have access")
|
raise AbortSave("asset group does not exist or you don't have access")
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestrictionUpdate(
|
class AssetRuleUpdate(LoginRequiredMixin, AssetRulesPermissionMixin, ObjectUpdate):
|
||||||
# LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectUpdate
|
model = AssetRule
|
||||||
# ):
|
form_class = AssetRuleForm
|
||||||
# model = AssetRestriction
|
|
||||||
# form_class = AssetRestrictionForm
|
|
||||||
|
|
||||||
# submit_url_name = "assetrestriction_update"
|
submit_url_name = "assetrule_update"
|
||||||
# submit_url_args = ["type", "pk", "group"]
|
submit_url_args = ["type", "pk", "group"]
|
||||||
|
|
||||||
|
|
||||||
# class AssetRestrictionDelete(
|
class AssetRuleDelete(LoginRequiredMixin, AssetRulesPermissionMixin, ObjectDelete):
|
||||||
# LoginRequiredMixin, AssetRestrictionsPermissionMixin, ObjectDelete
|
model = AssetRule
|
||||||
# ):
|
|
||||||
# model = AssetRestriction
|
|
||||||
|
|
||||||
|
|
||||||
class AssetGroupAPI(APIView):
|
class AssetGroupAPI(APIView):
|
||||||
|
@ -164,7 +153,6 @@ class AssetGroupAPI(APIView):
|
||||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
topic = request.data["topic"]
|
topic = request.data["topic"]
|
||||||
print("YES TOPIC", topic)
|
|
||||||
new_assets = []
|
new_assets = []
|
||||||
assets_split = topic.split(",")
|
assets_split = topic.split(",")
|
||||||
if not assets_split:
|
if not assets_split:
|
||||||
|
@ -176,88 +164,46 @@ class AssetGroupAPI(APIView):
|
||||||
else:
|
else:
|
||||||
log.error(f"Asset API {webhook_id} asset {asset} is not an asset")
|
log.error(f"Asset API {webhook_id} asset {asset} is not an asset")
|
||||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||||
print("YES PAIRS", new_assets)
|
|
||||||
|
|
||||||
|
aggregation = "none"
|
||||||
|
value = None
|
||||||
# Check if we have lower/upper bounds
|
# Check if we have lower/upper bounds
|
||||||
if group.aggregation != "none":
|
|
||||||
if "aggs" in request.data["meta"]:
|
if "aggs" in request.data["meta"]:
|
||||||
aggs = request.data["meta"]["aggs"]
|
aggs = request.data["meta"]["aggs"]
|
||||||
if group.aggregation in aggs:
|
# TODO: support more aggregations
|
||||||
if "value" in aggs[group.aggregation]:
|
if len(aggs) != 1:
|
||||||
value = aggs[group.aggregation]["value"]
|
log.error(
|
||||||
print("YES AVG", value)
|
f"Asset API {webhook_id} has an invalid number of aggregations"
|
||||||
is_match = assetfilter.check_asset_aggregation(
|
|
||||||
value, group.trigger_above, group.trigger_below
|
|
||||||
)
|
)
|
||||||
print("YES AVG IS MATCH", is_match)
|
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
first_agg_key = list(aggs.keys())[0]
|
||||||
|
if "value" in aggs[first_agg_key]:
|
||||||
|
aggregation = first_agg_key
|
||||||
|
value = aggs[first_agg_key]["value"]
|
||||||
|
|
||||||
for asset in new_assets:
|
for asset in new_assets:
|
||||||
group.allowed[asset] = is_match is True or None
|
asset_rule, _ = AssetRule.objects.get_or_create(
|
||||||
group.save()
|
group=group, user=group.user, asset=asset
|
||||||
|
)
|
||||||
|
if is_match:
|
||||||
|
# Make our own is_match based on the aggregations
|
||||||
|
is_match = assetfilter.check_asset_aggregation(
|
||||||
|
value, asset_rule.trigger_above, asset_rule.trigger_below
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No matches
|
||||||
|
is_match = None
|
||||||
|
asset_rule.aggregation = aggregation
|
||||||
|
if value:
|
||||||
|
asset_rule.value = value
|
||||||
|
if is_match is not None:
|
||||||
|
our_status = is_match
|
||||||
|
else:
|
||||||
|
our_status = 1
|
||||||
|
asset_rule.original_status = our_status
|
||||||
|
asset_rule.status = assetfilter.update_status_from_mappings(
|
||||||
|
our_status, group
|
||||||
|
)
|
||||||
|
asset_rule.save()
|
||||||
|
|
||||||
return HttpResponse(status=status.HTTP_200_OK)
|
return HttpResponse(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
# Asset group allowed field
|
|
||||||
class AssetFilterList(LoginRequiredMixin, ObjectList):
|
|
||||||
list_template = "partials/asset-filter-list.html"
|
|
||||||
page_title = "List of asset filters."
|
|
||||||
page_subtitle = None
|
|
||||||
|
|
||||||
context_object_name_singular = "asset filter"
|
|
||||||
context_object_name = "asset filters"
|
|
||||||
|
|
||||||
list_url_name = "assetfilters"
|
|
||||||
list_url_args = ["type", "group_id"]
|
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
|
||||||
group_id = kwargs.get("group_id", None)
|
|
||||||
self.extra_context = {"group_id": group_id}
|
|
||||||
try:
|
|
||||||
group = AssetGroup.objects.get(id=group_id, user=self.request.user)
|
|
||||||
except AssetGroup.DoesNotExist:
|
|
||||||
context = {"message": "Asset group does not exist", "class": "danger"}
|
|
||||||
return self.render_to_response(context)
|
|
||||||
return group.allowed
|
|
||||||
|
|
||||||
|
|
||||||
class AssetFilterFlip(LoginRequiredMixin, ObjectNameMixin, View):
|
|
||||||
template_name = "mixins/partials/notify.html"
|
|
||||||
model = AssetGroup
|
|
||||||
|
|
||||||
def get(self, request, group_id, symbol):
|
|
||||||
try:
|
|
||||||
group = AssetGroup.objects.get(id=group_id, user=request.user)
|
|
||||||
except AssetGroup.DoesNotExist:
|
|
||||||
context = {"message": "Asset group does not exist", "class": "danger"}
|
|
||||||
return render(request, self.template_name, context)
|
|
||||||
if symbol not in group.allowed:
|
|
||||||
context = {"message": "Asset filter does not exist", "class": "danger"}
|
|
||||||
return render(request, self.template_name, context)
|
|
||||||
group.allowed[symbol] = not group.allowed[symbol]
|
|
||||||
group.save()
|
|
||||||
context = {"message": "Asset filter updated", "class": "success"}
|
|
||||||
response = render(request, self.template_name, context)
|
|
||||||
response["HX-Trigger"] = "assetgroupEvent"
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class AssetFilterDelete(LoginRequiredMixin, ObjectNameMixin, View):
|
|
||||||
template_name = "mixins/partials/notify.html"
|
|
||||||
model = AssetGroup
|
|
||||||
|
|
||||||
def delete(self, request, group_id, symbol):
|
|
||||||
try:
|
|
||||||
group = AssetGroup.objects.get(id=group_id, user=request.user)
|
|
||||||
except AssetGroup.DoesNotExist:
|
|
||||||
context = {"message": "Asset group does not exist", "class": "danger"}
|
|
||||||
return render(request, self.template_name, context)
|
|
||||||
if symbol not in group.allowed:
|
|
||||||
context = {"message": "Asset filter does not exist", "class": "danger"}
|
|
||||||
return render(request, self.template_name, context)
|
|
||||||
del group.allowed[symbol]
|
|
||||||
group.save()
|
|
||||||
context = {"message": "Asset filter deleted", "class": "success"}
|
|
||||||
response = render(request, self.template_name, context)
|
|
||||||
response["HX-Trigger"] = "assetgroupEvent"
|
|
||||||
return response
|
|
||||||
|
|
|
@ -147,7 +147,7 @@ class HookList(LoginRequiredMixin, ObjectList):
|
||||||
# window_content = "window-content/hooks.html"
|
# window_content = "window-content/hooks.html"
|
||||||
list_template = "partials/hook-list.html"
|
list_template = "partials/hook-list.html"
|
||||||
model = Hook
|
model = Hook
|
||||||
page_title = "List of active URL endpoints for receiving hooks."
|
page_title = "List of active URL endpoints for receiving hooks"
|
||||||
page_subtitle = (
|
page_subtitle = (
|
||||||
"Add URLs here to receive Drakdoo callbacks. "
|
"Add URLs here to receive Drakdoo callbacks. "
|
||||||
"Make then unique and hard to guess!"
|
"Make then unique and hard to guess!"
|
||||||
|
|
|
@ -19,7 +19,7 @@ log = logs.get_logger(__name__)
|
||||||
# Trend directions
|
# Trend directions
|
||||||
class TrendDirectionList(LoginRequiredMixin, ObjectList):
|
class TrendDirectionList(LoginRequiredMixin, ObjectList):
|
||||||
list_template = "partials/trend-direction-list.html"
|
list_template = "partials/trend-direction-list.html"
|
||||||
page_title = "List of trend directions for a strategy."
|
page_title = "List of trend directions for a strategy"
|
||||||
page_subtitle = None
|
page_subtitle = None
|
||||||
|
|
||||||
context_object_name_singular = "trend direction"
|
context_object_name_singular = "trend direction"
|
||||||
|
@ -86,7 +86,7 @@ class TrendDirectionDelete(LoginRequiredMixin, ObjectNameMixin, View):
|
||||||
class TradingTimeList(LoginRequiredMixin, ObjectList):
|
class TradingTimeList(LoginRequiredMixin, ObjectList):
|
||||||
list_template = "partials/trading-time-list.html"
|
list_template = "partials/trading-time-list.html"
|
||||||
model = TradingTime
|
model = TradingTime
|
||||||
page_title = "List of allowed trading times. Used as options for a strategy."
|
page_title = "List of allowed trading times, used as options for a strategy"
|
||||||
page_subtitle = "Add times here in order to permit trading."
|
page_subtitle = "Add times here in order to permit trading."
|
||||||
|
|
||||||
list_url_name = "tradingtimes"
|
list_url_name = "tradingtimes"
|
||||||
|
|
|
@ -11,7 +11,7 @@ log = logs.get_logger(__name__)
|
||||||
class RiskList(LoginRequiredMixin, ObjectList):
|
class RiskList(LoginRequiredMixin, ObjectList):
|
||||||
list_template = "partials/risk-list.html"
|
list_template = "partials/risk-list.html"
|
||||||
model = RiskModel
|
model = RiskModel
|
||||||
page_title = "List of risk management strategies. Linked to accounts."
|
page_title = "List of risk management strategies, linked to accounts"
|
||||||
|
|
||||||
list_url_name = "risks"
|
list_url_name = "risks"
|
||||||
list_url_args = ["type"]
|
list_url_args = ["type"]
|
||||||
|
|
|
@ -11,7 +11,7 @@ log = logs.get_logger(__name__)
|
||||||
class SignalList(LoginRequiredMixin, ObjectList):
|
class SignalList(LoginRequiredMixin, ObjectList):
|
||||||
list_template = "partials/signal-list.html"
|
list_template = "partials/signal-list.html"
|
||||||
model = Signal
|
model = Signal
|
||||||
page_title = "List of signals. Linked to hooks and strategies."
|
page_title = "List of signals, linked to hooks and strategies"
|
||||||
page_subtitle = (
|
page_subtitle = (
|
||||||
"Link signals you have defined in Drakdoo to their corresponding hooks."
|
"Link signals you have defined in Drakdoo to their corresponding hooks."
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,7 +59,7 @@ class TradeList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
list_template = "partials/trade-list.html"
|
list_template = "partials/trade-list.html"
|
||||||
model = Trade
|
model = Trade
|
||||||
page_title = (
|
page_title = (
|
||||||
"List of bot and manual trades. This may not reflect actual live trades."
|
"List of bot and manual trades, this may not reflect actual live trades"
|
||||||
)
|
)
|
||||||
page_subtitle = "Trades deleted here will not be closed on the exchange."
|
page_subtitle = "Trades deleted here will not be closed on the exchange."
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue