Compare commits

...

94 Commits

Author SHA1 Message Date
e94d693a39 Update compose to work with Podman 2025-01-23 11:40:30 +00:00
7a44660fc1 Resolve conflict with Redis 2025-01-23 11:40:04 +00:00
9ac3ffa540 Add static directory generated by collectstatic to ignore 2025-01-23 11:38:53 +00:00
6ccf84be26 Make project work with Podman 2024-12-28 13:20:55 +00:00
6d9c78d2e1 Remove dev compose 2024-12-22 17:23:31 +00:00
4079207a05 Begin implementing MEXC 2024-12-03 14:12:42 +00:00
761b084704 Fix Redis, begin implementing MEXC 2024-11-16 17:31:43 +00:00
95a4a6930c Fix changed OANDA API 2024-06-10 06:42:46 +01:00
a788a65ba6 Change Redis cache 2024-06-10 05:28:31 +01:00
e10c6f5c46 Fix price extraction bug and remove debugging statements 2023-08-26 11:05:28 +00:00
cd32dff779 Narrowing down 2023-08-24 17:59:17 +00:00
a2f3170ab5 Even more... 2023-08-24 17:55:53 +00:00
3d91fb394a More debugging 2023-08-24 17:52:18 +00:00
771a944a13 Add more debugging 2023-08-24 17:50:55 +00:00
542dca8324 Add debugging information 2023-08-24 17:47:02 +00:00
a68ade9efe Fix development 2023-08-10 17:11:40 +00:00
aca9897f44 Add development Makefile 2023-07-29 16:34:29 +00:00
9474a516ac Undo Podman changes 2023-07-29 16:28:12 +00:00
8ef39ffe48 Migrate to Podman 2023-07-06 16:11:02 +00:00
b4424a7782 Begin work on increasing position size 2023-02-28 07:20:12 +00:00
5843000df6 Add comments and clean up Lago customers 2023-02-27 07:20:42 +00:00
9d37e2bfb8 Integrate Lago with Stripe 2023-02-24 07:20:51 +00:00
cde1392e68 Consolidate migrations 2023-02-24 07:20:31 +00:00
be10375f60 Amend admin for user 2023-02-24 07:20:31 +00:00
ac4c248175 Begin implementing billing 2023-02-24 07:20:31 +00:00
0937f7299a Remove old models from admin 2023-02-24 07:20:31 +00:00
c6dd0ff286 Remove new ID field 2023-02-24 07:20:31 +00:00
86ace02de8 Attempt to fix migrations 2023-02-24 07:20:31 +00:00
fb5521c9f7 Migrate user id to UUID 2023-02-24 07:20:31 +00:00
682c42c0e8 Separate live tests for active management 2023-02-22 07:20:21 +00:00
9c537187f0 Finish AMS tests 2023-02-22 07:20:58 +00:00
ed63085e10 Implement updating protection 2023-02-22 07:20:37 +00:00
ba8eb69309 Begin protection checks 2023-02-20 23:57:20 +00:00
314d4022ea Add description to AMS policy form 2023-02-20 17:21:33 +00:00
89ef8408e6 Amend asset filter matching to be more explicit 2023-02-20 07:20:01 +00:00
9e22abe057 Implement adjusting positions and begin writing live tests for AMS 2023-02-20 07:20:03 +00:00
a840be3834 Adjust initial balance in live tests 2023-02-20 07:20:37 +00:00
8bb5c2c91b Remove empty functions from checks 2023-02-20 07:20:22 +00:00
db58fb34eb Ensure an account only has one strategy with active management 2023-02-18 21:42:56 +00:00
ea0a6f21ce Remove some comments 2023-02-18 21:39:06 +00:00
8d9fe15346 Fix returning the balance 2023-02-18 21:36:46 +00:00
2b6f00a889 Run checks and actions from the management command 2023-02-18 21:36:38 +00:00
0bf3329b61 Remove comment 2023-02-18 21:25:11 +00:00
911ccde37b Implement trade mutation pipeline and active management actions 2023-02-18 21:23:59 +00:00
ae104f446a Start implementing active management actions 2023-02-18 17:55:39 +00:00
15a8bec105 Simplify active management by only specifying trade IDs for violations 2023-02-18 14:36:58 +00:00
466b17400f Finish implementing active management hooks 2023-02-18 11:54:30 +00:00
3e35214e82 Fix open trades checks 2023-02-17 22:23:12 +00:00
d262f208b5 Write crossfilter, asset groups and max open trades implementation and tests 2023-02-17 22:11:46 +00:00
67117f0978 Write protection check tests 2023-02-17 17:05:52 +00:00
1dbb3fcf79 Add more hooks to active management 2023-02-17 07:20:15 +00:00
dd3b3521d9 Move more checks from market into checks library 2023-02-17 07:20:28 +00:00
da67177a18 Begin work on scheduling management command 2023-02-17 07:20:19 +00:00
ffdbcecc8d Do profit calculation the right way around 2023-02-16 07:20:41 +00:00
c0f266da73 Add signals and active management to strategy list 2023-02-15 20:04:23 +00:00
3854bdcc7d Add signals trading enabled 2023-02-15 20:02:38 +00:00
5c090433a3 Add migration to remove order settings from strategies 2023-02-15 19:13:16 +00:00
eefd704800 Add user field to all list templates 2023-02-15 18:44:19 +00:00
b4afa32a6e Move order settings to OrderSettings 2023-02-15 18:41:08 +00:00
69cf8dcc10 Add order settings to strategy 2023-02-15 18:35:46 +00:00
660aca44db Begin adding order settings 2023-02-15 18:33:38 +00:00
1974b19157 Move risk model to strategy 2023-02-15 18:15:36 +00:00
9a5ed32be9 Add Lago 2023-02-15 07:20:53 +00:00
b37c62f5f1 Fix delete confirmation 2023-02-15 07:20:53 +00:00
bc60eabb05 Fix caching with different types 2023-02-15 07:20:53 +00:00
b6952767d5 Fix asset filter 2023-02-14 07:20:47 +00:00
0a89d96b86 Log assetfilter messages to console 2023-02-14 07:20:47 +00:00
73cf56c50e Use correct template for position details 2023-02-14 07:20:47 +00:00
b6126a8454 Remove subtitle for positions 2023-02-14 07:20:47 +00:00
7a593b902b Add help texts to AssetRule 2023-02-14 07:20:47 +00:00
74fdd8a735 Fix asset_group reference 2023-02-14 07:20:47 +00:00
f4ae8fbc5f Check equality with None instead of truthfulness 2023-02-14 07:20:47 +00:00
27de8090de Add instruments to account readout 2023-02-14 07:20:47 +00:00
1fc969177d Send all precision errors to the user 2023-02-14 07:20:47 +00:00
68a33cea7d Send the user a more detailed precision error message 2023-02-14 07:20:47 +00:00
c915fd1e41 Improve get precision error messages 2023-02-14 07:20:47 +00:00
507708574c Add original status 2023-02-14 07:20:47 +00:00
6385339b7b Don't print the JSON of webhooks 2023-02-14 07:20:47 +00:00
6464b6de05 Filter for enabled accounts 2023-02-13 21:02:59 +00:00
6ff5f718ba Implement asset rules as Asset Group children objects 2023-02-13 20:45:23 +00:00
b48af50620 Rename pairs to assets 2023-02-13 17:50:46 +00:00
0321aff9d5 Implement checking direction with assetfilter 2023-02-13 17:47:47 +00:00
dcfb963be6 Remove asset restrictions and make asset groups smarter 2023-02-13 07:20:40 +00:00
287facbab2 Allow changing the asset filter list 2023-02-11 18:46:26 +00:00
da9f32e882 Send the user a message when an asset restriction is hit 2023-02-11 18:25:09 +00:00
313c7f79d0 Write tests for asset filter 2023-02-11 18:22:49 +00:00
ce0b75ae2d Make account on AssetGroup optional 2023-02-11 18:18:07 +00:00
bdf8f04210 Re-add property fields 2023-02-11 18:07:05 +00:00
7afdd39af7 Fix adding asset restrictions 2023-02-11 17:45:22 +00:00
33d8e26c9b Use cachalot to invalidate caches 2023-02-11 17:22:25 +00:00
dea1cfe889 Use Hiredis 2023-02-11 16:01:26 +00:00
7d693ad1fa Vary cache on URL 2023-02-11 15:48:53 +00:00
a0c94b2097 Cache all object list templates 2023-02-11 14:52:00 +00:00
0acddb2048 Remove comments from settings 2023-02-11 14:04:21 +00:00
142 changed files with 6562 additions and 3293 deletions

