Compare commits
103 Commits
119acdd734
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
e94d693a39
|
|||
|
7a44660fc1
|
|||
|
9ac3ffa540
|
|||
|
6ccf84be26
|
|||
|
6d9c78d2e1
|
|||
|
4079207a05
|
|||
|
761b084704
|
|||
|
95a4a6930c
|
|||
|
a788a65ba6
|
|||
|
e10c6f5c46
|
|||
|
cd32dff779
|
|||
|
a2f3170ab5
|
|||
|
3d91fb394a
|
|||
|
771a944a13
|
|||
|
542dca8324
|
|||
|
a68ade9efe
|
|||
|
aca9897f44
|
|||
|
9474a516ac
|
|||
|
8ef39ffe48
|
|||
|
b4424a7782
|
|||
|
5843000df6
|
|||
|
9d37e2bfb8
|
|||
|
cde1392e68
|
|||
|
be10375f60
|
|||
|
ac4c248175
|
|||
|
0937f7299a
|
|||
|
c6dd0ff286
|
|||
|
86ace02de8
|
|||
|
fb5521c9f7
|
|||
|
682c42c0e8
|
|||
|
9c537187f0
|
|||
|
ed63085e10
|
|||
|
ba8eb69309
|
|||
|
314d4022ea
|
|||
|
89ef8408e6
|
|||
|
9e22abe057
|
|||
|
a840be3834
|
|||
|
8bb5c2c91b
|
|||
|
db58fb34eb
|
|||
|
ea0a6f21ce
|
|||
|
8d9fe15346
|
|||
|
2b6f00a889
|
|||
|
0bf3329b61
|
|||
|
911ccde37b
|
|||
|
ae104f446a
|
|||
|
15a8bec105
|
|||
|
466b17400f
|
|||
|
3e35214e82
|
|||
|
d262f208b5
|
|||
|
67117f0978
|
|||
|
1dbb3fcf79
|
|||
|
dd3b3521d9
|
|||
|
da67177a18
|
|||
|
ffdbcecc8d
|
|||
|
c0f266da73
|
|||
|
3854bdcc7d
|
|||
|
5c090433a3
|
|||
|
eefd704800
|
|||
|
b4afa32a6e
|
|||
|
69cf8dcc10
|
|||
|
660aca44db
|
|||
|
1974b19157
|
|||
|
9a5ed32be9
|
|||
|
b37c62f5f1
|
|||
|
bc60eabb05
|
|||
|
b6952767d5
|
|||
|
0a89d96b86
|
|||
|
73cf56c50e
|
|||
|
b6126a8454
|
|||
|
7a593b902b
|
|||
|
74fdd8a735
|
|||
|
f4ae8fbc5f
|
|||
|
27de8090de
|
|||
|
1fc969177d
|
|||
|
68a33cea7d
|
|||
|
c915fd1e41
|
|||
|
507708574c
|
|||
|
6385339b7b
|
|||
|
6464b6de05
|
|||
|
6ff5f718ba
|
|||
|
b48af50620
|
|||
|
0321aff9d5
|
|||
|
dcfb963be6
|
|||
|
287facbab2
|
|||
|
da9f32e882
|
|||
|
313c7f79d0
|
|||
|
ce0b75ae2d
|
|||
|
bdf8f04210
|
|||
|
7afdd39af7
|
|||
|
33d8e26c9b
|
|||
|
dea1cfe889
|
|||
|
7d693ad1fa
|
|||
|
a0c94b2097
|
|||
|
0acddb2048
|
|||
|
8455d64e31
|
|||
|
57078c10c1
|
|||
|
1f43a00c7a
|
|||
|
010aba7f81
|
|||
|
c283c6c192
|
|||
|
aa227c53ac
|
|||
|
0b7dc001bf
|
|||
|
1d01368570
|
|||
|
101a4933c9
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -158,3 +158,5 @@ cython_debug/
|
||||
.vscode/
|
||||
core/static/admin
|
||||
core/static/debug_toolbar
|
||||
Makefile
|
||||
static/
|
||||
|
||||
26
Makefile
26
Makefile
@@ -1,26 +0,0 @@
|
||||
run:
|
||||
docker-compose --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose --env-file=stack.env logs -f
|
||||
|
||||
test:
|
||||
docker-compose --env-file=stack.env run -e LIVE=$(LIVE) --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
makemigrations:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"
|
||||
|
||||
auth:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py createsuperuser"
|
||||
|
||||
token:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
||||
26
Makefile-dev
Normal file
26
Makefile-dev
Normal file
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
docker-compose --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose --env-file=stack.env logs -f
|
||||
|
||||
test:
|
||||
docker-compose --env-file=stack.env run -e LIVE=$(LIVE) --rm app_dev sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
makemigrations:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py makemigrations"
|
||||
|
||||
auth:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py createsuperuser"
|
||||
|
||||
token:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
||||
26
Makefile-prod
Normal file
26
Makefile-prod
Normal file
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env logs -f
|
||||
|
||||
test:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run -e LIVE=$(LIVE) --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
makemigrations:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"
|
||||
|
||||
auth:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py createsuperuser"
|
||||
|
||||
token:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
||||
@@ -13,7 +13,8 @@ ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",")
|
||||
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",")
|
||||
|
||||
# Stripe
|
||||
STRIPE_ENABLED = getenv("STRIPE_ENABLED", "false").lower() in trues
|
||||
BILLING_ENABLED = getenv("BILLING_ENABLED", "false").lower() in trues
|
||||
|
||||
STRIPE_TEST = getenv("STRIPE_TEST", "true").lower() in trues
|
||||
STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "")
|
||||
STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "")
|
||||
@@ -31,6 +32,7 @@ REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues
|
||||
|
||||
# Hook URL, do not include leading or trailing slash
|
||||
HOOK_PATH = "hook"
|
||||
ASSET_PATH = "asset"
|
||||
|
||||
NOTIFY_TOPIC = getenv("NOTIFY_TOPIC", "great-fisk")
|
||||
|
||||
@@ -39,16 +41,25 @@ ELASTICSEARCH_PASSWORD = getenv("ELASTICSEARCH_PASSWORD", "changeme")
|
||||
ELASTICSEARCH_HOST = getenv("ELASTICSEARCH_HOST", "localhost")
|
||||
ELASTICSEARCH_TLS = getenv("ELASTICSEARCH_TLS", "false") in trues
|
||||
|
||||
LAGO_API_KEY = getenv("LAGO_API_KEY", "")
|
||||
LAGO_ORG_ID = getenv("LAGO_ORG_ID", "")
|
||||
LAGO_URL = getenv("LAGO_URL", "")
|
||||
|
||||
DEBUG = getenv("DEBUG", "false").lower() in trues
|
||||
PROFILER = getenv("PROFILER", "false").lower() in trues
|
||||
|
||||
REDIS_HOST = getenv("REDIS_HOST", "redis_fisk_dev")
|
||||
REDIS_PASSWORD = getenv("REDIS_PASSWORD", "changeme")
|
||||
REDIS_DB = int(getenv("REDIS_DB", "10"))
|
||||
REDIS_PORT = int(getenv("REDIS_PORT", "6379"))
|
||||
|
||||
if DEBUG:
|
||||
import socket # only if you haven't already imported this
|
||||
|
||||
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
|
||||
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
|
||||
"127.0.0.1",
|
||||
"10.0.2.2",
|
||||
# "10.0.2.2",
|
||||
]
|
||||
|
||||
SETTINGS_EXPORT = ["STRIPE_ENABLED", "URL", "HOOK_PATH"]
|
||||
SETTINGS_EXPORT = ["BILLING_ENABLED", "URL", "HOOK_PATH", "ASSET_PATH"]
|
||||
|
||||
@@ -53,7 +53,9 @@ INSTALLED_APPS = [
|
||||
# "two_factor.plugins.yubikey",
|
||||
# "otp_yubikey",
|
||||
"mixins",
|
||||
"cachalot",
|
||||
]
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bulma"
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
||||
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
|
||||
@@ -62,7 +64,9 @@ MIDDLEWARE = [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
# 'django.middleware.cache.UpdateCacheMiddleware',
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
# 'django.middleware.cache.FetchFromCacheMiddleware',
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django_otp.middleware.OTPMiddleware",
|
||||
@@ -163,7 +167,7 @@ REST_FRAMEWORK = {
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
"10.1.10.11",
|
||||
# "10.1.10.11",
|
||||
]
|
||||
|
||||
DEBUG_TOOLBAR_PANELS = [
|
||||
@@ -182,10 +186,29 @@ DEBUG_TOOLBAR_PANELS = [
|
||||
"debug_toolbar.panels.logging.LoggingPanel",
|
||||
"debug_toolbar.panels.redirects.RedirectsPanel",
|
||||
"debug_toolbar.panels.profiling.ProfilingPanel",
|
||||
"cachalot.panels.CachalotPanel",
|
||||
]
|
||||
|
||||
from app.local_settings import * # noqa
|
||||
|
||||
# Performance optimisations
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
# "LOCATION": "unix:///var/run/socks/redis.sock",
|
||||
"LOCATION": "unix:///var/run/redis.sock",
|
||||
"OPTIONS": {
|
||||
"db": REDIS_DB,
|
||||
# "parser_class": "django_redis.cache.RedisCache",
|
||||
# "PASSWORD": REDIS_PASSWORD,
|
||||
"pool_class": "redis.BlockingConnectionPool",
|
||||
},
|
||||
}
|
||||
}
|
||||
# CACHE_MIDDLEWARE_ALIAS = 'default'
|
||||
# CACHE_MIDDLEWARE_SECONDS = '600'
|
||||
# CACHE_MIDDLEWARE_KEY_PREFIX = ''
|
||||
|
||||
if PROFILER: # noqa - trust me its there
|
||||
import pyroscope
|
||||
|
||||
|
||||
101
app/urls.py
101
app/urls.py
@@ -18,7 +18,6 @@ from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
|
||||
from core.views import (
|
||||
@@ -29,6 +28,8 @@ from core.views import (
|
||||
hooks,
|
||||
limits,
|
||||
notifications,
|
||||
ordersettings,
|
||||
policies,
|
||||
positions,
|
||||
profit,
|
||||
risk,
|
||||
@@ -36,23 +37,24 @@ from core.views import (
|
||||
strategies,
|
||||
trades,
|
||||
)
|
||||
from core.views.stripe_callbacks import Callback
|
||||
|
||||
# from core.views.stripe_callbacks import Callback
|
||||
|
||||
urlpatterns = [
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
path("", base.Home.as_view(), name="home"),
|
||||
path("callback", Callback.as_view(), name="callback"),
|
||||
# path("callback", Callback.as_view(), name="callback"),
|
||||
path("billing/", base.Billing.as_view(), name="billing"),
|
||||
path("order/<str:plan_name>/", base.Order.as_view(), name="order"),
|
||||
path(
|
||||
"cancel_subscription/<str:plan_name>/",
|
||||
base.Cancel.as_view(),
|
||||
name="cancel_subscription",
|
||||
),
|
||||
path(
|
||||
"success/", TemplateView.as_view(template_name="success.html"), name="success"
|
||||
),
|
||||
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
|
||||
# path("order/<str:plan_name>/", base.Order.as_view(), name="order"),
|
||||
# path(
|
||||
# "cancel_subscription/<str:plan_name>/",
|
||||
# base.Cancel.as_view(),
|
||||
# name="cancel_subscription",
|
||||
# ),
|
||||
# path(
|
||||
# "success/", TemplateView.as_view(template_name="success.html"), name="success"
|
||||
# ),
|
||||
# path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
|
||||
path("portal", base.Portal.as_view(), name="portal"),
|
||||
path("sapp/", admin.site.urls),
|
||||
# 2FA login urls
|
||||
@@ -74,6 +76,11 @@ urlpatterns = [
|
||||
path(
|
||||
f"{settings.HOOK_PATH}/<str:hook_name>/", hooks.HookAPI.as_view(), name="hook"
|
||||
),
|
||||
path(
|
||||
f"{settings.ASSET_PATH}/<str:webhook_id>/",
|
||||
assets.AssetGroupAPI.as_view(),
|
||||
name="asset",
|
||||
),
|
||||
path("signals/<str:type>/", signals.SignalList.as_view(), name="signals"),
|
||||
path(
|
||||
"signals/<str:type>/create/",
|
||||
@@ -258,25 +265,67 @@ urlpatterns = [
|
||||
assets.AssetGroupDelete.as_view(),
|
||||
name="assetgroup_delete",
|
||||
),
|
||||
# Asset Restrictions
|
||||
# Asset Rules
|
||||
path(
|
||||
"restriction/<str:type>/<str:group>/",
|
||||
assets.AssetRestrictionList.as_view(),
|
||||
name="assetrestrictions",
|
||||
"assetrule/<str:type>/<str:group>/",
|
||||
assets.AssetRuleList.as_view(),
|
||||
name="assetrules",
|
||||
),
|
||||
path(
|
||||
"restriction/<str:type>/create/<str:group>/",
|
||||
assets.AssetRestrictionCreate.as_view(),
|
||||
name="assetrestriction_create",
|
||||
"assetrule/<str:type>/create/<str:group>/",
|
||||
assets.AssetRuleCreate.as_view(),
|
||||
name="assetrule_create",
|
||||
),
|
||||
path(
|
||||
"restriction/<str:type>/update/<str:group>/<str:pk>/",
|
||||
assets.AssetRestrictionUpdate.as_view(),
|
||||
name="assetrestriction_update",
|
||||
"assetrule/<str:type>/update/<str:group>/<str:pk>/",
|
||||
assets.AssetRuleUpdate.as_view(),
|
||||
name="assetrule_update",
|
||||
),
|
||||
path(
|
||||
"restriction/<str:type>/delete/<str:group>/<str:pk>/",
|
||||
assets.AssetRestrictionDelete.as_view(),
|
||||
name="assetrestriction_delete",
|
||||
"assetrule/<str:type>/delete/<str:group>/<str:pk>/",
|
||||
assets.AssetRuleDelete.as_view(),
|
||||
name="assetrule_delete",
|
||||
),
|
||||
# Order Settings
|
||||
path(
|
||||
"ordersettings/<str:type>/",
|
||||
ordersettings.OrderSettingsList.as_view(),
|
||||
name="ordersettings",
|
||||
),
|
||||
path(
|
||||
"ordersettings/<str:type>/create/",
|
||||
ordersettings.OrderSettingsCreate.as_view(),
|
||||
name="ordersettings_create",
|
||||
),
|
||||
path(
|
||||
"ordersettings/<str:type>/update/<str:pk>/",
|
||||
ordersettings.OrderSettingsUpdate.as_view(),
|
||||
name="ordersettings_update",
|
||||
),
|
||||
path(
|
||||
"ordersettings/<str:type>/delete/<str:pk>/",
|
||||
ordersettings.OrderSettingsDelete.as_view(),
|
||||
name="ordersettings_delete",
|
||||
),
|
||||
# Active Management Policies
|
||||
path(
|
||||
"ams/<str:type>/",
|
||||
policies.ActiveManagementPolicyList.as_view(),
|
||||
name="ams",
|
||||
),
|
||||
path(
|
||||
"ams/<str:type>/create/",
|
||||
policies.ActiveManagementPolicyCreate.as_view(),
|
||||
name="ams_create",
|
||||
),
|
||||
path(
|
||||
"ams/<str:type>/update/<str:pk>/",
|
||||
policies.ActiveManagementPolicyUpdate.as_view(),
|
||||
name="ams_update",
|
||||
),
|
||||
path(
|
||||
"ams/<str:type>/delete/<str:pk>/",
|
||||
policies.ActiveManagementPolicyDelete.as_view(),
|
||||
name="ams_delete",
|
||||
),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
@@ -2,12 +2,13 @@ from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .forms import CustomUserCreationForm
|
||||
from .models import (
|
||||
from .models import ( # AssetRestriction,; Plan,; Session,
|
||||
Account,
|
||||
AssetGroup,
|
||||
Callback,
|
||||
Hook,
|
||||
Plan,
|
||||
Session,
|
||||
NotificationSettings,
|
||||
RiskModel,
|
||||
Signal,
|
||||
Strategy,
|
||||
Trade,
|
||||
@@ -24,24 +25,24 @@ from .models import (
|
||||
|
||||
# Register your models here.
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
list_filter = ["plans"]
|
||||
# list_filter = ["plans"]
|
||||
model = User
|
||||
add_form = CustomUserCreationForm
|
||||
fieldsets = (
|
||||
*UserAdmin.fieldsets,
|
||||
(
|
||||
"Stripe information",
|
||||
{"fields": ("stripe_id",)},
|
||||
),
|
||||
(
|
||||
"Payment information",
|
||||
{
|
||||
"fields": (
|
||||
"plans",
|
||||
"last_payment",
|
||||
)
|
||||
},
|
||||
"Billing information",
|
||||
{"fields": ("billing_provider_id", "customer_id", "stripe_id")},
|
||||
),
|
||||
# (
|
||||
# "Payment information",
|
||||
# {
|
||||
# "fields": (
|
||||
# # "plans",
|
||||
# "last_payment",
|
||||
# )
|
||||
# },
|
||||
# ),
|
||||
)
|
||||
|
||||
|
||||
@@ -73,9 +74,33 @@ class StrategyAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "description", "account", "enabled")
|
||||
|
||||
|
||||
class NotificationSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "ntfy_topic", "ntfy_url")
|
||||
|
||||
|
||||
class RiskModelAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"user",
|
||||
"name",
|
||||
"description",
|
||||
"max_loss_percent",
|
||||
"max_risk_percent",
|
||||
"max_open_trades",
|
||||
"max_open_trades_per_symbol",
|
||||
)
|
||||
|
||||
|
||||
class AssetGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "description", "webhook_id")
|
||||
|
||||
|
||||
# class AssetRestrictionAdmin(admin.ModelAdmin):
|
||||
# list_display = ("user", "name", "description", "webhook_id", "group")
|
||||
|
||||
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
admin.site.register(Plan)
|
||||
admin.site.register(Session)
|
||||
# admin.site.register(Plan)
|
||||
# admin.site.register(Session)
|
||||
|
||||
admin.site.register(Account, AccountAdmin)
|
||||
admin.site.register(Hook, HookAdmin)
|
||||
@@ -84,3 +109,7 @@ admin.site.register(Trade, TradeAdmin)
|
||||
admin.site.register(Callback, CallbackAdmin)
|
||||
admin.site.register(TradingTime, TradingTimeAdmin)
|
||||
admin.site.register(Strategy, StrategyAdmin)
|
||||
admin.site.register(NotificationSettings, NotificationSettingsAdmin)
|
||||
admin.site.register(RiskModel, RiskModelAdmin)
|
||||
admin.site.register(AssetGroup, AssetGroupAdmin)
|
||||
# admin.site.register(AssetRestriction, AssetRestrictionAdmin)
|
||||
|
||||
@@ -205,7 +205,7 @@ class BaseExchange(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_trade(self, trade_id):
|
||||
def close_trade(self, trade_id, units=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -121,7 +121,7 @@ class AlpacaExchange(BaseExchange):
|
||||
trade.save()
|
||||
return order
|
||||
|
||||
def close_trade(self, trade_id): # TODO
|
||||
def close_trade(self, trade_id, units=None): # TODO
|
||||
"""
|
||||
Close a trade
|
||||
"""
|
||||
|
||||
@@ -33,11 +33,12 @@ def get_pair(account, base, quote, invert=False):
|
||||
:param invert: Invert the pair
|
||||
:return: currency symbol, e.g. BTC_USD, BTC/USD, etc.
|
||||
"""
|
||||
# Currently we only have two exchanges with different pair separators
|
||||
if account.exchange == "alpaca":
|
||||
separator = "/"
|
||||
elif account.exchange == "oanda":
|
||||
separator = "_"
|
||||
else:
|
||||
separator = "_"
|
||||
|
||||
# Flip the pair if needed
|
||||
if invert:
|
||||
@@ -50,6 +51,16 @@ def get_pair(account, base, quote, invert=False):
|
||||
return symbol
|
||||
|
||||
|
||||
def get_symbol_price(account, price_index, symbol):
|
||||
try:
|
||||
prices = account.client.get_currencies([symbol])
|
||||
except GenericAPIError as e:
|
||||
log.error(f"Error getting currencies and inverted currencies: {e}")
|
||||
return None
|
||||
price = D(prices["prices"][0][price_index][0]["price"])
|
||||
return price
|
||||
|
||||
|
||||
def to_currency(direction, account, amount, from_currency, to_currency):
|
||||
"""
|
||||
Convert an amount from one currency to another.
|
||||
@@ -79,12 +90,7 @@ def to_currency(direction, account, amount, from_currency, to_currency):
|
||||
if not symbol:
|
||||
log.error(f"Could not find symbol for {from_currency} -> {to_currency}")
|
||||
raise Exception("Could not find symbol")
|
||||
try:
|
||||
prices = account.client.get_currencies([symbol])
|
||||
except GenericAPIError as e:
|
||||
log.error(f"Error getting currencies and inverted currencies: {e}")
|
||||
return None
|
||||
price = D(prices["prices"][0][price_index][0]["price"])
|
||||
price = get_symbol_price(account, price_index, symbol)
|
||||
|
||||
# If we had to flip base and quote, we need to use the reciprocal of the price
|
||||
if inverted:
|
||||
@@ -92,5 +98,4 @@ def to_currency(direction, account, amount, from_currency, to_currency):
|
||||
|
||||
# Convert the amount to the destination currency
|
||||
converted = D(amount) * price
|
||||
|
||||
return converted
|
||||
|
||||
@@ -79,7 +79,6 @@ def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised
|
||||
initial_price = D(current_price) - pl_per_unit
|
||||
else:
|
||||
initial_price = D(current_price) + pl_per_unit
|
||||
|
||||
# Get the percent change of the TP price from the initial price.
|
||||
change_percent = ((initial_price - D(tp_price)) / initial_price) * 100
|
||||
|
||||
@@ -103,6 +102,50 @@ def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised
|
||||
return round(change_percent, 5)
|
||||
|
||||
|
||||
def tp_percent_to_price(tp_percent, side, current_price, current_units, unrealised_pl):
|
||||
"""
|
||||
Determine the price of the TP percent from the initial price.
|
||||
Negative values for tp_percent indicate a loss.
|
||||
"""
|
||||
pl_per_unit = D(unrealised_pl) / D(current_units)
|
||||
if side == "long":
|
||||
initial_price = D(current_price) - pl_per_unit
|
||||
else:
|
||||
initial_price = D(current_price) + pl_per_unit
|
||||
|
||||
# Get the percent change of the TP price from the initial price.
|
||||
change_percent = D(tp_percent) / 100
|
||||
|
||||
# Get the price of the TP percent from the initial price.
|
||||
change_price = initial_price * abs(change_percent)
|
||||
# loss is true if tp_percent is:
|
||||
# - below initial_price for long
|
||||
# - above initial_price for short
|
||||
|
||||
if D(tp_percent) < D(0):
|
||||
loss = True
|
||||
else:
|
||||
loss = False
|
||||
|
||||
if side == "long":
|
||||
if loss:
|
||||
tp_price = D(initial_price) - change_price
|
||||
else:
|
||||
tp_price = D(initial_price) + change_price
|
||||
else:
|
||||
if loss:
|
||||
tp_price = D(initial_price) + change_price
|
||||
else:
|
||||
tp_price = D(initial_price) - change_price
|
||||
|
||||
# if side == "long":
|
||||
# tp_price = initial_price - change_price
|
||||
# else:
|
||||
# tp_price = initial_price + change_price
|
||||
|
||||
return round(tp_price, 5)
|
||||
|
||||
|
||||
def sl_price_to_percent(sl_price, side, current_price, current_units, unrealised_pl):
|
||||
"""
|
||||
Determine the percent change of the SL price from the initial price.
|
||||
@@ -146,6 +189,41 @@ def sl_price_to_percent(sl_price, side, current_price, current_units, unrealised
|
||||
return round(change_percent, 5)
|
||||
|
||||
|
||||
def sl_percent_to_price(sl_percent, side, current_price, current_units, unrealised_pl):
|
||||
"""
|
||||
Determine the price of the SL percent from the initial price.
|
||||
"""
|
||||
pl_per_unit = D(unrealised_pl) / D(current_units)
|
||||
if side == "long":
|
||||
initial_price = D(current_price) - pl_per_unit
|
||||
else:
|
||||
initial_price = D(current_price) + pl_per_unit
|
||||
|
||||
# Get the percent change of the SL price from the initial price.
|
||||
change_percent = D(sl_percent) / 100
|
||||
|
||||
# Get the price of the SL percent from the initial price.
|
||||
change_price = initial_price * abs(change_percent)
|
||||
|
||||
if D(sl_percent) < D(0):
|
||||
profit = True
|
||||
else:
|
||||
profit = False
|
||||
|
||||
if side == "long":
|
||||
if profit:
|
||||
sl_price = D(initial_price) + change_price
|
||||
else:
|
||||
sl_price = D(initial_price) - change_price
|
||||
else:
|
||||
if profit:
|
||||
sl_price = D(initial_price) - change_price
|
||||
else:
|
||||
sl_price = D(initial_price) + change_price
|
||||
|
||||
return round(sl_price, 5)
|
||||
|
||||
|
||||
def annotate_trade_tp_sl_percent(trade):
|
||||
"""
|
||||
Annotate the trade with the TP and SL percent.
|
||||
@@ -222,12 +300,16 @@ def open_trade_to_unified_format(trade):
|
||||
"id": trade["id"],
|
||||
"symbol": trade["symbol"],
|
||||
"amount": current_units,
|
||||
# For crossfilter
|
||||
"units": current_units,
|
||||
"side": side,
|
||||
"direction": side_to_direction(side),
|
||||
"state": trade["state"],
|
||||
"current_price": current_price,
|
||||
"pl": unrealised_pl,
|
||||
}
|
||||
if "openTime" in trade:
|
||||
cast["open_time"] = trade["openTime"]
|
||||
# Add some extra fields, sometimes we have already looked up the
|
||||
# prices and don't need to call convert_trades_to_usd
|
||||
# This is mostly for tests, but it can be useful in other places.
|
||||
|
||||
57
core/exchanges/fake.py
Normal file
57
core/exchanges/fake.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from core.exchanges import BaseExchange
|
||||
|
||||
|
||||
class FakeExchange(BaseExchange):
|
||||
def call_method(self, request):
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
pass
|
||||
|
||||
def get_account(self):
|
||||
pass
|
||||
|
||||
def get_instruments(self):
|
||||
pass
|
||||
|
||||
def get_currencies(self, currencies):
|
||||
pass
|
||||
|
||||
def get_supported_assets(self, response=None):
|
||||
pass
|
||||
|
||||
def get_balance(self, return_usd=False):
|
||||
pass
|
||||
|
||||
def get_market_value(self, symbol):
|
||||
pass
|
||||
|
||||
def post_trade(self, trade):
|
||||
pass
|
||||
|
||||
def close_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def update_trade(self, trade):
|
||||
pass
|
||||
|
||||
def cancel_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_position_info(self, symbol):
|
||||
pass
|
||||
|
||||
def get_all_positions(self):
|
||||
pass
|
||||
|
||||
def get_all_open_trades(self):
|
||||
pass
|
||||
|
||||
def close_position(self, side, symbol):
|
||||
pass
|
||||
|
||||
def close_all_positions(self):
|
||||
pass
|
||||
82
core/exchanges/mexc.py
Normal file
82
core/exchanges/mexc.py
Normal file
File diff suppressed because one or more lines are too long
@@ -2,6 +2,9 @@ from oandapyV20 import API
|
||||
from oandapyV20.endpoints import accounts, orders, positions, pricing, trades
|
||||
|
||||
from core.exchanges import BaseExchange, common
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("oanda")
|
||||
|
||||
|
||||
class OANDAExchange(BaseExchange):
|
||||
@@ -98,25 +101,58 @@ class OANDAExchange(BaseExchange):
|
||||
trade.save()
|
||||
return response
|
||||
|
||||
def close_trade(self, trade_id):
|
||||
def get_trade_precision(self, symbol):
|
||||
instruments = self.account.instruments
|
||||
if not instruments:
|
||||
log.error("No instruments found")
|
||||
return None
|
||||
# Extract the information for the symbol
|
||||
instrument = self.extract_instrument(instruments, symbol)
|
||||
if not instrument:
|
||||
log.error(f"Symbol not found: {symbol}")
|
||||
return None
|
||||
# Get the required precision
|
||||
try:
|
||||
trade_precision = instrument["tradeUnitsPrecision"]
|
||||
return trade_precision
|
||||
except KeyError:
|
||||
log.error(f"Precision not found for {symbol} from {instrument}")
|
||||
return None
|
||||
|
||||
def close_trade(self, trade_id, units=None, symbol=None):
|
||||
"""
|
||||
Close a trade.
|
||||
"""
|
||||
r = trades.TradeClose(accountID=self.account_id, tradeID=trade_id)
|
||||
return self.call(r)
|
||||
if not units:
|
||||
r = trades.TradeClose(accountID=self.account_id, tradeID=trade_id)
|
||||
return self.call(r)
|
||||
else:
|
||||
trade_precision = self.get_trade_precision(symbol)
|
||||
if trade_precision is None:
|
||||
log.error(f"Unable to get trade precision for {symbol}")
|
||||
return None
|
||||
units = round(units, trade_precision)
|
||||
data = {
|
||||
"units": str(units),
|
||||
}
|
||||
r = trades.TradeClose(
|
||||
accountID=self.account_id, tradeID=trade_id, data=data
|
||||
)
|
||||
return self.call(r)
|
||||
|
||||
def get_trade(self, trade_id):
|
||||
# OANDA is off by one...
|
||||
r = trades.TradeDetails(accountID=self.account_id, tradeID=trade_id)
|
||||
return self.call(r)
|
||||
|
||||
def update_trade(self, trade):
|
||||
raise NotImplementedError
|
||||
# r = orders.OrderReplace(
|
||||
# accountID=self.account_id, orderID=trade.order_id, data=data
|
||||
# )
|
||||
# self.client.request(r)
|
||||
# return r.response
|
||||
def update_trade(self, trade_id, take_profit_price, stop_loss_price):
|
||||
data = {}
|
||||
if take_profit_price:
|
||||
data["takeProfit"] = {"price": str(take_profit_price)}
|
||||
if stop_loss_price:
|
||||
data["stopLoss"] = {"price": str(stop_loss_price)}
|
||||
r = trades.TradeCRCDO(accountID=self.account_id, tradeID=trade_id, data=data)
|
||||
return self.call(r)
|
||||
|
||||
def cancel_trade(self, trade_id):
|
||||
raise NotImplementedError
|
||||
|
||||
190
core/forms.py
190
core/forms.py
@@ -4,12 +4,14 @@ from django.core.exceptions import FieldDoesNotExist
|
||||
from django.forms import ModelForm
|
||||
from mixins.restrictions import RestrictedFormMixin
|
||||
|
||||
from .models import (
|
||||
from .models import ( # AssetRestriction,
|
||||
Account,
|
||||
ActiveManagementPolicy,
|
||||
AssetGroup,
|
||||
AssetRestriction,
|
||||
AssetRule,
|
||||
Hook,
|
||||
NotificationSettings,
|
||||
OrderSettings,
|
||||
RiskModel,
|
||||
Signal,
|
||||
Strategy,
|
||||
@@ -89,10 +91,9 @@ class AccountForm(RestrictedFormMixin, ModelForm):
|
||||
"exchange",
|
||||
"api_key",
|
||||
"api_secret",
|
||||
"initial_balance",
|
||||
"sandbox",
|
||||
"enabled",
|
||||
"risk_model",
|
||||
"initial_balance",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the account. Informational only.",
|
||||
@@ -101,7 +102,6 @@ class AccountForm(RestrictedFormMixin, ModelForm):
|
||||
"api_secret": "The API secret or password/token for the account.",
|
||||
"sandbox": "Whether to use the sandbox/demo or not.",
|
||||
"enabled": "Whether the account is enabled.",
|
||||
"risk_model": "The risk model to use for this account.",
|
||||
"initial_balance": "The initial balance of the account.",
|
||||
}
|
||||
|
||||
@@ -113,44 +113,45 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
|
||||
"trend_signals": {"type": "trend"},
|
||||
}
|
||||
|
||||
# Filter for enabled accounts
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StrategyForm, self).__init__(*args, **kwargs)
|
||||
self.fields["account"].queryset = Account.objects.filter(enabled=True)
|
||||
|
||||
class Meta:
|
||||
model = Strategy
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"account",
|
||||
"asset_group",
|
||||
"risk_model",
|
||||
"trading_times",
|
||||
"order_type",
|
||||
"time_in_force",
|
||||
"order_settings",
|
||||
"entry_signals",
|
||||
"exit_signals",
|
||||
"trend_signals",
|
||||
"signal_trading_enabled",
|
||||
"active_management_enabled",
|
||||
"active_management_policy",
|
||||
"enabled",
|
||||
"take_profit_percent",
|
||||
"stop_loss_percent",
|
||||
"trailing_stop_loss_percent",
|
||||
"price_slippage_percent",
|
||||
"callback_price_deviation_percent",
|
||||
"trade_size_percent",
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
"name": "Name of the strategy. Informational only.",
|
||||
"description": "Description of the strategy. Informational only.",
|
||||
"account": "The account to use for this strategy.",
|
||||
"asset_group": "Asset groups determine which pairs can be traded.",
|
||||
"risk_model": "The risk model to use for this strategy. Highly recommended.",
|
||||
"trading_times": "When the strategy will place new trades.",
|
||||
"order_type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.",
|
||||
"time_in_force": "The time in force controls how the order is executed.",
|
||||
"order_settings": "Order settings to use for this strategy.",
|
||||
"entry_signals": "Callbacks received to these signals will trigger a trade.",
|
||||
"exit_signals": "Callbacks received to these signals will close all trades for the symbol on the account.",
|
||||
"trend_signals": "Callbacks received to these signals will limit the trading direction of the given symbol to the callback direction until further notice.",
|
||||
"signal_trading_enabled": "Whether the strategy will place trades based on signals.",
|
||||
"active_management_enabled": "Whether the strategy will amend/remove trades on the account that violate the rules.",
|
||||
"active_management_policy": "The policy to use for active management.",
|
||||
"enabled": "Whether the strategy is enabled.",
|
||||
"take_profit_percent": "The take profit will be set at this percentage above/below the entry price.",
|
||||
"stop_loss_percent": "The stop loss will be set at this percentage above/below the entry price.",
|
||||
"trailing_stop_loss_percent": "The trailing stop loss will be set at this percentage above/below the entry price. A trailing stop loss will follow the price as it moves in your favor.",
|
||||
"price_slippage_percent": "The price slippage is the maximum percent the price can move against you before the trade is cancelled. Limit orders will be set at this percentage against your favour. Market orders will have a price bound set if this is supported.",
|
||||
"callback_price_deviation_percent": "The callback price deviation is the maximum percent the price can change between receiving the callback and acting on it. This protects against rogue or delayed callbacks. Keep it low.",
|
||||
"trade_size_percent": "Percentage of the account balance to use for each trade.",
|
||||
}
|
||||
|
||||
entry_signals = forms.ModelMultipleChoiceField(
|
||||
@@ -176,9 +177,9 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super(StrategyForm, self).clean()
|
||||
entry_signals = self.cleaned_data.get("entry_signals")
|
||||
exit_signals = self.cleaned_data.get("exit_signals")
|
||||
cleaned_data = super(StrategyForm, self).clean()
|
||||
entry_signals = cleaned_data.get("entry_signals")
|
||||
exit_signals = cleaned_data.get("exit_signals")
|
||||
for entry in entry_signals.all():
|
||||
if entry in exit_signals.all():
|
||||
self._errors["entry_signals"] = self.error_class(
|
||||
@@ -215,6 +216,29 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
|
||||
"You cannot have entry and exit signals that are the same direction. At least one must be opposing."
|
||||
]
|
||||
)
|
||||
if cleaned_data.get("active_management_enabled"):
|
||||
# Ensure that no other strategy with this account has active management enabled
|
||||
if (
|
||||
Strategy.objects.filter(
|
||||
account=cleaned_data.get("account"),
|
||||
active_management_enabled=True,
|
||||
enabled=True,
|
||||
)
|
||||
.exclude(id=self.instance.id)
|
||||
.exists()
|
||||
):
|
||||
self.add_error(
|
||||
"active_management_enabled",
|
||||
"You cannot have more than one strategy with active management enabled for the same account.",
|
||||
)
|
||||
return
|
||||
if not cleaned_data.get("active_management_policy"):
|
||||
self.add_error(
|
||||
"active_management_policy",
|
||||
"You must select an active management policy if active management is enabled.",
|
||||
)
|
||||
return
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class TradeForm(RestrictedFormMixin, ModelForm):
|
||||
@@ -290,6 +314,8 @@ class RiskModelForm(RestrictedFormMixin, ModelForm):
|
||||
"max_risk_percent",
|
||||
"max_open_trades",
|
||||
"max_open_trades_per_symbol",
|
||||
"price_slippage_percent",
|
||||
"callback_price_deviation_percent",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the risk model. Informational only.",
|
||||
@@ -298,6 +324,8 @@ class RiskModelForm(RestrictedFormMixin, ModelForm):
|
||||
"max_risk_percent": "The maximum percent of the account balance that can be risked on all open trades.",
|
||||
"max_open_trades": "The maximum number of open trades.",
|
||||
"max_open_trades_per_symbol": "The maximum number of open trades per symbol.",
|
||||
"price_slippage_percent": "The price slippage is the maximum percent the price can move against you before the trade is cancelled. Limit orders will be set at this percentage against your favour. Market orders will have a price bound set if this is supported.",
|
||||
"callback_price_deviation_percent": "The callback price deviation is the maximum percent the price can change between receiving the callback and acting on it. This protects against rogue or delayed callbacks. Keep it low.",
|
||||
}
|
||||
|
||||
|
||||
@@ -307,44 +335,108 @@ class AssetGroupForm(RestrictedFormMixin, ModelForm):
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"account",
|
||||
"when_no_data",
|
||||
"when_no_match",
|
||||
"when_no_aggregation",
|
||||
"when_not_in_bounds",
|
||||
"when_bullish",
|
||||
"when_bearish",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the asset group. Informational only.",
|
||||
"description": "Description of the asset group. Informational only.",
|
||||
"account": "Account to pull assets from.",
|
||||
"when_no_data": "The action to take when no webhooks have been received for an asset.",
|
||||
"when_no_match": "The action to take when there were no matches last callback for an asset.",
|
||||
"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.",
|
||||
}
|
||||
|
||||
|
||||
class AssetRestrictionForm(RestrictedFormMixin, ModelForm):
|
||||
class AssetRuleForm(RestrictedFormMixin, ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AssetRuleForm, self).__init__(*args, **kwargs)
|
||||
self.fields["value"].disabled = True
|
||||
self.fields["original_status"].disabled = True
|
||||
self.fields["aggregation"].disabled = True
|
||||
|
||||
class Meta:
|
||||
model = AssetRestriction
|
||||
model = AssetRule
|
||||
fields = (
|
||||
"asset",
|
||||
"aggregation",
|
||||
"value",
|
||||
"original_status",
|
||||
"status",
|
||||
"trigger_below",
|
||||
"trigger_above",
|
||||
)
|
||||
|
||||
help_texts = {
|
||||
"asset": "The asset to apply the rule to.",
|
||||
"aggregation": "Aggregation of the callback",
|
||||
"value": "Value of the aggregation",
|
||||
"original_status": "The original status of the asset.",
|
||||
"status": "The status of the asset, following rules configured on the Asset Group.",
|
||||
"trigger_below": "Trigger Bearish when value is below this.",
|
||||
"trigger_above": "Trigger Bullish when value is above this.",
|
||||
}
|
||||
|
||||
|
||||
class OrderSettingsForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = OrderSettings
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"pairs",
|
||||
"order_type",
|
||||
"time_in_force",
|
||||
"take_profit_percent",
|
||||
"stop_loss_percent",
|
||||
"trailing_stop_loss_percent",
|
||||
"trade_size_percent",
|
||||
)
|
||||
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.",
|
||||
"name": "Name of the order settings. Informational only.",
|
||||
"description": "Description of the order settings. Informational only.",
|
||||
"order_type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.",
|
||||
"time_in_force": "The time in force controls how the order is executed.",
|
||||
"take_profit_percent": "The take profit will be set at this percentage above/below the entry price.",
|
||||
"stop_loss_percent": "The stop loss will be set at this percentage above/below the entry price.",
|
||||
"trailing_stop_loss_percent": "The trailing stop loss will be set at this percentage above/below the entry price. A trailing stop loss will follow the price as it moves in your favor.",
|
||||
"trade_size_percent": "Percentage of the account balance to use for each trade.",
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
return cleaned_data
|
||||
class ActiveManagementPolicyForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = ActiveManagementPolicy
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"when_trading_time_violated",
|
||||
"when_trends_violated",
|
||||
"when_position_size_violated",
|
||||
"when_protection_violated",
|
||||
"when_asset_groups_violated",
|
||||
"when_max_open_trades_violated",
|
||||
"when_max_open_trades_per_symbol_violated",
|
||||
"when_max_loss_violated",
|
||||
"when_max_risk_violated",
|
||||
"when_crossfilter_violated",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the active management policy. Informational only.",
|
||||
"description": "Description of the active management policy. Informational only.",
|
||||
"when_trading_time_violated": "The action to take when the trading time is violated.",
|
||||
"when_trends_violated": "The action to take a trade against the trend is discovered.",
|
||||
"when_position_size_violated": "The action to take when a trade exceeding the position size is discovered.",
|
||||
"when_protection_violated": "The action to take when a trade violating/lacking defined TP/SL/TSL is discovered.",
|
||||
"when_asset_groups_violated": "The action to take when a trade violating the asset group rules is discovered.",
|
||||
"when_max_open_trades_violated": "The action to take when a trade puts the account above the maximum open trades.",
|
||||
"when_max_open_trades_per_symbol_violated": "The action to take when a trade puts the account above the maximum open trades per symbol.",
|
||||
"when_max_loss_violated": "The action to take when the account exceeds its maximum loss. NOTE: The close action will close all trades.",
|
||||
"when_max_risk_violated": "The action to take when a trade exposes the account to more than the maximum risk.",
|
||||
"when_crossfilter_violated": "The action to take when a trade is deemed to conflict with another -- e.g. a buy and sell on the same asset.",
|
||||
}
|
||||
|
||||
111
core/lib/billing.py
Normal file
111
core/lib/billing.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from lago_python_client import Client
|
||||
from lago_python_client.exceptions import LagoApiError
|
||||
from lago_python_client.models import Customer, CustomerBillingConfiguration
|
||||
|
||||
client = Client(api_key=settings.LAGO_API_KEY, api_url=settings.LAGO_URL)
|
||||
|
||||
|
||||
def expand_name(first_name, last_name):
|
||||
"""
|
||||
Convert two name variables into one.
|
||||
Last name without a first name is ignored.
|
||||
:param first_name: The first name
|
||||
:param last_name: The last name
|
||||
:return: A string with the first and last name, or None if both are None
|
||||
"""
|
||||
name = None
|
||||
if first_name:
|
||||
name = first_name
|
||||
# We only want to put the last name if we have a first name
|
||||
if last_name:
|
||||
name += f" {last_name}"
|
||||
return name
|
||||
|
||||
|
||||
def get_or_create(email, first_name, last_name):
|
||||
"""
|
||||
Get a customer ID from Stripe if one with the given email exists.
|
||||
Create a customer if one does not.
|
||||
Raise an exception if two or more customers matching the given email exist.
|
||||
:param email: The email address of the customer
|
||||
:param first_name: The first name of the customer
|
||||
:param last_name: The last name of the customer
|
||||
:return: The customer ID
|
||||
"""
|
||||
# Let's see if we're just missing the ID
|
||||
matching_customers = stripe.Customer.list(email=email, limit=2)
|
||||
if len(matching_customers) == 2:
|
||||
# Something is horribly wrong
|
||||
raise Exception(f"Two customers found for email {email}")
|
||||
|
||||
elif len(matching_customers) == 1:
|
||||
# We found a customer. Let's copy the ID
|
||||
customer = matching_customers["data"][0]
|
||||
customer_id = customer["id"]
|
||||
return customer_id
|
||||
|
||||
else:
|
||||
# We didn't find anything. Create the customer
|
||||
|
||||
# Create a name, since we have 2 variables which could be null
|
||||
name = expand_name(first_name, last_name)
|
||||
cast = {"email": email}
|
||||
if name:
|
||||
cast["name"] = name
|
||||
customer = stripe.Customer.create(**cast)
|
||||
|
||||
return customer.id
|
||||
|
||||
|
||||
def update_customer_fields(user):
|
||||
"""
|
||||
Update the customer fields in Stripe.
|
||||
"""
|
||||
stripe.Customer.modify(user.stripe_id, email=user.email)
|
||||
name = expand_name(user.first_name, user.last_name)
|
||||
stripe.Customer.modify(user.stripe_id, name=name)
|
||||
|
||||
|
||||
def create_or_update_customer(user):
|
||||
"""
|
||||
Create or update a customer in Lago.
|
||||
"""
|
||||
try:
|
||||
customer = client.customers().find(str(user.customer_id))
|
||||
except LagoApiError:
|
||||
customer = None
|
||||
if not customer:
|
||||
customer = Customer(
|
||||
external_id=str(user.customer_id),
|
||||
name=f"{user.first_name} {user.last_name}",
|
||||
)
|
||||
|
||||
customer.external_id = str(user.customer_id)
|
||||
customer.email = user.email
|
||||
customer.name = f"{user.first_name} {user.last_name}"
|
||||
customer.billing_configuration = CustomerBillingConfiguration(
|
||||
payment_provider="stripe",
|
||||
provider_customer_id=str(user.stripe_id),
|
||||
)
|
||||
|
||||
try:
|
||||
created = client.customers().create(customer)
|
||||
except LagoApiError as e:
|
||||
print(e.response)
|
||||
|
||||
lago_id = created.lago_id
|
||||
|
||||
return lago_id
|
||||
|
||||
|
||||
def delete_customer(user):
|
||||
"""
|
||||
Delete a customer from Lago.
|
||||
:param user: User object to delete
|
||||
"""
|
||||
try:
|
||||
client.customers().destroy(str(user.customer_id))
|
||||
except LagoApiError:
|
||||
pass
|
||||
@@ -1,65 +0,0 @@
|
||||
import logging
|
||||
|
||||
import stripe
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def expand_name(first_name, last_name):
|
||||
"""
|
||||
Convert two name variables into one.
|
||||
Last name without a first name is ignored.
|
||||
"""
|
||||
name = None
|
||||
if first_name:
|
||||
name = first_name
|
||||
# We only want to put the last name if we have a first name
|
||||
if last_name:
|
||||
name += f" {last_name}"
|
||||
return name
|
||||
|
||||
|
||||
def get_or_create(email, first_name, last_name):
|
||||
"""
|
||||
Get a customer ID from Stripe if one with the given email exists.
|
||||
Create a customer if one does not.
|
||||
Raise an exception if two or more customers matching the given email exist.
|
||||
"""
|
||||
# Let's see if we're just missing the ID
|
||||
matching_customers = stripe.Customer.list(email=email, limit=2)
|
||||
if len(matching_customers) == 2:
|
||||
# Something is horribly wrong
|
||||
logger.error(f"Two customers found for email {email}")
|
||||
raise Exception(f"Two customers found for email {email}")
|
||||
|
||||
elif len(matching_customers) == 1:
|
||||
# We found a customer. Let's copy the ID
|
||||
customer = matching_customers["data"][0]
|
||||
customer_id = customer["id"]
|
||||
return customer_id
|
||||
|
||||
else:
|
||||
# We didn't find anything. Create the customer
|
||||
|
||||
# Create a name, since we have 2 variables which could be null
|
||||
name = expand_name(first_name, last_name)
|
||||
cast = {"email": email}
|
||||
if name:
|
||||
cast["name"] = name
|
||||
customer = stripe.Customer.create(**cast)
|
||||
logger.info(f"Created new Stripe customer {customer.id} with email {email}")
|
||||
|
||||
return customer.id
|
||||
|
||||
|
||||
def update_customer_fields(stripe_id, email=None, first_name=None, last_name=None):
|
||||
"""
|
||||
Update the customer fields in Stripe.
|
||||
"""
|
||||
if email:
|
||||
stripe.Customer.modify(stripe_id, email=email)
|
||||
logger.info(f"Modified Stripe customer {stripe_id} to have email {email}")
|
||||
if first_name or last_name:
|
||||
name = expand_name(first_name, last_name)
|
||||
stripe.Customer.modify(stripe_id, name=name)
|
||||
logger.info(f"Modified Stripe customer {stripe_id} to have email {name}")
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from elastic_transport import ConnectionError
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
from core.util import logs
|
||||
@@ -22,11 +23,16 @@ def initialise_elasticsearch():
|
||||
|
||||
|
||||
def store_msg(index, msg):
|
||||
global client
|
||||
if not client:
|
||||
client = initialise_elasticsearch()
|
||||
if "ts" not in msg:
|
||||
msg["ts"] = datetime.utcnow().isoformat()
|
||||
result = client.index(index=index, body=msg)
|
||||
if not result["result"] == "created":
|
||||
log.error(f"Indexing of '{msg}' failed: {result}")
|
||||
return
|
||||
# global client
|
||||
# if not client:
|
||||
# client = initialise_elasticsearch()
|
||||
# if "ts" not in msg:
|
||||
# msg["ts"] = datetime.utcnow().isoformat()
|
||||
# try:
|
||||
# result = client.index(index=index, body=msg)
|
||||
# except ConnectionError as e:
|
||||
# log.error(f"Error indexing '{msg}': {e}")
|
||||
# return
|
||||
# if not result["result"] == "created":
|
||||
# log.error(f"Indexing of '{msg}' failed: {result}")
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
from asgiref.sync import sync_to_async
|
||||
# from asgiref.sync import sync_to_async
|
||||
|
||||
from core.models import Plan
|
||||
# from core.models import Plan
|
||||
|
||||
|
||||
async def assemble_plan_map(product_id_filter=None):
|
||||
"""
|
||||
Get all the plans from the database and create an object Stripe wants.
|
||||
"""
|
||||
line_items = []
|
||||
for plan in await sync_to_async(list)(Plan.objects.all()):
|
||||
if product_id_filter:
|
||||
if plan.product_id != product_id_filter:
|
||||
continue
|
||||
line_items.append(
|
||||
{
|
||||
"price": plan.product_id,
|
||||
"quantity": 1,
|
||||
}
|
||||
)
|
||||
return line_items
|
||||
# async def assemble_plan_map(product_id_filter=None):
|
||||
# """
|
||||
# Get all the plans from the database and create an object Stripe wants.
|
||||
# """
|
||||
# line_items = []
|
||||
# for plan in await sync_to_async(list)(Plan.objects.all()):
|
||||
# if product_id_filter:
|
||||
# if plan.product_id != product_id_filter:
|
||||
# continue
|
||||
# line_items.append(
|
||||
# {
|
||||
# "price": plan.product_id,
|
||||
# "quantity": 1,
|
||||
# }
|
||||
# )
|
||||
# return line_items
|
||||
|
||||
@@ -1 +1 @@
|
||||
from core.lib.schemas import alpaca_s, drakdoo_s, oanda_s # noqa
|
||||
from core.lib.schemas import alpaca_s, drakdoo_s, oanda_s, mexc_s # noqa
|
||||
|
||||
1
core/lib/schemas/mexc_s.py
Normal file
1
core/lib/schemas/mexc_s.py
Normal file
@@ -0,0 +1 @@
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -1,29 +1,30 @@
|
||||
from decimal import Decimal as D
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PositionLong(BaseModel):
|
||||
units: str
|
||||
averagePrice: str | None
|
||||
averagePrice: Optional[str] = None
|
||||
pl: str
|
||||
resettablePL: str
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
tradeIDs: list[str] | None
|
||||
tradeIDs: Optional[list[str]] = []
|
||||
unrealizedPL: str
|
||||
|
||||
|
||||
class PositionShort(BaseModel):
|
||||
units: str
|
||||
averagePrice: str | None
|
||||
averagePrice: Optional[str] = None
|
||||
pl: str
|
||||
resettablePL: str
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
tradeIDs: list[str] | None
|
||||
tradeIDs: Optional[list[str]] = []
|
||||
unrealizedPL: str
|
||||
|
||||
|
||||
@@ -46,6 +47,20 @@ class OpenPositions(BaseModel):
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
def parse_time(x):
|
||||
"""
|
||||
Parse the time from the Oanda API.
|
||||
"""
|
||||
if "openTime" in x:
|
||||
ts_split = x["openTime"].split(".")
|
||||
else:
|
||||
ts_split = x["trade"]["openTime"].split(".")
|
||||
microseconds = ts_split[1].replace("Z", "")
|
||||
microseconds_6 = microseconds[:6]
|
||||
new_ts = ts_split[0] + "." + microseconds_6 + "Z"
|
||||
return new_ts
|
||||
|
||||
|
||||
def prevent_hedging(x):
|
||||
"""
|
||||
Our implementation breaks if a position has both.
|
||||
@@ -292,7 +307,7 @@ class PositionDetailsNested(BaseModel):
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
unrealizedPL: str
|
||||
marginUsed: str | None
|
||||
marginUsed: Optional[str] = None
|
||||
|
||||
|
||||
class PositionDetails(BaseModel):
|
||||
@@ -359,7 +374,9 @@ class Instrument(BaseModel):
|
||||
guaranteedStopLossOrderMode: str
|
||||
tags: list[InstrumentTag]
|
||||
financing: InstrumentFinancing
|
||||
guaranteedStopLossOrderLevelRestriction: InstrumentGuaranteedRestriction | None
|
||||
guaranteedStopLossOrderLevelRestriction: Optional[
|
||||
InstrumentGuaranteedRestriction
|
||||
] = None
|
||||
|
||||
|
||||
class AccountInstruments(BaseModel):
|
||||
@@ -460,33 +477,33 @@ class Trade(BaseModel):
|
||||
quoteGuaranteedExecutionFee: str
|
||||
halfSpreadCost: str
|
||||
# takeProfitOrder: TakeProfitOrder | None
|
||||
takeProfitOrder: dict | None
|
||||
stopLossOrder: dict | None
|
||||
trailingStopLossOrder: dict | None
|
||||
takeProfitOrder: Optional[dict] = None
|
||||
stopLossOrder: Optional[dict] = None
|
||||
trailingStopLossOrder: Optional[dict] = None
|
||||
|
||||
|
||||
class SideCarOrder(BaseModel):
|
||||
id: str
|
||||
createTime: str
|
||||
state: str
|
||||
price: str | None
|
||||
price: Optional[str] = None
|
||||
timeInForce: str
|
||||
gtdTime: str | None
|
||||
clientExtensions: dict | None
|
||||
gtdTime: Optional[str] = None
|
||||
clientExtensions: Optional[dict] = None
|
||||
tradeID: str
|
||||
clientTradeID: str | None
|
||||
clientTradeID: Optional[str] = None
|
||||
type: str
|
||||
time: str | None
|
||||
priceBound: str | None
|
||||
positionFill: str | None
|
||||
reason: str | None
|
||||
orderFillTransactionID: str | None
|
||||
tradeOpenedID: str | None
|
||||
tradeReducedID: str | None
|
||||
tradeClosedIDs: list[str] | None
|
||||
cancellingTransactionID: str | None
|
||||
replacesOrderID: str | None
|
||||
replacedByOrderID: str | None
|
||||
time: Optional[str] = None
|
||||
priceBound: Optional[str] = None
|
||||
positionFill: Optional[str] = None
|
||||
reason: Optional[str] = None
|
||||
orderFillTransactionID: Optional[str] = None
|
||||
tradeOpenedID: Optional[str] = None
|
||||
tradeReducedID: Optional[str] = None
|
||||
tradeClosedIDs: Optional[list[str]] = []
|
||||
cancellingTransactionID: Optional[str] = None
|
||||
replacesOrderID: Optional[str] = None
|
||||
replacedByOrderID: Optional[str] = None
|
||||
|
||||
|
||||
class OpenTradesTrade(BaseModel):
|
||||
@@ -503,10 +520,10 @@ class OpenTradesTrade(BaseModel):
|
||||
dividendAdjustment: str
|
||||
unrealizedPL: str
|
||||
marginUsed: str
|
||||
takeProfitOrder: SideCarOrder | None
|
||||
stopLossOrder: SideCarOrder | None
|
||||
trailingStopLossOrder: SideCarOrder | None
|
||||
trailingStopValue: dict | None
|
||||
takeProfitOrder: Optional[SideCarOrder] = None
|
||||
stopLossOrder: Optional[SideCarOrder] = None
|
||||
trailingStopLossOrder: Optional[SideCarOrder] = None
|
||||
trailingStopValue: Optional[dict] = None
|
||||
|
||||
|
||||
class OpenTrades(BaseModel):
|
||||
@@ -522,7 +539,7 @@ OpenTradesSchema = {
|
||||
"id": "id",
|
||||
"symbol": "instrument",
|
||||
"price": "price",
|
||||
"openTime": "openTime",
|
||||
"openTime": parse_time,
|
||||
"initialUnits": "initialUnits",
|
||||
"initialMarginRequired": "initialMarginRequired",
|
||||
"state": "state",
|
||||
@@ -564,13 +581,13 @@ class OrderTransaction(BaseModel):
|
||||
requestID: str
|
||||
time: str
|
||||
type: str
|
||||
instrument: str | None
|
||||
units: str | None
|
||||
timeInForce: str | None
|
||||
positionFill: str | None
|
||||
instrument: Optional[str] = None
|
||||
units: Optional[str] = None
|
||||
timeInForce: Optional[str] = None
|
||||
positionFill: Optional[str] = None
|
||||
reason: str
|
||||
longPositionCloseout: LongPositionCloseout | None
|
||||
longOrderFillTransaction: dict | None
|
||||
longOrderFillTransaction: Optional[dict] = None
|
||||
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
@@ -663,12 +680,12 @@ class TradeDetailsTrade(BaseModel):
|
||||
state: str
|
||||
currentUnits: str
|
||||
realizedPL: str
|
||||
closingTransactionIDs: list[str] | None
|
||||
closingTransactionIDs: Optional[list[str]] = []
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
closeTime: str | None
|
||||
averageClosePrice: str | None
|
||||
clientExtensions: ClientExtensions | None
|
||||
closeTime: Optional[str] = None
|
||||
averageClosePrice: Optional[str] = None
|
||||
clientExtensions: Optional[ClientExtensions] = None
|
||||
|
||||
|
||||
class TradeDetails(BaseModel):
|
||||
@@ -680,7 +697,7 @@ TradeDetailsSchema = {
|
||||
"id": "trade.id",
|
||||
"symbol": "trade.instrument",
|
||||
"price": "trade.price",
|
||||
"openTime": "trade.openTime",
|
||||
"openTime": parse_time,
|
||||
"initialUnits": "trade.initialUnits",
|
||||
"initialMarginRequired": "trade.initialMarginRequired",
|
||||
"state": "trade.state",
|
||||
@@ -716,3 +733,22 @@ TradeCloseSchema = {
|
||||
"longPositionCloseout": "orderCreateTransaction.longPositionCloseout",
|
||||
"longOrderFillTransaction": "orderCreateTransaction.longOrderFillTransaction",
|
||||
}
|
||||
|
||||
|
||||
class TradeCRCDO(BaseModel):
|
||||
takeProfitOrderCancelTransaction: Optional[OrderTransaction]
|
||||
takeProfitOrderTransaction: Optional[OrderTransaction]
|
||||
stopLossOrderCancelTransaction: Optional[OrderTransaction]
|
||||
stopLossOrderTransaction: Optional[OrderTransaction]
|
||||
relatedTransactionIDs: list[str]
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
TradeCRCDOSchema = {
|
||||
"takeProfitOrderCancelTransaction": "takeProfitOrderCancelTransaction",
|
||||
"takeProfitOrderTransaction": "takeProfitOrderTransaction",
|
||||
"stopLossOrderCancelTransaction": "stopLossOrderCancelTransaction",
|
||||
"stopLossOrderTransaction": "stopLossOrderTransaction",
|
||||
"relatedTransactionIDs": "relatedTransactionIDs",
|
||||
"lastTransactionID": "lastTransactionID",
|
||||
}
|
||||
|
||||
52
core/management/commands/scheduling.py
Normal file
52
core/management/commands/scheduling.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import asyncio
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import Strategy
|
||||
from core.trading import active_management
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("scheduling")
|
||||
|
||||
INTERVAL = 5
|
||||
|
||||
|
||||
async def job():
|
||||
"""
|
||||
Run all schedules matching the given interval.
|
||||
:param interval_seconds: The interval to run.
|
||||
"""
|
||||
strategies = await sync_to_async(list)(
|
||||
Strategy.objects.filter(enabled=True, active_management_enabled=True)
|
||||
)
|
||||
log.debug(f"Found {len(strategies)} strategies")
|
||||
for strategy in strategies:
|
||||
log.debug(f"Running strategy {strategy.name}")
|
||||
ams = active_management.ActiveManagement(strategy) # noqa
|
||||
ams.run_checks()
|
||||
ams.execute_actions()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Start the scheduling process.
|
||||
"""
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
log.debug(f"Scheduling checking process job every {INTERVAL} seconds")
|
||||
scheduler.add_job(job, "interval", seconds=INTERVAL)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
scheduler._eventloop = loop
|
||||
scheduler.start()
|
||||
try:
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
log.info("Process terminating")
|
||||
finally:
|
||||
scheduler.shutdown(wait=False)
|
||||
loop.close()
|
||||
@@ -30,8 +30,7 @@ class Migration(migrations.Migration):
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('stripe_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('last_payment', models.DateTimeField(blank=True, null=True)),
|
||||
('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
],
|
||||
@@ -44,32 +43,6 @@ class Migration(migrations.Migration):
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Plan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('cost', models.IntegerField()),
|
||||
('product_id', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('image', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('subscription_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.plan')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='plans',
|
||||
field=models.ManyToManyField(blank=True, to='core.plan'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Generated by Django 4.1.7 on 2023-02-24 13:18
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('exchange', models.CharField(choices=[('alpaca', 'Alpaca'), ('oanda', 'OANDA'), ('fake', 'Fake')], max_length=255)),
|
||||
('api_key', models.CharField(max_length=255)),
|
||||
('api_secret', models.CharField(max_length=255)),
|
||||
('sandbox', models.BooleanField(default=False)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('supported_symbols', models.JSONField(default=list)),
|
||||
('instruments', models.JSONField(default=list)),
|
||||
('currency', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('initial_balance', models.FloatField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ActiveManagementPolicy',
|
||||
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)),
|
||||
('when_trading_time_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_trends_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_position_size_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only'), ('adjust', 'Adjust violating trades')], default='none', max_length=255)),
|
||||
('when_protection_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only'), ('adjust', 'Adjust violating trades')], default='none', max_length=255)),
|
||||
('when_asset_groups_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_open_trades_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_open_trades_per_symbol_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_loss_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_risk_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_crossfilter_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetGroup',
|
||||
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)),
|
||||
('webhook_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('when_no_data', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=7)),
|
||||
('when_no_match', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
|
||||
('when_no_aggregation', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
|
||||
('when_not_in_bounds', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
|
||||
('when_bullish', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=2)),
|
||||
('when_bearish', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=3)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Hook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=1024)),
|
||||
('hook', models.CharField(max_length=255, unique=True)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderSettings',
|
||||
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)),
|
||||
('order_type', models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], default='market', max_length=255)),
|
||||
('time_in_force', models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255)),
|
||||
('take_profit_percent', models.FloatField(default=1.5)),
|
||||
('stop_loss_percent', models.FloatField(default=1.0)),
|
||||
('trailing_stop_loss_percent', models.FloatField(blank=True, default=1.0, null=True)),
|
||||
('trade_size_percent', models.FloatField(default=0.5)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RiskModel',
|
||||
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)),
|
||||
('max_loss_percent', models.FloatField(default=0.05)),
|
||||
('max_risk_percent', models.FloatField(default=0.05)),
|
||||
('max_open_trades', models.IntegerField(default=10)),
|
||||
('max_open_trades_per_symbol', models.IntegerField(default=2)),
|
||||
('price_slippage_percent', models.FloatField(default=2.5)),
|
||||
('callback_price_deviation_percent', models.FloatField(default=0.5)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Signal',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=1024)),
|
||||
('signal', models.CharField(max_length=256)),
|
||||
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('type', models.CharField(choices=[('entry', 'Entry'), ('exit', 'Exit'), ('trend', 'Trend')], max_length=255)),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TradingTime',
|
||||
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)),
|
||||
('start_day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')])),
|
||||
('end_day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')])),
|
||||
('start_time', models.TimeField()),
|
||||
('end_time', models.TimeField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Trade',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('symbol', models.CharField(max_length=255)),
|
||||
('time_in_force', models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255)),
|
||||
('type', models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], max_length=255)),
|
||||
('amount', models.FloatField(blank=True, null=True)),
|
||||
('amount_usd', models.FloatField(blank=True, null=True)),
|
||||
('price', models.FloatField(blank=True, null=True)),
|
||||
('stop_loss', models.FloatField(blank=True, null=True)),
|
||||
('trailing_stop_loss', models.FloatField(blank=True, null=True)),
|
||||
('take_profit', models.FloatField(blank=True, null=True)),
|
||||
('status', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('information', models.JSONField(blank=True, null=True)),
|
||||
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
|
||||
('order_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('client_order_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('response', models.JSONField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('signal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.signal')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
('signal_trading_enabled', models.BooleanField(default=False)),
|
||||
('active_management_enabled', models.BooleanField(default=False)),
|
||||
('trends', models.JSONField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('active_management_policy', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.activemanagementpolicy')),
|
||||
('asset_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.assetgroup')),
|
||||
('entry_signals', models.ManyToManyField(blank=True, related_name='entry_strategies', to='core.signal')),
|
||||
('exit_signals', models.ManyToManyField(blank=True, related_name='exit_strategies', to='core.signal')),
|
||||
('order_settings', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.ordersettings')),
|
||||
('risk_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.riskmodel')),
|
||||
('trading_times', models.ManyToManyField(to='core.tradingtime')),
|
||||
('trend_signals', models.ManyToManyField(blank=True, related_name='trend_strategies', to='core.signal')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'strategies',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Callback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('message', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('period', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('sent', models.BigIntegerField(blank=True, null=True)),
|
||||
('trade', models.BigIntegerField(blank=True, null=True)),
|
||||
('exchange', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('base', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('quote', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('contract', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('price', models.FloatField(blank=True, null=True)),
|
||||
('symbol', models.CharField(max_length=255)),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('signal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.signal')),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
('original_status', models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'Always allow'), (7, 'Always deny')], default=0)),
|
||||
('status', models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'Always allow'), (7, 'Always deny')], 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)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('asset', 'group')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-14 23:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_session_session'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Hook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('hook', models.CharField(max_length=255)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
20
core/migrations/0003_user_customer_id.py
Normal file
20
core/migrations/0003_user_customer_id.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.7 on 2023-02-24 13:21
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_account_activemanagementpolicy_assetgroup_hook_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='customer_id',
|
||||
field=models.UUIDField(blank=True, default=uuid.uuid4, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-15 18:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_hook'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Callback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('data', models.JSONField()),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.0.6 on 2022-10-12 09:08
|
||||
# Generated by Django 4.1.7 on 2023-02-24 16:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
('core', '0003_user_customer_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='session',
|
||||
model_name='user',
|
||||
name='stripe_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,77 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-15 22:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_callback'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='callback',
|
||||
name='data',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_contract',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_currency',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_exchange',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_item',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='message',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='period',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='timestamp_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='timestamp_trade',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='hook',
|
||||
name='hook',
|
||||
field=models.CharField(max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='hook',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-16 13:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_remove_callback_data_callback_market_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='callback',
|
||||
name='market',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='callback',
|
||||
name='timestamp_sent',
|
||||
field=models.BigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='callback',
|
||||
name='timestamp_trade',
|
||||
field=models.BigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 17:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_remove_callback_market_alter_callback_timestamp_sent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('exchange', models.CharField(max_length=255)),
|
||||
('api_key', models.CharField(max_length=255)),
|
||||
('api_secret', models.CharField(max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 17:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_account'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Trade',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('symbol', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=255)),
|
||||
('amount', models.FloatField()),
|
||||
('price', models.FloatField()),
|
||||
('stop_loss', models.FloatField(blank=True, null=True)),
|
||||
('take_profit', models.FloatField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 18:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_trade'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='exchange_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 18:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_trade_exchange_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='sandbox',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='direction',
|
||||
field=models.CharField(blank=True, choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='symbol',
|
||||
field=models.CharField(choices=[('BTCUSD', 'Bitcoin/USD')], max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 08:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_account_sandbox_trade_direction_trade_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='exchange',
|
||||
field=models.CharField(choices=[('binance', 'Binance'), ('alpaca', 'Alpaca')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 13:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_alter_account_exchange'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='trade',
|
||||
old_name='exchange_id',
|
||||
new_name='client_order_id',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='order_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='response',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='symbol',
|
||||
field=models.CharField(choices=[('BTC/USD', 'Bitcoin/US Dollar'), ('LTC/USD', 'Litecoin/US Dollar')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 13:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_rename_exchange_id_trade_client_order_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='direction',
|
||||
field=models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], default='buy', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='price',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-21 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_alter_trade_direction_alter_trade_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='exchange',
|
||||
field=models.CharField(choices=[('alpaca', 'Alpaca')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
# 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,
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-10 18:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_rename_market_item_callback_base_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='instruments',
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='exchange',
|
||||
field=models.CharField(choices=[('alpaca', 'Alpaca'), ('oanda', 'OANDA')], max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='take_profit_percent',
|
||||
field=models.FloatField(default=1.5),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='trade_size_percent',
|
||||
field=models.FloatField(default=0.5),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-10 18:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_account_instruments_alter_account_exchange_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='currency',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-15 15:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_account_currency'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='strategy',
|
||||
options={'verbose_name_plural': 'strategies'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='callback_price_deviation_percent',
|
||||
field=models.FloatField(default=0.5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='order_type',
|
||||
field=models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], default='market', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-15 15:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0023_alter_strategy_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='time_in_force',
|
||||
field=models.CharField(choices=[('gtc', 'Good Til Cancelled'), ('gfd', 'Good For Day'), ('fok', 'Fill Or Kill'), ('ioc', 'Immediate Or Cancel')], default='gtc', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-15 15:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0024_strategy_time_in_force'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='time_in_force',
|
||||
field=models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-15 15:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0025_alter_strategy_time_in_force'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='time_in_force',
|
||||
field=models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-15 15:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0026_trade_time_in_force'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='trailing_stop_loss_percent',
|
||||
field=models.FloatField(blank=True, default=1.0, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='trailing_stop_loss',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 17:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0027_strategy_trailing_stop_loss_percent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TradingTime',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('start_ts', models.DateTimeField()),
|
||||
('end_ts', models.DateTimeField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 17:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0028_tradingtime'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='name',
|
||||
field=models.CharField(default='DEFAULT', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,45 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 17:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0029_alter_tradingtime_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tradingtime',
|
||||
name='end_ts',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tradingtime',
|
||||
name='start_ts',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tradingtime',
|
||||
name='end_day',
|
||||
field=models.CharField(choices=[('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday')], default='monday', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tradingtime',
|
||||
name='end_time',
|
||||
field=models.TimeField(default='12:00'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tradingtime',
|
||||
name='start_day',
|
||||
field=models.CharField(choices=[('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday')], default='monday', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tradingtime',
|
||||
name='start_time',
|
||||
field=models.TimeField(default='12:00'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 18:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0030_remove_tradingtime_end_ts_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='trading_times',
|
||||
field=models.ManyToManyField(blank=True, to='core.tradingtime'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 18:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0031_strategy_trading_times'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='end_day',
|
||||
field=models.CharField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='start_day',
|
||||
field=models.CharField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 18:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0032_alter_tradingtime_end_day_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='end_day',
|
||||
field=models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='start_day',
|
||||
field=models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 18:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0033_alter_tradingtime_end_day_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='trading_times',
|
||||
field=models.ManyToManyField(to='core.tradingtime'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-25 19:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0034_alter_strategy_trading_times'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='end_day',
|
||||
field=models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tradingtime',
|
||||
name='start_day',
|
||||
field=models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')]),
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 18:22
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0035_alter_tradingtime_end_day_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='hook',
|
||||
name='direction',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='hook',
|
||||
name='name',
|
||||
field=models.CharField(default='Unknown', max_length=1024),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Signal',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=1024)),
|
||||
('signal', models.CharField(max_length=256)),
|
||||
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 18:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0036_remove_hook_direction_alter_hook_name_signal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='signal',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.signal'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 18:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0037_callback_signal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='strategy',
|
||||
name='hooks',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='entry_signals',
|
||||
field=models.ManyToManyField(related_name='entry_strategies', to='core.signal'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='exit_signals',
|
||||
field=models.ManyToManyField(related_name='exit_signals', to='core.signal'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='signal',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.signal'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 18:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0038_remove_strategy_hooks_strategy_entry_signals_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='exit_signals',
|
||||
field=models.ManyToManyField(related_name='exit_strategies', to='core.signal'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 18:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0039_alter_strategy_exit_signals'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='entry_signals',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='entry_strategies', to='core.signal'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='exit_signals',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='exit_strategies', to='core.signal'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 18:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0040_alter_strategy_entry_signals_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='entry_signals',
|
||||
field=models.ManyToManyField(blank=True, related_name='entry_strategies', to='core.signal'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='exit_signals',
|
||||
field=models.ManyToManyField(blank=True, related_name='exit_strategies', to='core.signal'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-01 19:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0041_alter_strategy_entry_signals_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='information',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-06 19:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0042_trade_information'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='trend_signals',
|
||||
field=models.ManyToManyField(blank=True, related_name='trend_strategies', to='core.signal'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-06 19:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0043_strategy_trend_signals'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='trends',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-07 09:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0044_strategy_trends'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hook',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('entry', 'Entry'), ('exit', 'Exit'), ('trend', 'Trend')], default='entry', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-07 10:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0045_hook_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='hook',
|
||||
name='type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='signal',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('entry', 'Entry'), ('exit', 'Exit'), ('trend', 'Trend')], default='entry', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.1.3 on 2022-12-18 17:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0046_remove_hook_type_signal_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NotificationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-21 21:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0047_notificationsettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RiskModel',
|
||||
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)),
|
||||
('max_loss_percent', models.FloatField(default=0.05)),
|
||||
('max_risk_percent', models.FloatField(default=0.05)),
|
||||
('max_open_trades', models.IntegerField(default=10)),
|
||||
('max_open_trades_per_symbol', models.IntegerField(default=2)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='riskmodel',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.riskmodel'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-21 21:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0048_riskmodel_account_riskmodel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='account',
|
||||
old_name='riskmodel',
|
||||
new_name='risk_model',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2023-01-01 15:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0049_rename_riskmodel_account_risk_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='enabled',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2023-01-11 17:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0050_account_enabled'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='initial_balance',
|
||||
field=models.FloatField(default=0),
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2023-02-10 13:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0051_account_initial_balance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AssetGroup',
|
||||
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)),
|
||||
('allowed', models.JSONField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetRestriction',
|
||||
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)),
|
||||
('pairs', models.CharField(blank=True, max_length=4096, null=True)),
|
||||
('group', models.ForeignKey(blank=True, null=True, 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 4.1.4 on 2023-02-10 13:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0052_assetgroup_assetrestriction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='assetrestriction',
|
||||
name='pairs_parsed',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='assetgroup',
|
||||
name='allowed',
|
||||
field=models.JSONField(blank=True, default={}, null=True),
|
||||
),
|
||||
]
|
||||
287
core/models.py
287
core/models.py
@@ -1,3 +1,4 @@
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
import stripe
|
||||
@@ -6,12 +7,21 @@ from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from core.exchanges.alpaca import AlpacaExchange
|
||||
from core.exchanges.fake import FakeExchange
|
||||
from core.exchanges.mexc import MEXCExchange
|
||||
from core.exchanges.oanda import OANDAExchange
|
||||
from core.lib.customers import get_or_create, update_customer_fields
|
||||
|
||||
# from core.lib.customers import get_or_create, update_customer_fields
|
||||
from core.lib import billing
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange}
|
||||
EXCHANGE_MAP = {
|
||||
"alpaca": AlpacaExchange,
|
||||
"oanda": OANDAExchange,
|
||||
"mexc": MEXCExchange,
|
||||
"fake": FakeExchange,
|
||||
}
|
||||
TYPE_CHOICES = (
|
||||
("market", "Market"),
|
||||
("limit", "Limit"),
|
||||
@@ -40,69 +50,109 @@ SIGNAL_TYPE_CHOICES = (
|
||||
("exit", "Exit"),
|
||||
("trend", "Trend"),
|
||||
)
|
||||
AGGREGATION_CHOICES = (
|
||||
("none", "None"),
|
||||
("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"),
|
||||
)
|
||||
|
||||
class Plan(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
description = models.CharField(max_length=1024, null=True, blank=True)
|
||||
cost = models.IntegerField()
|
||||
product_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
|
||||
image = models.CharField(max_length=1024, null=True, blank=True)
|
||||
MAPPING_CHOICES = (
|
||||
(6, "Always allow"),
|
||||
(7, "Always deny"),
|
||||
(2, "Bullish"),
|
||||
(3, "Bearish"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} (£{self.cost})"
|
||||
CLOSE_NOTIFY_CHOICES = (
|
||||
("none", "None"),
|
||||
("close", "Close violating trades"),
|
||||
("notify", "Notify only"),
|
||||
)
|
||||
|
||||
ADJUST_CLOSE_NOTIFY_CHOICES = (
|
||||
("none", "None"),
|
||||
("close", "Close violating trades"),
|
||||
("notify", "Notify only"),
|
||||
("adjust", "Adjust violating trades"),
|
||||
)
|
||||
|
||||
ADJUST_WITH_DIRECTION_CHOICES = (
|
||||
("none", "None"),
|
||||
("close", "Close violating trades"),
|
||||
("notify", "Notify only"),
|
||||
("adjust", "Increase and reduce"),
|
||||
("adjust_up", "Increase only"),
|
||||
("adjust_down", "Reduce only"),
|
||||
)
|
||||
|
||||
# class Plan(models.Model):
|
||||
# name = models.CharField(max_length=255, unique=True)
|
||||
# description = models.CharField(max_length=1024, null=True, blank=True)
|
||||
# cost = models.IntegerField()
|
||||
# product_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
|
||||
# image = models.CharField(max_length=1024, null=True, blank=True)
|
||||
|
||||
# def __str__(self):
|
||||
# return f"{self.name} (£{self.cost})"
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
# Stripe customer ID
|
||||
stripe_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
last_payment = models.DateTimeField(null=True, blank=True)
|
||||
plans = models.ManyToManyField(Plan, blank=True)
|
||||
customer_id = models.UUIDField(default=uuid.uuid4, null=True, blank=True)
|
||||
billing_provider_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
# last_payment = models.DateTimeField(null=True, blank=True)
|
||||
# plans = models.ManyToManyField(Plan, blank=True)
|
||||
email = models.EmailField(unique=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._original = self
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Override the save function to create a Stripe customer.
|
||||
"""
|
||||
if settings.STRIPE_ENABLED:
|
||||
if not self.stripe_id: # stripe ID not stored
|
||||
self.stripe_id = get_or_create(
|
||||
self.email, self.first_name, self.last_name
|
||||
)
|
||||
|
||||
to_update = {}
|
||||
if self.email != self._original.email:
|
||||
to_update["email"] = self.email
|
||||
if self.first_name != self._original.first_name:
|
||||
to_update["first_name"] = self.first_name
|
||||
if self.last_name != self._original.last_name:
|
||||
to_update["last_name"] = self.last_name
|
||||
|
||||
update_customer_fields(self.stripe_id, **to_update)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if settings.STRIPE_ENABLED:
|
||||
if settings.BILLING_ENABLED:
|
||||
if self.stripe_id:
|
||||
stripe.Customer.delete(self.stripe_id)
|
||||
log.info(f"Deleted Stripe customer {self.stripe_id}")
|
||||
if self.billing_provider_id:
|
||||
billing.delete_customer(self)
|
||||
log.info(f"Deleted Billing customer {self.billing_provider_id}")
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def has_plan(self, plan):
|
||||
plan_list = [plan.name for plan in self.plans.all()]
|
||||
return plan in plan_list
|
||||
# Override save to update attributes in Lago
|
||||
def save(self, *args, **kwargs):
|
||||
if self.customer_id is None:
|
||||
self.customer_id = uuid.uuid4()
|
||||
|
||||
if settings.BILLING_ENABLED:
|
||||
if not self.stripe_id: # stripe ID not stored
|
||||
self.stripe_id = billing.get_or_create(
|
||||
self.email, self.first_name, self.last_name
|
||||
)
|
||||
if not self.billing_provider_id:
|
||||
self.billing_provider_id = billing.create_or_update_customer(self)
|
||||
|
||||
billing.update_customer_fields(self)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_notification_settings(self):
|
||||
return NotificationSettings.objects.get_or_create(user=self)[0]
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
EXCHANGE_CHOICES = (("alpaca", "Alpaca"), ("oanda", "OANDA"))
|
||||
EXCHANGE_CHOICES = (
|
||||
("alpaca", "Alpaca"),
|
||||
("oanda", "OANDA"),
|
||||
("mexc", "MEXC"),
|
||||
("fake", "Fake"),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
exchange = models.CharField(choices=EXCHANGE_CHOICES, max_length=255)
|
||||
@@ -113,9 +163,6 @@ class Account(models.Model):
|
||||
supported_symbols = models.JSONField(default=list)
|
||||
instruments = models.JSONField(default=list)
|
||||
currency = models.CharField(max_length=255, null=True, blank=True)
|
||||
risk_model = models.ForeignKey(
|
||||
"core.RiskModel", on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
initial_balance = models.FloatField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
@@ -129,11 +176,13 @@ class Account(models.Model):
|
||||
if client:
|
||||
response = client.get_instruments()
|
||||
supported_symbols = client.get_supported_assets(response)
|
||||
currency = client.get_account()["currency"]
|
||||
acct_info = client.get_account()
|
||||
log.debug(f"Supported symbols for {self.name}: {supported_symbols}")
|
||||
self.supported_symbols = supported_symbols
|
||||
self.instruments = response
|
||||
self.currency = currency
|
||||
if "currency" in acct_info.keys():
|
||||
currency = acct_info["currency"]
|
||||
self.currency = currency
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
@@ -141,7 +190,8 @@ class Account(models.Model):
|
||||
"""
|
||||
Override the save function to update supported symbols.
|
||||
"""
|
||||
self.update_info(save=False)
|
||||
if self.exchange != "fake":
|
||||
self.update_info(save=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_client(self):
|
||||
@@ -173,14 +223,6 @@ class Account(models.Model):
|
||||
return cls.objects.get(id=account_id)
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
request = models.CharField(max_length=255, null=True, blank=True)
|
||||
session = models.CharField(max_length=255, null=True, blank=True)
|
||||
subscription_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Hook(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=1024)
|
||||
@@ -346,10 +388,6 @@ class Strategy(models.Model):
|
||||
description = models.TextField(null=True, blank=True)
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
trading_times = models.ManyToManyField(TradingTime)
|
||||
order_type = models.CharField(
|
||||
choices=TYPE_CHOICES, max_length=255, default="market"
|
||||
)
|
||||
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
|
||||
entry_signals = models.ManyToManyField(
|
||||
Signal, related_name="entry_strategies", blank=True
|
||||
)
|
||||
@@ -360,14 +398,27 @@ class Strategy(models.Model):
|
||||
Signal, related_name="trend_strategies", blank=True
|
||||
)
|
||||
enabled = models.BooleanField(default=False)
|
||||
take_profit_percent = models.FloatField(default=1.5)
|
||||
stop_loss_percent = models.FloatField(default=1.0)
|
||||
trailing_stop_loss_percent = models.FloatField(default=1.0, null=True, blank=True)
|
||||
price_slippage_percent = models.FloatField(default=2.5)
|
||||
callback_price_deviation_percent = models.FloatField(default=0.5)
|
||||
trade_size_percent = models.FloatField(default=0.5)
|
||||
signal_trading_enabled = models.BooleanField(default=False)
|
||||
active_management_enabled = models.BooleanField(default=False)
|
||||
trends = models.JSONField(null=True, blank=True)
|
||||
|
||||
asset_group = models.ForeignKey(
|
||||
"core.AssetGroup", on_delete=models.PROTECT, null=True, blank=True
|
||||
)
|
||||
risk_model = models.ForeignKey(
|
||||
"core.RiskModel", on_delete=models.PROTECT, null=True, blank=True
|
||||
)
|
||||
order_settings = models.ForeignKey(
|
||||
"core.OrderSettings",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
active_management_policy = models.ForeignKey(
|
||||
"core.ActiveManagementPolicy",
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "strategies"
|
||||
|
||||
@@ -397,6 +448,9 @@ class RiskModel(models.Model):
|
||||
# Maximum number of trades per symbol
|
||||
max_open_trades_per_symbol = models.IntegerField(default=2)
|
||||
|
||||
price_slippage_percent = models.FloatField(default=2.5)
|
||||
callback_price_deviation_percent = models.FloatField(default=0.5)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -406,11 +460,15 @@ class AssetGroup(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
|
||||
# Account for checking pairs on children if specified
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
|
||||
# Dict like {"RUB": True, "USD": False}
|
||||
allowed = models.JSONField(null=True, blank=True, default=dict)
|
||||
when_no_data = models.IntegerField(choices=MAPPING_CHOICES, default=7)
|
||||
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)
|
||||
|
||||
when_bullish = models.IntegerField(choices=MAPPING_CHOICES, default=2)
|
||||
when_bearish = models.IntegerField(choices=MAPPING_CHOICES, default=3)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -420,25 +478,80 @@ class AssetGroup(models.Model):
|
||||
"""
|
||||
Get the total number of matches for this group.
|
||||
"""
|
||||
if isinstance(self.allowed, dict):
|
||||
truthy_values = [x for x in self.allowed.values() if x is True]
|
||||
return f"{len(truthy_values)}/{len(self.allowed)}"
|
||||
|
||||
@property
|
||||
def restrictions(self):
|
||||
"""
|
||||
Get the total number of restrictions for this group.
|
||||
"""
|
||||
return self.assetrestriction_set.count()
|
||||
asset_rule_total = AssetRule.objects.filter(group=self).count()
|
||||
asset_rule_positive = AssetRule.objects.filter(group=self, status=2).count()
|
||||
return f"{asset_rule_positive}/{asset_rule_total}"
|
||||
|
||||
|
||||
class AssetRestriction(models.Model):
|
||||
class AssetRule(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
asset = models.CharField(max_length=64)
|
||||
group = models.ForeignKey(AssetGroup, on_delete=models.CASCADE)
|
||||
aggregation = models.CharField(
|
||||
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)
|
||||
|
||||
# Ensure that the asset is unique per group
|
||||
class Meta:
|
||||
unique_together = ("asset", "group")
|
||||
|
||||
|
||||
class OrderSettings(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
pairs = models.CharField(max_length=4096, null=True, blank=True)
|
||||
pairs_parsed = models.JSONField(null=True, blank=True)
|
||||
|
||||
group = models.ForeignKey(
|
||||
AssetGroup, on_delete=models.CASCADE, null=True, blank=True
|
||||
order_type = models.CharField(
|
||||
choices=TYPE_CHOICES, max_length=255, default="market"
|
||||
)
|
||||
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
|
||||
take_profit_percent = models.FloatField(default=1.5)
|
||||
stop_loss_percent = models.FloatField(default=1.0)
|
||||
trailing_stop_loss_percent = models.FloatField(default=1.0, null=True, blank=True)
|
||||
trade_size_percent = models.FloatField(default=0.5)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ActiveManagementPolicy(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
when_trading_time_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_trends_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_position_size_violated = models.CharField(
|
||||
choices=ADJUST_CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_protection_violated = models.CharField(
|
||||
choices=ADJUST_CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_asset_groups_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_open_trades_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_open_trades_per_symbol_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_loss_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_risk_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_crossfilter_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -1,315 +1,328 @@
|
||||
{% load static %}
|
||||
{% load has_plan %}
|
||||
{% load cache %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>XF - {{ request.path_info }}</title>
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
|
||||
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
|
||||
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/gridstack-all.js' %}"></script>
|
||||
<script defer src="{% static 'js/magnet.min.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener("restore-scroll", function(event) {
|
||||
var scrollpos = localStorage.getItem('scrollpos');
|
||||
if (scrollpos) {
|
||||
window.scrollTo(0, scrollpos)
|
||||
};
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:beforeSwap", function(event) {
|
||||
localStorage.setItem('scrollpos', window.scrollY);
|
||||
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Get all "navbar-burger" elements
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach( el => {
|
||||
el.addEventListener('click', () => {
|
||||
|
||||
// Get the target from the "data-target" attribute
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
|
||||
});
|
||||
{% cache 600 head request.path_info %}
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>XF - {{ request.path_info }}</title>
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
|
||||
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
|
||||
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/gridstack-all.js' %}"></script>
|
||||
<script defer src="{% static 'js/magnet.min.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener("restore-scroll", function(event) {
|
||||
var scrollpos = localStorage.getItem('scrollpos');
|
||||
if (scrollpos) {
|
||||
window.scrollTo(0, scrollpos)
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.icon { border-bottom: 0px !important;}
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.nowrap-parent {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nowrap-child {
|
||||
display: inline-block;
|
||||
}
|
||||
.htmx-indicator{
|
||||
opacity:0;
|
||||
transition: opacity 500ms ease-in;
|
||||
}
|
||||
.htmx-request .htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
.htmx-request.htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
document.addEventListener("htmx:beforeSwap", function(event) {
|
||||
localStorage.setItem('scrollpos', window.scrollY);
|
||||
|
||||
.tooltiptext {
|
||||
visibility: hidden;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
.rounded-tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
// Get all "navbar-burger" elements
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
|
||||
.table {
|
||||
background: transparent !important;
|
||||
}
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach( el => {
|
||||
el.addEventListener('click', () => {
|
||||
|
||||
tr {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
// Get the target from the "data-target" attribute
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
tr:hover {
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
|
||||
a.panel-block {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
a.panel-block:hover {
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.icon { border-bottom: 0px !important;}
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.nowrap-parent {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nowrap-child {
|
||||
display: inline-block;
|
||||
}
|
||||
.htmx-indicator{
|
||||
opacity:0;
|
||||
transition: opacity 500ms ease-in;
|
||||
}
|
||||
.htmx-request .htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
.htmx-request.htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
|
||||
.panel, .box, .modal {
|
||||
background-color:rgba(250, 250, 250, 0.5) !important;
|
||||
}
|
||||
.modal, .modal.box{
|
||||
background-color:rgba(210, 210, 210, 0.9) !important;
|
||||
}
|
||||
.modal-background{
|
||||
background-color:rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
.tooltiptext {
|
||||
visibility: hidden;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.has-background-grey-lighter{
|
||||
background-color:rgba(219, 219, 219, 0.5) !important;
|
||||
}
|
||||
.navbar {
|
||||
background-color:rgba(0, 0, 0, 0.03) !important;
|
||||
}
|
||||
.rounded-tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.grid-stack-item-content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
.table {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
tr {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
overflow-y:auto;
|
||||
overflow-x:auto;
|
||||
min-height: 90%;
|
||||
display: block;
|
||||
}
|
||||
tr:hover {
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.floating-window {
|
||||
/* background-color:rgba(210, 210, 210, 0.6) !important; */
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
max-height: 300px;
|
||||
z-index: 9000;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
}
|
||||
a.panel-block {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.floating-window .panel {
|
||||
background-color:rgba(250, 250, 250, 0.8) !important;
|
||||
}
|
||||
a.panel-block:hover {
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.grid-stack-item:hover .ui-resizable-handle {
|
||||
display: block !important;
|
||||
}
|
||||
.ui-resizable-handle {
|
||||
z-index: 39 !important;
|
||||
}
|
||||
.panel, .box, .modal {
|
||||
background-color:rgba(250, 250, 250, 0.5) !important;
|
||||
}
|
||||
.modal, .modal.box{
|
||||
background-color:rgba(210, 210, 210, 0.9) !important;
|
||||
}
|
||||
.modal-background{
|
||||
background-color:rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- Piwik --> {# Yes it's in the source, fight me #}
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
_paq.push(['setTrackerUrl', 'https://api-f98632bdcf666e3217c6c1a2bafc6c09.s.zm.is']);
|
||||
_paq.push(['setSiteId', 5]);
|
||||
_paq.push(['setApiToken', 'En6AFpSwq4vx3fuXEjSUY6jhUPi_MRinYBQw1FxOqsy']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://c87zpt9a74m181wto33r.s.zm.is/embed.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Piwik Code -->
|
||||
</head>
|
||||
.has-background-grey-lighter{
|
||||
background-color:rgba(219, 219, 219, 0.5) !important;
|
||||
}
|
||||
.navbar {
|
||||
background-color:rgba(0, 0, 0, 0.03) !important;
|
||||
}
|
||||
|
||||
.grid-stack-item-content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-block {
|
||||
overflow-y:auto;
|
||||
overflow-x:auto;
|
||||
min-height: 90%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.floating-window {
|
||||
/* background-color:rgba(210, 210, 210, 0.6) !important; */
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
max-height: 300px;
|
||||
z-index: 9000;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.floating-window .panel {
|
||||
background-color:rgba(250, 250, 250, 0.8) !important;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.grid-stack-item:hover .ui-resizable-handle {
|
||||
display: block !important;
|
||||
}
|
||||
.ui-resizable-handle {
|
||||
z-index: 39 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- Piwik --> {# Yes it's in the source, fight me #}
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
_paq.push(['setTrackerUrl', 'https://api-f98632bdcf666e3217c6c1a2bafc6c09.s.zm.is']);
|
||||
_paq.push(['setSiteId', 5]);
|
||||
_paq.push(['setApiToken', 'En6AFpSwq4vx3fuXEjSUY6jhUPi_MRinYBQw1FxOqsy']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://c87zpt9a74m181wto33r.s.zm.is/embed.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Piwik Code -->
|
||||
</head>
|
||||
{% endcache %}
|
||||
<body>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
<img src="{% static 'logo.svg' %}" width="112" height="28" alt="logo">
|
||||
</a>
|
||||
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="bar" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
{% cache 600 nav request.user.id %}
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
Home
|
||||
<img src="{% static 'logo.svg' %}" width="112" height="28" alt="logo">
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Exchange
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'profit' type='page' %}">
|
||||
Profit
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'positions' type='page' %}">
|
||||
Positions
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'trades' type='page' %}">
|
||||
Trades
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
|
||||
Accounts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Setup
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'strategies' type='page' %}">
|
||||
Strategies
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'signals' type='page' %}">
|
||||
Signals
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
|
||||
Hooks
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="{% url 'tradingtimes' type='page' %}">
|
||||
Trading Times
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'risks' type='page' %}">
|
||||
Risk Management
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'assetgroups' type='page' %}">
|
||||
Asset Groups
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Account
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
||||
Security
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||
Notifications
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.STRIPE_ENABLED %}
|
||||
{% if user.is_authenticated %}
|
||||
<a class="navbar-item" href="{% url 'billing' %}">
|
||||
Billing
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a class="navbar-item add-button">
|
||||
Install
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
{% if not user.is_authenticated %}
|
||||
<a class="button" href="{% url 'signup' %}">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button" href="{% url 'two_factor:login' %}">
|
||||
Log in
|
||||
</a>
|
||||
{% endif %}
|
||||
<div id="bar" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
Home
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Exchange
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'profit' type='page' %}">
|
||||
Profit
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'positions' type='page' %}">
|
||||
Positions
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'trades' type='page' %}">
|
||||
Trades
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
|
||||
Accounts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Setup
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'strategies' type='page' %}">
|
||||
Strategies
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'ordersettings' type='page' %}">
|
||||
Order Settings
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'signals' type='page' %}">
|
||||
Signals
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
|
||||
Hooks
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="{% url 'tradingtimes' type='page' %}">
|
||||
Trading Times
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'risks' type='page' %}">
|
||||
Risk Management
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'assetgroups' type='page' %}">
|
||||
Asset Groups
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'ams' type='page' %}">
|
||||
Active Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Account
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
||||
Security
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||
Notifications
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.BILLING_ENABLED %}
|
||||
{% if user.is_authenticated %}
|
||||
<a class="button" href="{% url 'logout' %}">Logout</a>
|
||||
<a class="navbar-item" href="{% url 'billing' %}">
|
||||
Billing
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a class="navbar-item add-button">
|
||||
Install
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
{% if not user.is_authenticated %}
|
||||
<a class="button" href="{% url 'signup' %}">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button" href="{% url 'two_factor:login' %}">
|
||||
Log in
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<form method="POST" action="{% url 'logout' %}" style="display:inline;">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="button">Logout</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
{% endcache %}
|
||||
<script>
|
||||
let deferredPrompt;
|
||||
const addBtn = document.querySelector('.add-button');
|
||||
|
||||
@@ -10,20 +10,6 @@
|
||||
</span>
|
||||
<span class="tag">{{ user.first_name }} {{ user.last_name }}</span>
|
||||
</a>
|
||||
<a class="panel-block">
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-binary" aria-hidden="true"></i>
|
||||
</span>
|
||||
{% for plan in user.plans.all %}
|
||||
<span class="tag">{{ plan.name }}</span>
|
||||
{% endfor %}
|
||||
</a>
|
||||
<a class="panel-block">
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-credit-card" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag">{{ user.last_payment }}</span>
|
||||
</a>
|
||||
<a class="panel-block" href="{% url 'portal' %}">
|
||||
<span class="panel-icon">
|
||||
<i class="fa-brands fa-stripe-s" aria-hidden="true"></i>
|
||||
|
||||
@@ -1,112 +1,116 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Account' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>exchange</th>
|
||||
<th>currency</th>
|
||||
<th>initial</th>
|
||||
<th>API key</th>
|
||||
<th>sandbox</th>
|
||||
<th>enabled</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.exchange }}</td>
|
||||
<td>{{ item.currency }}</td>
|
||||
<td>{{ item.initial_balance }}</td>
|
||||
<td>{{ item.api_key }}</td>
|
||||
<td>
|
||||
{% if item.sandbox %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% 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>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'account_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
{% cache 600 objects_accounts request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>exchange</th>
|
||||
<th>currency</th>
|
||||
<th>initial</th>
|
||||
<th>API key</th>
|
||||
<th>sandbox</th>
|
||||
<th>enabled</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.exchange }}</td>
|
||||
<td>{{ item.currency }}</td>
|
||||
<td>{{ item.initial_balance }}</td>
|
||||
<td>{{ item.api_key }}</td>
|
||||
<td>
|
||||
{% if item.sandbox %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'account_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'account_info' type=type pk=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'account_info' type=type pk=item.id %}"
|
||||
hx-get="{% url 'account_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'account_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'account_info' type=type pk=item.id %}"><button
|
||||
class="button">
|
||||
<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 'account_info' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
61
core/templates/partials/activemanagement-list.html
Normal file
61
core/templates/partials/activemanagement-list.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetManagementPolicy' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_active_management request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ams_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'ams_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
59
core/templates/partials/asset-filter-list.html
Normal file
59
core/templates/partials/asset-filter-list.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetGroup' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_assetgroups_field request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>symbol</th>
|
||||
<th>status</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for key, item in object_list.items %}
|
||||
<tr class="
|
||||
{% if item is True %}has-background-success-light
|
||||
{% elif item is False %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ item }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetfilter_flip' group_id=group_id symbol=key %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon" data-tooltip="Flip direction">
|
||||
<i class="fa-solid fa-arrows-repeat"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'assetfilter_delete' group_id=group_id symbol=key %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,72 +1,78 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetGroup' 'core.AssetRule' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>account</th>
|
||||
<th>status</th>
|
||||
<th>restrictions</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.account }}</td>
|
||||
<td>{{ item.matches }}</td>
|
||||
<td>{{ item.restrictions }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetgroup_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'assetgroup_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<a href="{% url 'assetrestrictions' type='page' group=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% cache 600 objects_assetgroups request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>status</th>
|
||||
<th>hook</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</td>
|
||||
<td>
|
||||
<a
|
||||
href="{% url 'assetrules' type='page' group=item.id %}">
|
||||
{{ item.matches }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{settings.URL}}/{{settings.ASSET_PATH}}/{{ item.webhook_id }}/');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetgroup_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'assetgroup_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,61 +0,0 @@
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>pairs</th>
|
||||
<th>group</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.pairs_parsed|length }}</td>
|
||||
<td>{{ item.group }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetrestriction_update' type=type group=item.group.id pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'assetrestriction_delete' type=type group=item.group.id pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
77
core/templates/partials/assetrule-list.html
Normal file
77
core/templates/partials/assetrule-list.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetRule' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_assetrules request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>asset</th>
|
||||
<th>group</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>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<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.user }}</td>
|
||||
<td>{{ item.asset }}</td>
|
||||
<td>{{ item.group }}</td>
|
||||
<td>{{ item.get_aggregation_display }}</td>
|
||||
<td>{{ item.value }}</td>
|
||||
<td>{{ item.get_original_status_display }}</td>
|
||||
<td>{{ item.get_status_display }}</td>
|
||||
<td>{{ item.trigger_above }}</td>
|
||||
<td>{{ item.trigger_below }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetrule_update' type=type group=item.group.id pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'assetrule_delete' type=type group=item.group.id pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.asset }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,50 +1,54 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Callback' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_callbacks request.user.id object_list type last %}
|
||||
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>hook id</th>
|
||||
<th>hook name</th>
|
||||
<th>title</th>
|
||||
<th>message</th>
|
||||
<th>period</th>
|
||||
<th>sent</th>
|
||||
<th>trade</th>
|
||||
<th>exchange</th>
|
||||
<th>symbol</th>
|
||||
<th>price</th>
|
||||
<th>contract</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.hook.id }}</td>
|
||||
<td>
|
||||
<a
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML">{{ item.hook.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>{{ item.message }}</td>
|
||||
<td>{{ item.period }}</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>
|
||||
|
||||
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>hook id</th>
|
||||
<th>hook name</th>
|
||||
<th>title</th>
|
||||
<th>message</th>
|
||||
<th>period</th>
|
||||
<th>sent</th>
|
||||
<th>trade</th>
|
||||
<th>exchange</th>
|
||||
<th>symbol</th>
|
||||
<th>price</th>
|
||||
<th>contract</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.hook.id }}</td>
|
||||
<td>
|
||||
<a
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML">{{ item.hook.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>{{ item.message }}</td>
|
||||
<td>{{ item.period }}</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">
|
||||
|
||||
<td>
|
||||
<div class="buttons">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,84 +1,96 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Hook' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>hook</th>
|
||||
<th>received hooks</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td><code>{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/</code></td>
|
||||
<td>{{ item.received }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'hook_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
{% cache 600 objects_hooks request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>hook</th>
|
||||
<th>received hooks</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/');">
|
||||
<span class="icon" data-tooltip="Copy to clipboard">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'hook_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'callbacks' type='page' object_type='hook' object_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.received }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'callbacks' type=type object_type='hook' object_id=item.id %}"
|
||||
hx-get="{% url 'hook_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'hook_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'callbacks' type='page' object_type='hook' object_id=item.id %}"><button
|
||||
class="button">
|
||||
<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 'callbacks' type=type object_type='hook' object_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
69
core/templates/partials/ordersettings-list.html
Normal file
69
core/templates/partials/ordersettings-list.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.OrderSettings' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_ordersettings request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>TP</th>
|
||||
<th>SL</th>
|
||||
<th>TSL</th>
|
||||
<th>size</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</td>
|
||||
<td>{{ item.take_profit_percent}}</td>
|
||||
<td>{{ item.stop_loss_percent }}</td>
|
||||
<td>{{ item.trailing_stop_loss_percent }}</td>
|
||||
<td>{{ item.trade_size_percent }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ordersettings_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'ordersettings_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,33 +1,36 @@
|
||||
{% extends 'partials/generic-detail.html' %}
|
||||
{% extends 'mixins/partials/generic-detail.html' %}
|
||||
{% load cache %}
|
||||
|
||||
{% block tbody %}
|
||||
{% for key, item in object.items %}
|
||||
<tr>
|
||||
{% if key == 'trade_ids' %}
|
||||
<th>{{ key }}</th>
|
||||
<td>
|
||||
{% if item is not None %}
|
||||
{% for trade_id in item %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'trade_action' type=type account_id=object.account_id trade_id=trade_id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-small {% if trade_id in valid_trade_ids %}is-primary{% else %}is-warning{% endif %}">
|
||||
{{ trade_id }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<th>{{ key }}</th>
|
||||
<td>
|
||||
{% if item is not None %}
|
||||
{{ item }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% cache 600 object_position_detail request.user.id object type %}
|
||||
{% for key, item in object.items %}
|
||||
<tr>
|
||||
{% if key == 'trade_ids' %}
|
||||
<th>{{ key }}</th>
|
||||
<td>
|
||||
{% if item is not None %}
|
||||
{% for trade_id in item %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'trade_action' type=type account_id=object.account_id trade_id=trade_id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-small {% if trade_id in valid_trade_ids %}is-primary{% else %}is-warning{% endif %}">
|
||||
{{ trade_id }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<th>{{ key }}</th>
|
||||
<td>
|
||||
{% if item is not None %}
|
||||
{{ item }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endcache %}
|
||||
{% endblock %}
|
||||
@@ -1,76 +1,93 @@
|
||||
{% load cache %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body, every 5s"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>account</th>
|
||||
<th>asset</th>
|
||||
<th>price</th>
|
||||
<th>units</th>
|
||||
<th>quote</th>
|
||||
<th>P/L</th>
|
||||
<th>side</th>
|
||||
<th>trades</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.unrealized_pl > 0 %}has-background-success-light
|
||||
{% elif item.unrealized_pl < 0 %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.account }}</td>
|
||||
<td>{{ item.symbol }}</td>
|
||||
<td>{{ item.price }}</td>
|
||||
<td>{{ item.units }}</td>
|
||||
<td>{{ item.value }}</td>
|
||||
<td>{{ item.unrealized_pl }}</td>
|
||||
<td>
|
||||
{% if item.side == 'long' %}
|
||||
<span class="icon has-text-success" data-tooltip="long">
|
||||
<i class="fa-solid fa-up"></i>
|
||||
</span>
|
||||
{% elif item.side == 'short' %}
|
||||
<span class="icon has-text-danger" data-tooltip="short">
|
||||
<i class="fa-solid fa-down"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.trade_ids|length }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<!-- <button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
{% cache 600 objects_positions request.user.id object_list type %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body, every 5s"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>account</th>
|
||||
<th>asset</th>
|
||||
<th>price</th>
|
||||
<th>units</th>
|
||||
<th>quote</th>
|
||||
<th>P/L</th>
|
||||
<th>side</th>
|
||||
<th>trades</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.unrealized_pl > 0 %}has-background-success-light
|
||||
{% elif item.unrealized_pl < 0 %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.account }}</td>
|
||||
<td>{{ item.symbol }}</td>
|
||||
<td>{{ item.price }}</td>
|
||||
<td>{{ item.units }}</td>
|
||||
<td>{{ item.value }}</td>
|
||||
<td>{{ item.unrealized_pl }}</td>
|
||||
<td>
|
||||
{% if item.side == 'long' %}
|
||||
<span class="icon has-text-success" data-tooltip="long">
|
||||
<i class="fa-solid fa-up"></i>
|
||||
</span>
|
||||
</button> -->
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'position_action' side=item.side account_id=item.account_id symbol=item.symbol %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#notification"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you wish to close {{ item.symbol }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% elif item.side == 'short' %}
|
||||
<span class="icon has-text-danger" data-tooltip="short">
|
||||
<i class="fa-solid fa-down"></i>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.trade_ids|length }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<!-- <button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button> -->
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'position_action' side=item.side account_id=item.account_id symbol=item.symbol %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#notification"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you wish to close {{ item.symbol }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}">
|
||||
<button
|
||||
class="button">
|
||||
<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 'position_action' type=type account_id=item.account_id symbol=item.symbol %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
@@ -78,25 +95,11 @@
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,48 +1,50 @@
|
||||
{% load static %}
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Plan' as last %}
|
||||
{% cache 600 objects_plans request.user.id plans last %}
|
||||
|
||||
{% for plan in plans %}
|
||||
|
||||
|
||||
<div class="box">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-64x64">
|
||||
<img src="{% static plan.image %}" alt="Image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<p class="subtitle">
|
||||
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
|
||||
{% if plan in user_plans %}
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ plan.description }}
|
||||
</p>
|
||||
{% for plan in plans %}
|
||||
<div class="box">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-64x64">
|
||||
<img src="{% static plan.image %}" alt="Image">
|
||||
</figure>
|
||||
</div>
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-left">
|
||||
{% if plan not in user_plans %}
|
||||
<a class="level-item" href="/order/{{ plan.name }}">
|
||||
<span class="icon is-small has-text-success">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if plan in user_plans %}
|
||||
<a class="level-item" href="/cancel_subscription/{{ plan.name }}">
|
||||
<span class="icon is-small has-text-info">
|
||||
<i class="fas fa-cancel" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<p class="subtitle">
|
||||
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
|
||||
{% if plan in user_plans %}
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ plan.description }}
|
||||
</p>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-left">
|
||||
{% if plan not in user_plans %}
|
||||
<a class="level-item" href="/order/{{ plan.name }}">
|
||||
<span class="icon is-small has-text-success">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if plan in user_plans %}
|
||||
<a class="level-item" href="/cancel_subscription/{{ plan.name }}">
|
||||
<span class="icon is-small has-text-info">
|
||||
<i class="fas fa-cancel" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endcache %}
|
||||
@@ -1,32 +1,34 @@
|
||||
{% load cache %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_profit request.user.id object_list type %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body, every 3s"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>name</th>
|
||||
<th>P/L</th>
|
||||
<th>trade</th>
|
||||
<th>balance</th>
|
||||
<th>currency</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.pl > 0 %}has-background-success-light
|
||||
{% elif item.pl < 0 %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.account.id }}</td>
|
||||
<td>{{ item.account.name }}</td>
|
||||
<td>{{ item.pl }}</td>
|
||||
<td>{{ item.unrealizedPL }}</td>
|
||||
<td>{{ item.balance }}</td>
|
||||
<td>{{ item.currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body, every 3s"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>name</th>
|
||||
<th>P/L</th>
|
||||
<th>trade</th>
|
||||
<th>balance</th>
|
||||
<th>currency</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.pl > 0 %}has-background-success-light
|
||||
{% elif item.pl < 0 %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.account.id }}</td>
|
||||
<td>{{ item.account.name }}</td>
|
||||
<td>{{ item.pl }}</td>
|
||||
<td>{{ item.unrealizedPL }}</td>
|
||||
<td>{{ item.balance }}</td>
|
||||
<td>{{ item.currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,65 +1,73 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.RiskModel' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>max loss percent</th>
|
||||
<th>max risk percent</th>
|
||||
<th>max open trades</th>
|
||||
<th>max open trades per symbol</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.max_loss_percent }}</td>
|
||||
<td>{{ item.max_risk_percent }}</td>
|
||||
<td>{{ item.max_open_trades }}</td>
|
||||
<td>{{ item.max_open_trades_per_symbol }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'risk_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
{% cache 600 objects_risk request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>max loss percent</th>
|
||||
<th>max risk percent</th>
|
||||
<th>max open trades</th>
|
||||
<th>max open trades per symbol</th>
|
||||
<th>max price slippage percent</th>
|
||||
<th>max callback price deviation percent</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.max_loss_percent }}</td>
|
||||
<td>{{ item.max_risk_percent }}</td>
|
||||
<td>{{ item.max_open_trades }}</td>
|
||||
<td>{{ item.max_open_trades_per_symbol }}</td>
|
||||
<td>{{ item.price_slippage_percent }}</td>
|
||||
<td>{{ item.callback_price_deviation_percent }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'risk_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'risk_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'risk_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,99 +1,103 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Signal' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>signal</th>
|
||||
<th>hook</th>
|
||||
<th>direction</th>
|
||||
<th>received hooks</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.direction == 'buy' %}has-background-success-light
|
||||
{% elif item.direction == 'sell' %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.signal }}</td>
|
||||
<td>
|
||||
<a
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML">{{ item.hook.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.direction }}</td>
|
||||
<td>{{ item.received }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
{% cache 600 objects_signals request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>signal</th>
|
||||
<th>hook</th>
|
||||
<th>direction</th>
|
||||
<th>received hooks</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.direction == 'buy' %}has-background-success-light
|
||||
{% elif item.direction == 'sell' %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.signal }}</td>
|
||||
<td>
|
||||
<a
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'signal_update' type=type pk=item.id %}"
|
||||
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<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 'signal_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'callbacks' type='page' object_type='signal' object_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
hx-swap="innerHTML">{{ item.hook.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.direction }}</td>
|
||||
<td>{{ item.received }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'callbacks' type=type object_type='signal' object_id=item.id %}"
|
||||
hx-get="{% url 'signal_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'signal_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'callbacks' type='page' object_type='signal' object_id=item.id %}"><button
|
||||
class="button">
|
||||
<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 'callbacks' type=type object_type='signal' object_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,98 +1,124 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Strategy' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<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 object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</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 'strategy_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
{% cache 600 objects_strategies request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>account</th>
|
||||
<th>signal trading</th>
|
||||
<th>active management</th>
|
||||
<th>enabled</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</td>
|
||||
<td>{{ item.account }}</td>
|
||||
<td>
|
||||
{% if item.signal_trading_enabled %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'strategy_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'trenddirections' type=type strategy_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon" data-tooltip="View trends">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.active_management_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>
|
||||
{% 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>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'trenddirections' type=type strategy_id=item.id %}"
|
||||
hx-get="{% url 'strategy_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon" data-tooltip="View trends">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'strategy_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'trenddirections' type=type strategy_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon" data-tooltip="View trends">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'trenddirections' type=type strategy_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon" data-tooltip="View trends">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user