2
.gitignore vendored
View File

@@ -158,3 +158,5 @@ cython_debug/
.vscode/
core/static/admin
core/static/debug_toolbar
Makefile
static/

View File

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

View File

@@ -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", "")
@@ -40,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", "ASSET_PATH"]
SETTINGS_EXPORT = ["BILLING_ENABLED", "URL", "HOOK_PATH", "ASSET_PATH"]

View File

@@ -53,25 +53,9 @@ INSTALLED_APPS = [
# "two_factor.plugins.yubikey",
# "otp_yubikey",
"mixins",
"cachalot",
]
# Performance optimisations
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "unix:///var/run/socks/redis.sock",
"OPTIONS": {
"db": "10",
"parser_class": "redis.connection.PythonParser",
"pool_class": "redis.BlockingConnectionPool",
},
}
}
# CACHE_MIDDLEWARE_ALIAS
# CACHE_MIDDLEWARE_SECONDS
# CACHE_MIDDLEWARE_KEY_PREFIX
CRISPY_TEMPLATE_PACK = "bulma"
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
@@ -80,16 +64,15 @@ 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",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_htmx.middleware.HtmxMiddleware",
# 'django.middleware.cache.UpdateCacheMiddleware',
# 'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
]
ROOT_URLCONF = "app.urls"
@@ -184,7 +167,7 @@ REST_FRAMEWORK = {
INTERNAL_IPS = [
"127.0.0.1",
"10.1.10.11",
# "10.1.10.11",
]
DEBUG_TOOLBAR_PANELS = [
@@ -203,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

View File

@@ -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
@@ -76,7 +78,7 @@ urlpatterns = [
),
path(
f"{settings.ASSET_PATH}/<str:webhook_id>/",
assets.AssetRestrictionAPI.as_view(),
assets.AssetGroupAPI.as_view(),
name="asset",
),
path("signals/<str:type>/", signals.SignalList.as_view(), name="signals"),
@@ -263,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)

View File

@@ -2,16 +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,
AssetRestriction,
Callback,
Hook,
NotificationSettings,
Plan,
RiskModel,
Session,
Signal,
Strategy,
Trade,
@@ -28,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",
# )
# },
# ),
)
@@ -94,16 +91,16 @@ class RiskModelAdmin(admin.ModelAdmin):
class AssetGroupAdmin(admin.ModelAdmin):
list_display = ("user", "name", "description", "account")
list_display = ("user", "name", "description", "webhook_id")
class AssetRestrictionAdmin(admin.ModelAdmin):
list_display = ("user", "name", "description", "webhook_id", "group")
# 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)
@@ -115,4 +112,4 @@ 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)
# admin.site.register(AssetRestriction, AssetRestrictionAdmin)

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -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,7 +91,6 @@ class AccountForm(RestrictedFormMixin, ModelForm):
"exchange",
"api_key",
"api_secret",
"risk_model",
"initial_balance",
"sandbox",
"enabled",
@@ -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,6 +113,11 @@ 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 = (
@@ -120,19 +125,16 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
"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 = {
@@ -140,19 +142,16 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
"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(
@@ -178,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(
@@ -217,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):
@@ -292,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.",
@@ -300,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.",
}
@@ -309,47 +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",
"pairs_parsed",
"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 when a webhook is received. This does nothing on its own.",
"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.",
}
pairs_parsed = forms.BooleanField(widget=forms.HiddenInput)
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
View 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
from pydantic import BaseModel, Field

View File

@@ -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",
}

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
# Generated by Django 4.1.6 on 2023-02-10 21:07
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0053_assetrestriction_pairs_parsed_and_more'),
]
operations = [
migrations.AddField(
model_name='assetrestriction',
name='webhook_id',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
migrations.AlterField(
model_name='assetgroup',
name='allowed',
field=models.JSONField(blank=True, default=dict, null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 4.1.6 on 2023-02-10 22:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0054_assetrestriction_webhook_id_alter_assetgroup_allowed'),
]
operations = [
migrations.AddField(
model_name='strategy',
name='asset_group',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.assetgroup'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.6 on 2023-02-10 23:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0055_strategy_asset_group'),
]
operations = [
migrations.AlterField(
model_name='assetrestriction',
name='pairs_parsed',
field=models.JSONField(blank=True, default=dict, null=True),
),
]

View File

@@ -7,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"),
@@ -41,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)
@@ -114,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):
@@ -130,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()
@@ -142,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):
@@ -174,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)
@@ -347,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
)
@@ -361,17 +398,26 @@ 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"
@@ -402,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
@@ -411,41 +460,98 @@ 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 f"{self.name} ({self.restrictions})"
return self.name
@property
def matches(self):
"""
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, default=dict)
webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=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

View File

@@ -1,10 +1,9 @@
{% load static %}
{% load has_plan %}
{% load cache %}
<!DOCTYPE html>
<html lang="en-GB">
{% cache 600 head %}
{% cache 600 head request.path_info %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -248,6 +247,9 @@
<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>
@@ -264,6 +266,9 @@
<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">
@@ -281,7 +286,7 @@
</div>
</div>
{% endif %}
{% if settings.STRIPE_ENABLED %}
{% if settings.BILLING_ENABLED %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'billing' %}">
Billing
@@ -306,7 +311,10 @@
{% endif %}
{% if user.is_authenticated %}
<a class="button" href="{% url 'logout' %}">Logout</a>
<form method="POST" action="{% url 'logout' %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="button">Logout</button>
</form>
{% endif %}
</div>

View File

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

View File

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

View 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 %}

View 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 %}

View File

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

View File

@@ -1,71 +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>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 }}</td>
<td>{{ item.pairs_parsed|length }}</td>
<td>{{ item.group }}</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 '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>

View 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 %}

View File

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

View File

@@ -1,92 +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>
<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>
</a>
</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 %}

View 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 %}

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More