Compare commits
258 Commits
8ee56b0e37
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
e94d693a39
|
|||
|
7a44660fc1
|
|||
|
9ac3ffa540
|
|||
|
6ccf84be26
|
|||
|
6d9c78d2e1
|
|||
|
4079207a05
|
|||
|
761b084704
|
|||
|
95a4a6930c
|
|||
|
a788a65ba6
|
|||
|
e10c6f5c46
|
|||
|
cd32dff779
|
|||
|
a2f3170ab5
|
|||
|
3d91fb394a
|
|||
|
771a944a13
|
|||
|
542dca8324
|
|||
|
a68ade9efe
|
|||
|
aca9897f44
|
|||
|
9474a516ac
|
|||
|
8ef39ffe48
|
|||
|
b4424a7782
|
|||
|
5843000df6
|
|||
|
9d37e2bfb8
|
|||
|
cde1392e68
|
|||
|
be10375f60
|
|||
|
ac4c248175
|
|||
|
0937f7299a
|
|||
|
c6dd0ff286
|
|||
|
86ace02de8
|
|||
|
fb5521c9f7
|
|||
|
682c42c0e8
|
|||
|
9c537187f0
|
|||
|
ed63085e10
|
|||
|
ba8eb69309
|
|||
|
314d4022ea
|
|||
|
89ef8408e6
|
|||
|
9e22abe057
|
|||
|
a840be3834
|
|||
|
8bb5c2c91b
|
|||
|
db58fb34eb
|
|||
|
ea0a6f21ce
|
|||
|
8d9fe15346
|
|||
|
2b6f00a889
|
|||
|
0bf3329b61
|
|||
|
911ccde37b
|
|||
|
ae104f446a
|
|||
|
15a8bec105
|
|||
|
466b17400f
|
|||
|
3e35214e82
|
|||
|
d262f208b5
|
|||
|
67117f0978
|
|||
|
1dbb3fcf79
|
|||
|
dd3b3521d9
|
|||
|
da67177a18
|
|||
|
ffdbcecc8d
|
|||
|
c0f266da73
|
|||
|
3854bdcc7d
|
|||
|
5c090433a3
|
|||
|
eefd704800
|
|||
|
b4afa32a6e
|
|||
|
69cf8dcc10
|
|||
|
660aca44db
|
|||
|
1974b19157
|
|||
|
9a5ed32be9
|
|||
|
b37c62f5f1
|
|||
|
bc60eabb05
|
|||
|
b6952767d5
|
|||
|
0a89d96b86
|
|||
|
73cf56c50e
|
|||
|
b6126a8454
|
|||
|
7a593b902b
|
|||
|
74fdd8a735
|
|||
|
f4ae8fbc5f
|
|||
|
27de8090de
|
|||
|
1fc969177d
|
|||
|
68a33cea7d
|
|||
|
c915fd1e41
|
|||
|
507708574c
|
|||
|
6385339b7b
|
|||
|
6464b6de05
|
|||
|
6ff5f718ba
|
|||
|
b48af50620
|
|||
|
0321aff9d5
|
|||
|
dcfb963be6
|
|||
|
287facbab2
|
|||
|
da9f32e882
|
|||
|
313c7f79d0
|
|||
|
ce0b75ae2d
|
|||
|
bdf8f04210
|
|||
|
7afdd39af7
|
|||
|
33d8e26c9b
|
|||
|
dea1cfe889
|
|||
|
7d693ad1fa
|
|||
|
a0c94b2097
|
|||
|
0acddb2048
|
|||
|
8455d64e31
|
|||
|
57078c10c1
|
|||
|
1f43a00c7a
|
|||
|
010aba7f81
|
|||
|
c283c6c192
|
|||
|
aa227c53ac
|
|||
|
0b7dc001bf
|
|||
|
1d01368570
|
|||
|
101a4933c9
|
|||
|
119acdd734
|
|||
|
659b73e695
|
|||
|
8750e999b3
|
|||
|
f81d632df3
|
|||
|
7938bffc8d
|
|||
|
72055181bc
|
|||
|
e00cdc906e
|
|||
|
f4ef280f80
|
|||
|
70d1fdbbd3
|
|||
|
7d0f979a96
|
|||
|
3f05553c71
|
|||
|
9a69120695
|
|||
|
23faeb6f71
|
|||
|
e55f903f42
|
|||
|
93be9e6ffe
|
|||
|
ae42d9b223
|
|||
|
1bab2a729b
|
|||
|
db870c39c6
|
|||
|
2dfaef324c
|
|||
|
483333bf28
|
|||
|
d3e2bc8648
|
|||
|
a6f9e74ee1
|
|||
|
b8b39ea8d3
|
|||
|
9dda0e8b4a
|
|||
|
72671aa87f
|
|||
|
9835219e51
|
|||
|
2fa61fb195
|
|||
|
e0ea4c86fa
|
|||
|
46aaff43c0
|
|||
|
b2361bda77
|
|||
|
b31a2d1464
|
|||
|
a18c150fe2
|
|||
|
b818e7e3f5
|
|||
|
c81cb62aca
|
|||
|
4e24ceac72
|
|||
|
52ddef4c8f
|
|||
|
29125d5087
|
|||
|
3a7b5c3ffd
|
|||
|
a41a1e76a5
|
|||
|
b7c46ba1d3
|
|||
|
a96c99b9e4
|
|||
|
8afe638f0d
|
|||
|
50820172b1
|
|||
|
8de99c1bcd
|
|||
|
246674b03e
|
|||
|
7ee698f457
|
|||
|
4c463e88f2
|
|||
|
f4772a3c7d
|
|||
|
3a39181261
|
|||
|
b882ba15d0
|
|||
|
1793b5cc5d
|
|||
|
4218fdedbc
|
|||
|
d6ab0ffd0e
|
|||
|
06d8c9f4b2
|
|||
|
d1c44cee92
|
|||
|
cf4b8a0195
|
|||
|
05f94e6e93
|
|||
|
a18572ebda
|
|||
|
103a15f0e3
|
|||
|
cd89b11611
|
|||
|
633894ae75
|
|||
|
312ddb4dc1
|
|||
|
0aef440229
|
|||
|
8840b04059
|
|||
|
1e85e830b2
|
|||
|
d396abca84
|
|||
|
aa8ee887d3
|
|||
|
575b6a240f
|
|||
|
af69b886ba
|
|||
|
e388624f65
|
|||
|
2ee5f7b937
|
|||
|
baed991eca
|
|||
|
d59d571679
|
|||
|
3b8e5dbdd1
|
|||
|
15e00112af
|
|||
|
43caab5bf7
|
|||
|
62c37a7a45
|
|||
|
6a549f3fd7
|
|||
|
3b3faecdf1
|
|||
|
242c9fbaed
|
|||
|
a39a5c3857
|
|||
|
2b13802009
|
|||
|
b3bacde8df
|
|||
|
62476e5da3
|
|||
|
c5d289ce85
|
|||
|
1ce6c3fafa
|
|||
|
5aac60a7ee
|
|||
|
077768975d
|
|||
|
848f69da5e
|
|||
|
66a18a6406
|
|||
|
5c2eeae043
|
|||
|
c0c1ccde8b
|
|||
|
682d141b8a
|
|||
|
bdae8ab093
|
|||
|
4527a9d04b
|
|||
|
21b9585192
|
|||
|
851d021af2
|
|||
|
f7242f4dd8
|
|||
|
f240c4b381
|
|||
|
4e1b574921
|
|||
|
6321fb9089
|
|||
|
87c794e9ac
|
|||
|
8ce0066c38
|
|||
|
d3694d1821
|
|||
|
06865d0aa9
|
|||
|
3a6c3cee1f
|
|||
|
0fc7c5c712
|
|||
|
7a64759ceb
|
|||
|
bb7d6d1b41
|
|||
|
974deeafaa
|
|||
|
4973582bdf
|
|||
|
69a2b269ad
|
|||
|
baa8e4fead
|
|||
|
bcb3272064
|
|||
|
b525611aaa
|
|||
|
cb88cf33c2
|
|||
|
3f8fb66656
|
|||
|
46bba54cb7
|
|||
|
f6b5652268
|
|||
|
ca434b8cf0
|
|||
|
d7e81dedb2
|
|||
|
5c68191e5b
|
|||
|
c8f776e2a8
|
|||
|
2aac7d1bb5
|
|||
|
d2e0137c8d
|
|||
|
e3d57c9aa8
|
|||
|
781de3c772
|
|||
|
9fc5d2f4d7
|
|||
|
67404fc161
|
|||
|
3f855dfb59
|
|||
|
afb0504dca
|
|||
|
c3d908341a
|
|||
|
8b52063473
|
|||
|
af9f874209
|
|||
|
bf863f43b2
|
|||
|
40f6330a13
|
|||
|
47384aed5f
|
|||
|
e8a2f9b0fa
|
|||
|
6dd2997a74
|
|||
|
60979652d9
|
|||
|
d34ac39d68
|
|||
|
0c52cbd0f8
|
|||
|
c773b93675
|
|||
|
52216df5a4
|
|||
|
b36791d56b
|
|||
|
04a87c1da6
|
|||
|
7770a3844c
|
|||
|
74c46f2647
|
|||
|
f90f388e87
|
|||
|
65f650f1ac
|
|||
|
d319769fe0
|
|||
|
48858bf20b
|
|||
|
1f75da40af
|
|||
|
5cb7d08614
|
|||
|
396d838416
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,6 +9,7 @@ __pycache__/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
.python_history
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
@@ -157,3 +158,5 @@ cython_debug/
|
||||
.vscode/
|
||||
core/static/admin
|
||||
core/static/debug_toolbar
|
||||
Makefile
|
||||
static/
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: ^core/migrations
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.10.1
|
||||
rev: 5.11.5
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: [--max-line-length=88]
|
||||
exclude: ^core/migrations
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: 'v1.5.2' # replace with the latest tag on GitHub
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
- id: djhtml
|
||||
args: [-t 2]
|
||||
@@ -24,8 +24,7 @@ repos:
|
||||
exclude : ^core/static/css # slow
|
||||
- id: djjs
|
||||
exclude: ^core/static/js # slow
|
||||
# - repo: https://github.com/thibaudcolas/curlylint
|
||||
# rev: v0.13.1
|
||||
# hooks:
|
||||
# - id: curlylint
|
||||
# files: \.(html|sls)$
|
||||
- repo: https://github.com/sirwart/ripsecrets.git
|
||||
rev: v0.1.5
|
||||
hooks:
|
||||
- id: ripsecrets
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM python:3
|
||||
ARG OPERATION
|
||||
|
||||
RUN useradd -d /code xf
|
||||
RUN mkdir -p /code
|
||||
@@ -15,9 +16,13 @@ USER xf
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
WORKDIR /code
|
||||
COPY requirements.prod.txt /code/
|
||||
COPY requirements.txt /code/
|
||||
RUN python -m venv /venv
|
||||
RUN . /venv/bin/activate && pip install -r requirements.prod.txt
|
||||
RUN . /venv/bin/activate && pip install -r requirements.txt
|
||||
|
||||
# CMD . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini
|
||||
CMD . /venv/bin/activate && uvicorn --reload --workers 2 --uds /var/run/socks/app.sock app.asgi:application
|
||||
|
||||
CMD if [ "$OPERATION" = "uwsgi" ] ; then . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini ; else . /venv/bin/activate && exec python manage.py runserver 0.0.0.0:8000; fi
|
||||
|
||||
# CMD . /venv/bin/activate && uvicorn --reload --reload-include *.html --workers 2 --uds /var/run/socks/app.sock app.asgi:application
|
||||
# CMD . /venv/bin/activate && gunicorn -b 0.0.0.0:8000 --reload app.asgi:application -k uvicorn.workers.UvicornWorker
|
||||
23
Makefile
23
Makefile
@@ -1,23 +0,0 @@
|
||||
run:
|
||||
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env logs -f
|
||||
|
||||
migrate:
|
||||
docker-compose -f docker/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/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/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/docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
||||
26
Makefile-dev
Normal file
26
Makefile-dev
Normal file
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
docker-compose --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose --env-file=stack.env logs -f
|
||||
|
||||
test:
|
||||
docker-compose --env-file=stack.env run -e LIVE=$(LIVE) --rm app_dev sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
makemigrations:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py makemigrations"
|
||||
|
||||
auth:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py createsuperuser"
|
||||
|
||||
token:
|
||||
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
||||
26
Makefile-prod
Normal file
26
Makefile-prod
Normal file
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env up -d
|
||||
|
||||
build:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env build
|
||||
|
||||
stop:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env logs -f
|
||||
|
||||
test:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run -e LIVE=$(LIVE) --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
makemigrations:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"
|
||||
|
||||
auth:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py createsuperuser"
|
||||
|
||||
token:
|
||||
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"
|
||||
@@ -13,7 +13,8 @@ ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",")
|
||||
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",")
|
||||
|
||||
# Stripe
|
||||
STRIPE_ENABLED = getenv("STRIPE_ENABLED", "false").lower() in trues
|
||||
BILLING_ENABLED = getenv("BILLING_ENABLED", "false").lower() in trues
|
||||
|
||||
STRIPE_TEST = getenv("STRIPE_TEST", "true").lower() in trues
|
||||
STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "")
|
||||
STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "")
|
||||
@@ -29,74 +30,36 @@ STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "")
|
||||
|
||||
REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues
|
||||
|
||||
ASSET_FILTER = [
|
||||
"LINK/USDT",
|
||||
"PAXG/USD",
|
||||
"PAXG/USDT",
|
||||
"SHIB/USD",
|
||||
"TRX/USD",
|
||||
"TRX/USDT",
|
||||
"UNI/BTC",
|
||||
"UNI/USD",
|
||||
"UNI/USDT",
|
||||
"USDT/USD",
|
||||
"WBTC/USD",
|
||||
"YFI/BTC",
|
||||
"NEAR/USDT",
|
||||
"SUSHI/USDT",
|
||||
"DOGE/USDT",
|
||||
"LINK/BTC",
|
||||
"LINK/USD",
|
||||
"GRT/USD",
|
||||
"AVAX/BTC",
|
||||
"AVAX/USD",
|
||||
"AVAX/USDT",
|
||||
"SOL/BTC",
|
||||
"SOL/USD",
|
||||
"SOL/USDT",
|
||||
"BTC/USDT",
|
||||
"SUSHI/BTC",
|
||||
"SUSHI/USD",
|
||||
"BCH/BTC",
|
||||
"BCH/USD",
|
||||
"YFI/USD",
|
||||
"ETH/USD",
|
||||
"ETH/USDT",
|
||||
"YFI/USDT",
|
||||
"AAVE/USD",
|
||||
"AAVE/USDT",
|
||||
"ALGO/USD",
|
||||
"BAT/USD",
|
||||
"DAI/USDT",
|
||||
"ALGO/USDT",
|
||||
"MATIC/BTC",
|
||||
"MATIC/USD",
|
||||
"DOGE/USD",
|
||||
"MKR/USD",
|
||||
"BTC/USD",
|
||||
"DOGE/BTC",
|
||||
"LTC/BTC",
|
||||
"LTC/USD",
|
||||
"LTC/USDT",
|
||||
"ETH/BTC",
|
||||
"BCH/USDT",
|
||||
"DAI/USD",
|
||||
"NEAR/USD",
|
||||
]
|
||||
|
||||
# Hook URL, do not include leading or trailing slash
|
||||
HOOK_PATH = "hook"
|
||||
ASSET_PATH = "asset"
|
||||
|
||||
NOTIFY_TOPIC = getenv("NOTIFY_TOPIC", "great-fisk")
|
||||
|
||||
ELASTICSEARCH_USERNAME = getenv("ELASTICSEARCH_USERNAME", "elastic")
|
||||
ELASTICSEARCH_PASSWORD = getenv("ELASTICSEARCH_PASSWORD", "changeme")
|
||||
ELASTICSEARCH_HOST = getenv("ELASTICSEARCH_HOST", "localhost")
|
||||
ELASTICSEARCH_TLS = getenv("ELASTICSEARCH_TLS", "false") in trues
|
||||
|
||||
LAGO_API_KEY = getenv("LAGO_API_KEY", "")
|
||||
LAGO_ORG_ID = getenv("LAGO_ORG_ID", "")
|
||||
LAGO_URL = getenv("LAGO_URL", "")
|
||||
|
||||
DEBUG = getenv("DEBUG", "false").lower() in trues
|
||||
PROFILER = getenv("PROFILER", "false").lower() in trues
|
||||
|
||||
REDIS_HOST = getenv("REDIS_HOST", "redis_fisk_dev")
|
||||
REDIS_PASSWORD = getenv("REDIS_PASSWORD", "changeme")
|
||||
REDIS_DB = int(getenv("REDIS_DB", "10"))
|
||||
REDIS_PORT = int(getenv("REDIS_PORT", "6379"))
|
||||
|
||||
if DEBUG:
|
||||
import socket # only if you haven't already imported this
|
||||
|
||||
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
|
||||
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
|
||||
"127.0.0.1",
|
||||
"10.0.2.2",
|
||||
# "10.0.2.2",
|
||||
]
|
||||
|
||||
SETTINGS_EXPORT = ["STRIPE_ENABLED", "URL", "HOOK_PATH"]
|
||||
SETTINGS_EXPORT = ["BILLING_ENABLED", "URL", "HOOK_PATH", "ASSET_PATH"]
|
||||
|
||||
@@ -44,21 +44,29 @@ INSTALLED_APPS = [
|
||||
# "django_tables2_bulma_template",
|
||||
"django_otp",
|
||||
"django_otp.plugins.otp_totp",
|
||||
# "django_otp.plugins.otp_email",
|
||||
# 'django_otp.plugins.otp_hotp',
|
||||
"django_otp.plugins.otp_static",
|
||||
"two_factor",
|
||||
# "two_factor.plugins.phonenumber",
|
||||
# "two_factor.plugins.email",
|
||||
# "two_factor.plugins.yubikey",
|
||||
# "otp_yubikey",
|
||||
"mixins",
|
||||
"cachalot",
|
||||
]
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bulma"
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = (
|
||||
"uni_form",
|
||||
"bulma",
|
||||
)
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
||||
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
|
||||
|
||||
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",
|
||||
@@ -138,9 +146,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
AUTH_USER_MODEL = "core.User"
|
||||
|
||||
LOGIN_REDIRECT_URL = "home"
|
||||
|
||||
LOGOUT_REDIRECT_URL = "home"
|
||||
LOGIN_URL = "/accounts/login/"
|
||||
|
||||
LOGIN_REDIRECT_URL = "home"
|
||||
# LOGIN_URL = "/accounts/login/"
|
||||
|
||||
# 2FA
|
||||
LOGIN_URL = "two_factor:login"
|
||||
# LOGIN_REDIRECT_URL = 'two_factor:profile'
|
||||
|
||||
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
|
||||
ALLOWED_PAYMENT_METHODS = ["card"]
|
||||
@@ -153,7 +167,7 @@ REST_FRAMEWORK = {
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
"10.1.10.11",
|
||||
# "10.1.10.11",
|
||||
]
|
||||
|
||||
DEBUG_TOOLBAR_PANELS = [
|
||||
@@ -172,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
|
||||
|
||||
@@ -187,3 +220,12 @@ if PROFILER: # noqa - trust me its there
|
||||
# "region": f'{os.getenv("REGION")}',
|
||||
# }
|
||||
)
|
||||
|
||||
|
||||
def show_toolbar(request):
|
||||
return DEBUG # noqa: from local imports
|
||||
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"SHOW_TOOLBAR_CALLBACK": show_toolbar,
|
||||
}
|
||||
|
||||
236
app/urls.py
236
app/urls.py
@@ -16,36 +16,51 @@ Including another URLconf
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from django_otp.forms import OTPAuthenticationForm
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
|
||||
from core.views import accounts, base, callbacks, hooks, positions, strategies, trades
|
||||
from core.views.stripe_callbacks import Callback
|
||||
from core.views import (
|
||||
accounts,
|
||||
assets,
|
||||
base,
|
||||
callbacks,
|
||||
hooks,
|
||||
limits,
|
||||
notifications,
|
||||
ordersettings,
|
||||
policies,
|
||||
positions,
|
||||
profit,
|
||||
risk,
|
||||
signals,
|
||||
strategies,
|
||||
trades,
|
||||
)
|
||||
|
||||
# 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),
|
||||
path(
|
||||
"accounts/login/", LoginView.as_view(authentication_form=OTPAuthenticationForm)
|
||||
),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
# 2FA login urls
|
||||
path("", include(tf_urls)),
|
||||
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
||||
path("accounts/logout/", LogoutView.as_view(), name="logout"),
|
||||
path("hooks/<str:type>/", hooks.HookList.as_view(), name="hooks"),
|
||||
path("hooks/<str:type>/create/", hooks.HookCreate.as_view(), name="hook_create"),
|
||||
path(
|
||||
@@ -62,7 +77,28 @@ urlpatterns = [
|
||||
f"{settings.HOOK_PATH}/<str:hook_name>/", hooks.HookAPI.as_view(), name="hook"
|
||||
),
|
||||
path(
|
||||
"callbacks/<str:type>/<str:pk>/",
|
||||
f"{settings.ASSET_PATH}/<str:webhook_id>/",
|
||||
assets.AssetGroupAPI.as_view(),
|
||||
name="asset",
|
||||
),
|
||||
path("signals/<str:type>/", signals.SignalList.as_view(), name="signals"),
|
||||
path(
|
||||
"signals/<str:type>/create/",
|
||||
signals.SignalCreate.as_view(),
|
||||
name="signal_create",
|
||||
),
|
||||
path(
|
||||
"signals/<str:type>/update/<str:pk>/",
|
||||
signals.SignalUpdate.as_view(),
|
||||
name="signal_update",
|
||||
),
|
||||
path(
|
||||
"signals/<str:type>/delete/<str:pk>/",
|
||||
signals.SignalDelete.as_view(),
|
||||
name="signal_delete",
|
||||
),
|
||||
path(
|
||||
"callbacks/<str:type>/<str:object_type>/<str:object_id>/",
|
||||
callbacks.Callbacks.as_view(),
|
||||
name="callbacks",
|
||||
),
|
||||
@@ -97,6 +133,11 @@ urlpatterns = [
|
||||
trades.TradeUpdate.as_view(),
|
||||
name="trade_update",
|
||||
),
|
||||
path(
|
||||
"trades/<str:type>/view/<str:account_id>/<str:trade_id>/",
|
||||
trades.TradeAction.as_view(),
|
||||
name="trade_action",
|
||||
),
|
||||
path(
|
||||
"trades/<str:type>/delete/<str:pk>/",
|
||||
trades.TradeDelete.as_view(),
|
||||
@@ -107,6 +148,7 @@ urlpatterns = [
|
||||
trades.TradeDeleteAll.as_view(),
|
||||
name="trade_delete_all",
|
||||
),
|
||||
path("profit/<str:type>/", profit.Profit.as_view(), name="profit"),
|
||||
path("positions/<str:type>/", positions.Positions.as_view(), name="positions"),
|
||||
path(
|
||||
"positions/<str:type>/<str:account_id>/",
|
||||
@@ -114,7 +156,12 @@ urlpatterns = [
|
||||
name="positions",
|
||||
),
|
||||
path(
|
||||
"positions/<str:type>/<str:account_id>/<str:asset_id>/",
|
||||
"positions/close/<str:account_id>/<str:side>/<str:symbol>/",
|
||||
positions.PositionAction.as_view(),
|
||||
name="position_action",
|
||||
),
|
||||
path(
|
||||
"positions/<str:type>/<str:account_id>/<str:symbol>/",
|
||||
positions.PositionAction.as_view(),
|
||||
name="position_action",
|
||||
),
|
||||
@@ -136,4 +183,149 @@ urlpatterns = [
|
||||
strategies.StrategyDelete.as_view(),
|
||||
name="strategy_delete",
|
||||
),
|
||||
path(
|
||||
"trading_times/<str:type>/",
|
||||
limits.TradingTimeList.as_view(),
|
||||
name="tradingtimes",
|
||||
),
|
||||
path(
|
||||
"trading_times/<str:type>/create/",
|
||||
limits.TradingTimeCreate.as_view(),
|
||||
name="tradingtime_create",
|
||||
),
|
||||
path(
|
||||
"trading_times/<str:type>/update/<str:pk>/",
|
||||
limits.TradingTimeUpdate.as_view(),
|
||||
name="tradingtime_update",
|
||||
),
|
||||
path(
|
||||
"trading_times/<str:type>/delete/<str:pk>/",
|
||||
limits.TradingTimeDelete.as_view(),
|
||||
name="tradingtime_delete",
|
||||
),
|
||||
path(
|
||||
"trend_directions/<str:strategy_id>/flip/<str:symbol>/",
|
||||
limits.TrendDirectionFlip.as_view(),
|
||||
name="trenddirection_flip",
|
||||
),
|
||||
path(
|
||||
"trend_directions/<str:strategy_id>/delete/<str:symbol>/",
|
||||
limits.TrendDirectionDelete.as_view(),
|
||||
name="trenddirection_delete",
|
||||
),
|
||||
path(
|
||||
"trend_directions/<str:type>/view/<str:strategy_id>/",
|
||||
limits.TrendDirectionList.as_view(),
|
||||
name="trenddirections",
|
||||
),
|
||||
path(
|
||||
"notifications/<str:type>/update/",
|
||||
notifications.NotificationsUpdate.as_view(),
|
||||
name="notifications_update",
|
||||
),
|
||||
# Risks
|
||||
path(
|
||||
"risk/<str:type>/",
|
||||
risk.RiskList.as_view(),
|
||||
name="risks",
|
||||
),
|
||||
path(
|
||||
"risk/<str:type>/create/",
|
||||
risk.RiskCreate.as_view(),
|
||||
name="risk_create",
|
||||
),
|
||||
path(
|
||||
"risk/<str:type>/update/<str:pk>/",
|
||||
risk.RiskUpdate.as_view(),
|
||||
name="risk_update",
|
||||
),
|
||||
path(
|
||||
"risk/<str:type>/delete/<str:pk>/",
|
||||
risk.RiskDelete.as_view(),
|
||||
name="risk_delete",
|
||||
),
|
||||
# Asset Groups
|
||||
path(
|
||||
"group/<str:type>/",
|
||||
assets.AssetGroupList.as_view(),
|
||||
name="assetgroups",
|
||||
),
|
||||
path(
|
||||
"group/<str:type>/create/",
|
||||
assets.AssetGroupCreate.as_view(),
|
||||
name="assetgroup_create",
|
||||
),
|
||||
path(
|
||||
"group/<str:type>/update/<str:pk>/",
|
||||
assets.AssetGroupUpdate.as_view(),
|
||||
name="assetgroup_update",
|
||||
),
|
||||
path(
|
||||
"group/<str:type>/delete/<str:pk>/",
|
||||
assets.AssetGroupDelete.as_view(),
|
||||
name="assetgroup_delete",
|
||||
),
|
||||
# Asset Rules
|
||||
path(
|
||||
"assetrule/<str:type>/<str:group>/",
|
||||
assets.AssetRuleList.as_view(),
|
||||
name="assetrules",
|
||||
),
|
||||
path(
|
||||
"assetrule/<str:type>/create/<str:group>/",
|
||||
assets.AssetRuleCreate.as_view(),
|
||||
name="assetrule_create",
|
||||
),
|
||||
path(
|
||||
"assetrule/<str:type>/update/<str:group>/<str:pk>/",
|
||||
assets.AssetRuleUpdate.as_view(),
|
||||
name="assetrule_update",
|
||||
),
|
||||
path(
|
||||
"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)
|
||||
|
||||
107
core/admin.py
107
core/admin.py
@@ -1,11 +1,22 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django_otp.admin import OTPAdminSite
|
||||
|
||||
from .forms import CustomUserCreationForm
|
||||
from .models import Plan, Session, User
|
||||
from .models import ( # AssetRestriction,; Plan,; Session,
|
||||
Account,
|
||||
AssetGroup,
|
||||
Callback,
|
||||
Hook,
|
||||
NotificationSettings,
|
||||
RiskModel,
|
||||
Signal,
|
||||
Strategy,
|
||||
Trade,
|
||||
TradingTime,
|
||||
User,
|
||||
)
|
||||
|
||||
admin.site.__class__ = OTPAdminSite
|
||||
# admin.site.__class__ = OTPAdminSite
|
||||
|
||||
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
|
||||
# for model_cls, model_admin in admin.site._registry.items():
|
||||
@@ -14,27 +25,91 @@ admin.site.__class__ = OTPAdminSite
|
||||
|
||||
# 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",)},
|
||||
"Billing information",
|
||||
{"fields": ("billing_provider_id", "customer_id", "stripe_id")},
|
||||
),
|
||||
(
|
||||
"Payment information",
|
||||
{
|
||||
"fields": (
|
||||
"plans",
|
||||
"last_payment",
|
||||
# (
|
||||
# "Payment information",
|
||||
# {
|
||||
# "fields": (
|
||||
# # "plans",
|
||||
# "last_payment",
|
||||
# )
|
||||
# },
|
||||
# ),
|
||||
)
|
||||
},
|
||||
),
|
||||
|
||||
|
||||
class AccountAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "exchange", "sandbox", "currency")
|
||||
|
||||
|
||||
class HookAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "hook", "received")
|
||||
|
||||
|
||||
class SignalAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "signal", "hook", "direction", "received", "type")
|
||||
|
||||
|
||||
class TradeAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "account", "symbol", "amount", "direction", "status")
|
||||
|
||||
|
||||
class CallbackAdmin(admin.ModelAdmin):
|
||||
list_display = ("hook", "signal", "title", "symbol", "price")
|
||||
|
||||
|
||||
class TradingTimeAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "start_day", "start_time", "end_day", "end_time")
|
||||
|
||||
|
||||
class StrategyAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "description", "account", "enabled")
|
||||
|
||||
|
||||
class NotificationSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "ntfy_topic", "ntfy_url")
|
||||
|
||||
|
||||
class RiskModelAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"user",
|
||||
"name",
|
||||
"description",
|
||||
"max_loss_percent",
|
||||
"max_risk_percent",
|
||||
"max_open_trades",
|
||||
"max_open_trades_per_symbol",
|
||||
)
|
||||
|
||||
|
||||
class AssetGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "name", "description", "webhook_id")
|
||||
|
||||
|
||||
# class AssetRestrictionAdmin(admin.ModelAdmin):
|
||||
# list_display = ("user", "name", "description", "webhook_id", "group")
|
||||
|
||||
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
admin.site.register(Plan)
|
||||
admin.site.register(Session)
|
||||
# admin.site.register(Plan)
|
||||
# admin.site.register(Session)
|
||||
|
||||
admin.site.register(Account, AccountAdmin)
|
||||
admin.site.register(Hook, HookAdmin)
|
||||
admin.site.register(Signal, SignalAdmin)
|
||||
admin.site.register(Trade, TradeAdmin)
|
||||
admin.site.register(Callback, CallbackAdmin)
|
||||
admin.site.register(TradingTime, TradingTimeAdmin)
|
||||
admin.site.register(Strategy, StrategyAdmin)
|
||||
admin.site.register(NotificationSettings, NotificationSettingsAdmin)
|
||||
admin.site.register(RiskModel, RiskModelAdmin)
|
||||
admin.site.register(AssetGroup, AssetGroupAdmin)
|
||||
# admin.site.register(AssetRestriction, AssetRestrictionAdmin)
|
||||
|
||||
@@ -1,71 +1,241 @@
|
||||
from pydantic import ValidationError
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from alpaca.common.exceptions import APIError
|
||||
from glom import glom
|
||||
from oandapyV20.exceptions import V20Error
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
|
||||
from core.lib import schemas
|
||||
from core.util import logs
|
||||
|
||||
# Return error if the schema for the message type is not found
|
||||
STRICT_VALIDATION = False
|
||||
|
||||
class BaseExchange(object):
|
||||
# Raise exception if the conversion schema is not found
|
||||
STRICT_CONVERSION = False
|
||||
|
||||
# TODO: Set them to True when all message types are implemented
|
||||
|
||||
log = logs.get_logger("exchanges")
|
||||
|
||||
|
||||
class NoSchema(Exception):
|
||||
"""
|
||||
Raised when:
|
||||
- The schema for the message type is not found
|
||||
- The conversion schema is not found
|
||||
- There is no schema library for the exchange
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchMethod(Exception):
|
||||
"""
|
||||
Exchange library has no such method.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GenericAPIError(Exception):
|
||||
"""
|
||||
Generic API error.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ExchangeError(Exception):
|
||||
"""
|
||||
Exchange error.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def is_camel_case(s):
|
||||
return s != s.lower() and s != s.upper() and "_" not in s
|
||||
|
||||
|
||||
def snake_to_camel(word):
|
||||
if is_camel_case(word):
|
||||
return word
|
||||
return "".join(x.capitalize() or "_" for x in word.split("_"))
|
||||
|
||||
|
||||
class BaseExchange(ABC):
|
||||
def __init__(self, account):
|
||||
name = self.__class__.__name__
|
||||
self.name = name.replace("Exchange", "").lower()
|
||||
self.account = account
|
||||
self.log = logs.get_logger(name)
|
||||
self.client = None
|
||||
|
||||
self.set_schema()
|
||||
self.connect()
|
||||
|
||||
def set_schema(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def connect(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def call(self, method, *args, **kwargs) -> (bool, dict):
|
||||
@property
|
||||
def schema(self):
|
||||
"""
|
||||
Get the schema library for the exchange.
|
||||
"""
|
||||
# Does the schemas library have a library for this exchange name?
|
||||
if hasattr(schemas, f"{self.name}_s"):
|
||||
schema_instance = getattr(schemas, f"{self.name}_s")
|
||||
else:
|
||||
log.error(f"No schema library for {self.name}")
|
||||
raise Exception(f"No schema library for exchange {self.name}")
|
||||
|
||||
return schema_instance
|
||||
|
||||
def get_schema(self, method, convert=False):
|
||||
if isinstance(method, str):
|
||||
to_camel = snake_to_camel(method)
|
||||
else:
|
||||
to_camel = snake_to_camel(method.__class__.__name__)
|
||||
if convert:
|
||||
to_camel = f"{to_camel}Schema"
|
||||
# if hasattr(self.schema, method):
|
||||
# schema = getattr(self.schema, method)
|
||||
if hasattr(self.schema, to_camel):
|
||||
schema = getattr(self.schema, to_camel)
|
||||
else:
|
||||
raise NoSchema(f"Could not get schema: {to_camel}")
|
||||
return schema
|
||||
|
||||
def call_method(self, method, *args, **kwargs):
|
||||
"""
|
||||
Get a method from the exchange library.
|
||||
"""
|
||||
if hasattr(self.client, method):
|
||||
try:
|
||||
response = getattr(self.client, method)(*args, **kwargs)
|
||||
if isinstance(response, list):
|
||||
response = {"itemlist": response}
|
||||
if method not in self.schema:
|
||||
self.log.error(f"Method cannot be validated: {method}")
|
||||
self.log.debug(f"Response: {response}")
|
||||
return (False, f"Method cannot be validated: {method}")
|
||||
return (True, self.schema[method](**response).dict())
|
||||
except ValidationError as e:
|
||||
self.log.error(f"Could not validate response: {e}")
|
||||
return (False, e)
|
||||
except Exception as e:
|
||||
self.log.error(f"Error calling {method}: {e}")
|
||||
return (False, e)
|
||||
return response
|
||||
else:
|
||||
return (False, "No such method")
|
||||
raise NoSuchMethod
|
||||
|
||||
def convert_spec(self, response, method):
|
||||
"""
|
||||
Convert an API response to the requested spec.
|
||||
:raises NoSchema: If the conversion schema is not found
|
||||
"""
|
||||
schema = self.get_schema(method, convert=True)
|
||||
|
||||
# Use glom to convert the response to the schema
|
||||
converted = glom(response, schema)
|
||||
return converted
|
||||
|
||||
def validate_response(self, response, method):
|
||||
schema = self.get_schema(method)
|
||||
# Return a dict of the validated response
|
||||
try:
|
||||
response_valid = schema(**response).dict()
|
||||
except ValidationError as e:
|
||||
log.error(f"Error validating {method} response: {response}")
|
||||
log.error(f"Errors: {e}")
|
||||
raise GenericAPIError("Error validating response")
|
||||
return response_valid
|
||||
|
||||
def call(self, method, *args, **kwargs):
|
||||
"""
|
||||
Call the exchange API and validate the response
|
||||
:raises NoSchema: If the method is not in the schema mapping
|
||||
:raises ValidationError: If the response cannot be validated
|
||||
"""
|
||||
try:
|
||||
response = self.call_method(method, *args, **kwargs)
|
||||
except (APIError, V20Error) as e:
|
||||
log.error(f"Error calling method {method}: {e}")
|
||||
raise GenericAPIError(e)
|
||||
try:
|
||||
response_valid = self.validate_response(response, method)
|
||||
except NoSchema as e:
|
||||
log.error(f"{e} - {response}")
|
||||
response_valid = response
|
||||
# Convert the response to a format that we can use
|
||||
try:
|
||||
response_converted = self.convert_spec(response_valid, method)
|
||||
except NoSchema as e:
|
||||
log.error(f"{e} - {response}")
|
||||
response_converted = response_valid
|
||||
|
||||
# return (True, response_converted)
|
||||
return response_converted
|
||||
|
||||
# except Exception as e:
|
||||
# log.error(f"Error calling method: {e}")
|
||||
# raise GenericAPIError(e)
|
||||
|
||||
@abstractmethod
|
||||
def get_account(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def extract_instrument(self, instruments, instrument):
|
||||
for x in instruments["itemlist"]:
|
||||
if x["name"] == instrument:
|
||||
return x
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def get_currencies(self, symbols):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instruments(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_supported_assets(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_balance(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_market_value(self, symbol):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def post_trade(self, trade):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_trade(self, trade_id, units=None):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_trade(self, trade_id):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update_trade(self, trade):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_trade(self, trade_id):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def get_position_info(self, asset_id):
|
||||
raise NotImplementedError
|
||||
@abstractmethod
|
||||
def get_position_info(self, symbol):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_all_positions(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_all_open_trades(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_position(self, side, symbol):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_all_positions(self):
|
||||
pass
|
||||
|
||||
@@ -7,21 +7,10 @@ from alpaca.trading.requests import (
|
||||
MarketOrderRequest,
|
||||
)
|
||||
|
||||
from core.exchanges import BaseExchange
|
||||
from core.lib.schemas import alpaca_s
|
||||
|
||||
ALPACA_SCHEMA_MAPPING = {
|
||||
"get_account": alpaca_s.GetAccount,
|
||||
"get_all_assets": alpaca_s.GetAllAssets,
|
||||
"get_all_positions": alpaca_s.GetAllPositions,
|
||||
"get_open_position": alpaca_s.GetOpenPosition,
|
||||
}
|
||||
from core.exchanges import BaseExchange, ExchangeError, GenericAPIError, common
|
||||
|
||||
|
||||
class AlpacaExchange(BaseExchange):
|
||||
def set_schema(self):
|
||||
self.schema = ALPACA_SCHEMA_MAPPING
|
||||
|
||||
def connect(self):
|
||||
self.client = TradingClient(
|
||||
self.account.api_key,
|
||||
@@ -33,36 +22,44 @@ class AlpacaExchange(BaseExchange):
|
||||
def get_account(self):
|
||||
return self.call("get_account")
|
||||
|
||||
def get_supported_assets(self):
|
||||
def get_instruments(self):
|
||||
request = GetAssetsRequest(status="active", asset_class="crypto")
|
||||
success, assets = self.call("get_all_assets", filter=request)
|
||||
# assets = self.client.get_all_assets(filter=request)
|
||||
if not success:
|
||||
return (success, assets)
|
||||
assets = self.call("get_all_assets", filter=request)
|
||||
return assets
|
||||
|
||||
def get_currencies(self, currencies):
|
||||
pass # TODO
|
||||
|
||||
def get_supported_assets(self):
|
||||
assets = self.get_instruments()
|
||||
assets = assets["itemlist"]
|
||||
asset_list = [x["symbol"] for x in assets if "symbol" in x]
|
||||
print("Supported symbols", asset_list)
|
||||
|
||||
return (True, asset_list)
|
||||
return asset_list
|
||||
|
||||
def get_balance(self):
|
||||
success, account_info = self.call("get_account")
|
||||
if not success:
|
||||
return (success, account_info)
|
||||
account_info = self.call("get_account")
|
||||
equity = account_info["equity"]
|
||||
try:
|
||||
balance = float(equity)
|
||||
except ValueError:
|
||||
return (False, "Invalid balance")
|
||||
raise GenericAPIError(f"Balance is not a float: {equity}")
|
||||
|
||||
return (True, balance)
|
||||
common.get_balance_hook(
|
||||
self.account.user.id,
|
||||
self.account.user.username,
|
||||
self.account.id,
|
||||
self.account.name,
|
||||
balance,
|
||||
)
|
||||
return balance
|
||||
|
||||
def get_market_value(self, symbol): # TODO: pydantic
|
||||
try:
|
||||
position = self.client.get_position(symbol)
|
||||
except APIError as e:
|
||||
self.log.error(f"Could not get market value for {symbol}: {e}")
|
||||
return False
|
||||
raise GenericAPIError(e)
|
||||
return float(position["market_value"])
|
||||
|
||||
def post_trade(self, trade): # TODO: pydantic
|
||||
@@ -72,19 +69,19 @@ class AlpacaExchange(BaseExchange):
|
||||
elif trade.direction == "sell":
|
||||
direction = OrderSide.SELL
|
||||
else:
|
||||
raise Exception("Unknown direction")
|
||||
raise ExchangeError("Unknown direction")
|
||||
|
||||
cast = {
|
||||
"symbol": trade.symbol,
|
||||
"side": direction,
|
||||
"time_in_force": TimeInForce.IOC,
|
||||
"time_in_force": TimeInForce.IOC, # TODO
|
||||
}
|
||||
if trade.amount is not None:
|
||||
cast["qty"] = trade.amount
|
||||
if trade.amount_usd is not None:
|
||||
cast["notional"] = trade.amount_usd
|
||||
if not trade.amount and not trade.amount_usd:
|
||||
return (False, "No amount specified")
|
||||
raise ExchangeError("No amount specified")
|
||||
if trade.take_profit:
|
||||
cast["take_profit"] = {"limit_price": trade.take_profit}
|
||||
if trade.stop_loss:
|
||||
@@ -101,10 +98,10 @@ class AlpacaExchange(BaseExchange):
|
||||
self.log.error(f"Error placing market order: {e}")
|
||||
trade.status = "error"
|
||||
trade.save()
|
||||
return (False, e)
|
||||
raise GenericAPIError(e)
|
||||
elif trade.type == "limit":
|
||||
if not trade.price:
|
||||
return (False, "Limit order with no price")
|
||||
raise ExchangeError("No price specified for limit order")
|
||||
cast["limit_price"] = trade.price
|
||||
limit_order_data = LimitOrderRequest(**cast)
|
||||
try:
|
||||
@@ -113,19 +110,24 @@ class AlpacaExchange(BaseExchange):
|
||||
self.log.error(f"Error placing limit order: {e}")
|
||||
trade.status = "error"
|
||||
trade.save()
|
||||
return (False, e)
|
||||
raise GenericAPIError(e)
|
||||
|
||||
else:
|
||||
raise Exception("Unknown trade type")
|
||||
raise ExchangeError("Unknown trade type")
|
||||
trade.response = order
|
||||
trade.status = "posted"
|
||||
trade.order_id = order["id"]
|
||||
trade.client_order_id = order["client_order_id"]
|
||||
trade.save()
|
||||
return (True, order)
|
||||
return order
|
||||
|
||||
def close_trade(self, trade_id, units=None): # TODO
|
||||
"""
|
||||
Close a trade
|
||||
"""
|
||||
|
||||
def get_trade(self, trade_id):
|
||||
pass
|
||||
pass # TODO
|
||||
|
||||
def update_trade(self, trade):
|
||||
pass
|
||||
@@ -133,20 +135,23 @@ class AlpacaExchange(BaseExchange):
|
||||
def cancel_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_position_info(self, asset_id):
|
||||
success, position = self.call("get_open_position", asset_id)
|
||||
if not success:
|
||||
return (success, position)
|
||||
return (True, position)
|
||||
def get_position_info(self, symbol):
|
||||
position = self.call("get_open_position", symbol)
|
||||
return position # TODO: check
|
||||
|
||||
def get_all_positions(self):
|
||||
items = []
|
||||
success, positions = self.call("get_all_positions")
|
||||
if not success:
|
||||
return (success, positions)
|
||||
response = self.call("get_all_positions")
|
||||
|
||||
for item in positions["itemlist"]:
|
||||
for item in response["itemlist"]:
|
||||
item["account"] = self.account.name
|
||||
item["account_id"] = self.account.id
|
||||
item["unrealized_pl"] = float(item["unrealized_pl"])
|
||||
items.append(item)
|
||||
return (True, items)
|
||||
return items
|
||||
|
||||
def close_position(self, side, symbol):
|
||||
pass # TODO
|
||||
|
||||
def close_all_positions(self):
|
||||
pass # TODO
|
||||
|
||||
101
core/exchanges/common.py
Normal file
101
core/exchanges/common.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from decimal import Decimal as D
|
||||
|
||||
from core.exchanges import GenericAPIError
|
||||
from core.lib.elastic import store_msg
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
def get_balance_hook(user_id, user_name, account_id, account_name, balance):
|
||||
"""
|
||||
Called every time the balance is fetched on an account.
|
||||
Store this into Elasticsearch.
|
||||
"""
|
||||
store_msg(
|
||||
"balances",
|
||||
{
|
||||
"user_id": user_id,
|
||||
"user_name": user_name,
|
||||
"account_id": account_id,
|
||||
"account_name": account_name,
|
||||
"balance": balance,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_pair(account, base, quote, invert=False):
|
||||
"""
|
||||
Get the pair for the given account and currencies.
|
||||
:param account: Account object
|
||||
:param base: Base currency
|
||||
:param quote: Quote currency
|
||||
:param invert: Invert the pair
|
||||
:return: currency symbol, e.g. BTC_USD, BTC/USD, etc.
|
||||
"""
|
||||
if account.exchange == "alpaca":
|
||||
separator = "/"
|
||||
elif account.exchange == "oanda":
|
||||
separator = "_"
|
||||
else:
|
||||
separator = "_"
|
||||
|
||||
# Flip the pair if needed
|
||||
if invert:
|
||||
symbol = f"{quote.upper()}{separator}{base.upper()}"
|
||||
else:
|
||||
symbol = f"{base.upper()}{separator}{quote.upper()}"
|
||||
# Check it exists
|
||||
if symbol not in account.supported_symbols:
|
||||
return 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.
|
||||
:param direction: Direction of the trade
|
||||
:param account: Account object
|
||||
:param amount: Amount to convert
|
||||
:param from_currency: Currency to convert from
|
||||
:param to_currency: Currency to convert to
|
||||
:return: Converted amount
|
||||
"""
|
||||
# If we're converting to the same currency, just return the amount
|
||||
if from_currency == to_currency:
|
||||
return amount
|
||||
inverted = False
|
||||
|
||||
# This is needed because OANDA has different values for bid and ask
|
||||
if direction == "buy":
|
||||
price_index = "bids"
|
||||
elif direction == "sell":
|
||||
price_index = "asks"
|
||||
symbol = get_pair(account, from_currency, to_currency)
|
||||
if not symbol:
|
||||
symbol = get_pair(account, from_currency, to_currency, invert=True)
|
||||
inverted = True
|
||||
|
||||
# Bit of a hack but it works
|
||||
if not symbol:
|
||||
log.error(f"Could not find symbol for {from_currency} -> {to_currency}")
|
||||
raise Exception("Could not find symbol")
|
||||
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:
|
||||
price = D(1.0) / price
|
||||
|
||||
# Convert the amount to the destination currency
|
||||
converted = D(amount) * price
|
||||
return converted
|
||||
354
core/exchanges/convert.py
Normal file
354
core/exchanges/convert.py
Normal file
@@ -0,0 +1,354 @@
|
||||
from decimal import Decimal as D
|
||||
|
||||
from core.exchanges import GenericAPIError
|
||||
from core.models import Account
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
# Separate module to prevent circular import from
|
||||
# models -> exchanges -> common -> models
|
||||
# Since we need Account here to look up missing prices
|
||||
|
||||
|
||||
def get_price(account, direction, symbol):
|
||||
"""
|
||||
Get the price for a given symbol.
|
||||
:param account: Account object
|
||||
:param direction: direction of the trade
|
||||
:param symbol: symbol
|
||||
:return: price of bid for buys, price of ask for sells
|
||||
"""
|
||||
if direction == "buy":
|
||||
price_index = "bids"
|
||||
elif direction == "sell":
|
||||
price_index = "asks"
|
||||
try:
|
||||
prices = account.client.get_currencies([symbol])
|
||||
except GenericAPIError as e:
|
||||
log.error(f"Error getting currencies: {e}")
|
||||
return None
|
||||
price = D(prices["prices"][0][price_index][0]["price"])
|
||||
return price
|
||||
|
||||
|
||||
def side_to_direction(side, flip_direction=False):
|
||||
"""
|
||||
Convert a side to a direction.
|
||||
:param side: Side, e.g. long, short
|
||||
:param flip_direction: Flip the direction
|
||||
:return: Direction, e.g. buy, sell
|
||||
"""
|
||||
if side == "long":
|
||||
if flip_direction:
|
||||
return "sell"
|
||||
return "buy"
|
||||
elif side == "short":
|
||||
if flip_direction:
|
||||
return "buy"
|
||||
return "sell"
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def direction_to_side(direction, flip_side=False):
|
||||
"""
|
||||
Convert a direction to a side.
|
||||
:param direction: Direction, e.g. buy, sell
|
||||
:param flip_side: Flip the side
|
||||
:return: Side, e.g. long, short
|
||||
"""
|
||||
if direction == "buy":
|
||||
if flip_side:
|
||||
return "short"
|
||||
return "long"
|
||||
elif direction == "sell":
|
||||
if flip_side:
|
||||
return "long"
|
||||
return "short"
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def tp_price_to_percent(tp_price, side, current_price, current_units, unrealised_pl):
|
||||
"""
|
||||
Determine the percent change of the TP price from the initial price.
|
||||
Positive values indicate a profit, negative values 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 = ((initial_price - D(tp_price)) / initial_price) * 100
|
||||
|
||||
if side == "long":
|
||||
if D(tp_price) < initial_price:
|
||||
loss = True
|
||||
else:
|
||||
loss = False
|
||||
else:
|
||||
if D(tp_price) > initial_price:
|
||||
loss = True
|
||||
else:
|
||||
loss = False
|
||||
|
||||
# if we are in loss on the short side, we want to show a negative
|
||||
if loss:
|
||||
change_percent = 0 - abs(change_percent)
|
||||
else:
|
||||
change_percent = abs(change_percent)
|
||||
|
||||
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.
|
||||
Positive values indicate a loss, negative values indicate a profit.
|
||||
This may seem backwards, but it is important to note that by default,
|
||||
SL indicates a loss, and positive values should be expected.
|
||||
Negative values indicate a negative loss, so a profit.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# initial_price = D(current_price) - pl_per_unit
|
||||
|
||||
# Get the percent change of the SL price from the initial price.
|
||||
change_percent = ((initial_price - D(sl_price)) / initial_price) * 100
|
||||
|
||||
# If the trade is long, the SL price will be higher than the initial price.
|
||||
# if side == "long":
|
||||
# change_percent *= -1
|
||||
|
||||
if side == "long":
|
||||
if D(sl_price) > initial_price:
|
||||
profit = True
|
||||
else:
|
||||
profit = False
|
||||
else:
|
||||
if D(sl_price) < initial_price:
|
||||
profit = True
|
||||
else:
|
||||
profit = False
|
||||
|
||||
if profit:
|
||||
change_percent = 0 - abs(change_percent)
|
||||
else:
|
||||
change_percent = abs(change_percent)
|
||||
|
||||
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.
|
||||
This works on Trade objects, which will require an additional market
|
||||
lookup to get the current price.
|
||||
"""
|
||||
if "current_price" in trade:
|
||||
current_price = trade["current_price"]
|
||||
else:
|
||||
account_id = trade["account_id"]
|
||||
account = Account.get_by_id_no_user_check(account_id)
|
||||
|
||||
current_price = get_price(account, trade["direction"], trade["symbol"])
|
||||
trade["current_price"] = current_price
|
||||
|
||||
current_units = trade["amount"]
|
||||
if "pl" in trade:
|
||||
unrealised_pl = trade["pl"]
|
||||
else:
|
||||
unrealised_pl = 0
|
||||
|
||||
if "side" in trade:
|
||||
side = trade["side"]
|
||||
direction = side_to_direction(side)
|
||||
trade["direction"] = direction
|
||||
else:
|
||||
direction = trade["direction"]
|
||||
side = direction_to_side(direction)
|
||||
trade["side"] = side
|
||||
|
||||
if "take_profit" in trade:
|
||||
if trade["take_profit"]:
|
||||
take_profit = trade["take_profit"]
|
||||
take_profit_percent = tp_price_to_percent(
|
||||
take_profit, trade["side"], current_price, current_units, unrealised_pl
|
||||
)
|
||||
|
||||
trade["take_profit_percent"] = take_profit_percent
|
||||
|
||||
if "stop_loss" in trade:
|
||||
if trade["stop_loss"]:
|
||||
stop_loss = trade["stop_loss"]
|
||||
stop_loss_percent = sl_price_to_percent(
|
||||
stop_loss, side, current_price, current_units, unrealised_pl
|
||||
)
|
||||
|
||||
trade["stop_loss_percent"] = stop_loss_percent
|
||||
|
||||
if "trailing_stop_loss" in trade:
|
||||
if trade["trailing_stop_loss"]:
|
||||
trailing_stop_loss = trade["trailing_stop_loss"]
|
||||
trailing_stop_loss_percent = sl_price_to_percent(
|
||||
trailing_stop_loss,
|
||||
trade["side"],
|
||||
current_price,
|
||||
current_units,
|
||||
unrealised_pl,
|
||||
)
|
||||
|
||||
trade["trailing_stop_loss_percent"] = trailing_stop_loss_percent
|
||||
|
||||
return trade
|
||||
|
||||
|
||||
def open_trade_to_unified_format(trade):
|
||||
"""
|
||||
Convert an open trade to a Trade-like format.
|
||||
"""
|
||||
current_price = trade["price"]
|
||||
current_units = trade["currentUnits"]
|
||||
unrealised_pl = trade["unrealizedPL"]
|
||||
side = trade["side"]
|
||||
cast = {
|
||||
"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.
|
||||
if "take_profit_usd" in trade:
|
||||
cast["take_profit_usd"] = trade["take_profit_usd"]
|
||||
if "stop_loss_usd" in trade:
|
||||
cast["stop_loss_usd"] = trade["stop_loss_usd"]
|
||||
if "trailing_stop_loss_usd" in trade:
|
||||
cast["trailing_stop_loss_usd"] = trade["trailing_stop_loss_usd"]
|
||||
if "takeProfitOrder" in trade:
|
||||
if trade["takeProfitOrder"]:
|
||||
take_profit = trade["takeProfitOrder"]["price"]
|
||||
cast["take_profit"] = take_profit
|
||||
|
||||
if "stopLossOrder" in trade:
|
||||
if trade["stopLossOrder"]:
|
||||
stop_loss = trade["stopLossOrder"]["price"]
|
||||
cast["stop_loss"] = stop_loss
|
||||
|
||||
if "trailingStopLossOrder" in trade:
|
||||
if trade["trailingStopLossOrder"]:
|
||||
trailing_stop_loss = trade["trailingStopLossOrder"]["price"]
|
||||
cast["trailing_stop_loss"] = trailing_stop_loss
|
||||
|
||||
return cast
|
||||
|
||||
|
||||
def convert_trades(open_trades):
|
||||
"""
|
||||
Convert a list of open trades into a list of Trade-like objects.
|
||||
"""
|
||||
trades = []
|
||||
for trade in open_trades:
|
||||
if "currentUnits" in trade: # Open trade
|
||||
cast = open_trade_to_unified_format(trade)
|
||||
cast = annotate_trade_tp_sl_percent(cast)
|
||||
trades.append(cast)
|
||||
else:
|
||||
cast = annotate_trade_tp_sl_percent(trade)
|
||||
trades.append(cast)
|
||||
|
||||
return trades
|
||||
57
core/exchanges/fake.py
Normal file
57
core/exchanges/fake.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from core.exchanges import BaseExchange
|
||||
|
||||
|
||||
class FakeExchange(BaseExchange):
|
||||
def call_method(self, request):
|
||||
pass
|
||||
|
||||
def connect(self):
|
||||
pass
|
||||
|
||||
def get_account(self):
|
||||
pass
|
||||
|
||||
def get_instruments(self):
|
||||
pass
|
||||
|
||||
def get_currencies(self, currencies):
|
||||
pass
|
||||
|
||||
def get_supported_assets(self, response=None):
|
||||
pass
|
||||
|
||||
def get_balance(self, return_usd=False):
|
||||
pass
|
||||
|
||||
def get_market_value(self, symbol):
|
||||
pass
|
||||
|
||||
def post_trade(self, trade):
|
||||
pass
|
||||
|
||||
def close_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def update_trade(self, trade):
|
||||
pass
|
||||
|
||||
def cancel_trade(self, trade_id):
|
||||
pass
|
||||
|
||||
def get_position_info(self, symbol):
|
||||
pass
|
||||
|
||||
def get_all_positions(self):
|
||||
pass
|
||||
|
||||
def get_all_open_trades(self):
|
||||
pass
|
||||
|
||||
def close_position(self, side, symbol):
|
||||
pass
|
||||
|
||||
def close_all_positions(self):
|
||||
pass
|
||||
82
core/exchanges/mexc.py
Normal file
82
core/exchanges/mexc.py
Normal file
File diff suppressed because one or more lines are too long
@@ -1,30 +1,19 @@
|
||||
from oandapyV20 import API
|
||||
from oandapyV20.endpoints import accounts, orders, positions, trades
|
||||
from oandapyV20.endpoints import accounts, orders, positions, pricing, trades
|
||||
|
||||
from core.exchanges import BaseExchange
|
||||
from core.lib.schemas import oanda_s
|
||||
from core.exchanges import BaseExchange, common
|
||||
from core.util import logs
|
||||
|
||||
OANDA_SCHEMA_MAPPING = {"OpenPositions": oanda_s.OpenPositions}
|
||||
log = logs.get_logger("oanda")
|
||||
|
||||
|
||||
class OANDAExchange(BaseExchange):
|
||||
def call(self, method, request):
|
||||
def call_method(self, request):
|
||||
self.client.request(request)
|
||||
response = request.response
|
||||
if isinstance(response, list):
|
||||
response = {"itemlist": response}
|
||||
if method not in self.schema:
|
||||
self.log.error(f"Method cannot be validated: {method}")
|
||||
self.log.debug(f"Response: {response}")
|
||||
return (False, f"Method cannot be validated: {method}")
|
||||
try:
|
||||
return (True, self.schema[method](**response).dict())
|
||||
except ValidationError as e:
|
||||
self.log.error(f"Could not validate response: {e}")
|
||||
return (False, e)
|
||||
|
||||
def set_schema(self):
|
||||
self.schema = OANDA_SCHEMA_MAPPING
|
||||
return response
|
||||
|
||||
def connect(self):
|
||||
self.client = API(access_token=self.account.api_secret)
|
||||
@@ -32,50 +21,187 @@ class OANDAExchange(BaseExchange):
|
||||
|
||||
def get_account(self):
|
||||
r = accounts.AccountDetails(self.account_id)
|
||||
self.client.request(r)
|
||||
return r.response
|
||||
return self.call(r)
|
||||
|
||||
def get_supported_assets(self):
|
||||
return False
|
||||
def get_instruments(self):
|
||||
r = accounts.AccountInstruments(accountID=self.account_id)
|
||||
response = self.call(r)
|
||||
return response
|
||||
|
||||
def get_balance(self):
|
||||
raise NotImplementedError
|
||||
def get_currencies(self, currencies):
|
||||
params = {"instruments": ",".join(currencies)}
|
||||
r = pricing.PricingInfo(accountID=self.account_id, params=params)
|
||||
response = self.call(r)
|
||||
return response
|
||||
|
||||
def get_supported_assets(self, response=None):
|
||||
if not response:
|
||||
response = self.get_instruments()
|
||||
return [x["name"] for x in response["itemlist"]]
|
||||
|
||||
def get_balance(self, return_usd=False):
|
||||
r = accounts.AccountSummary(self.account_id)
|
||||
response = self.call(r)
|
||||
balance = float(response["balance"])
|
||||
currency = response["currency"]
|
||||
balance_usd = common.to_currency("sell", self.account, balance, currency, "USD")
|
||||
|
||||
common.get_balance_hook(
|
||||
self.account.user.id,
|
||||
self.account.user.username,
|
||||
self.account.id,
|
||||
self.account.name,
|
||||
balance_usd,
|
||||
)
|
||||
if return_usd:
|
||||
return balance_usd
|
||||
return balance
|
||||
|
||||
def get_market_value(self, symbol):
|
||||
raise NotImplementedError
|
||||
|
||||
def post_trade(self, trade):
|
||||
raise NotImplementedError
|
||||
r = orders.OrderCreate(accountID, data=data)
|
||||
self.client.request(r)
|
||||
return r.response
|
||||
if trade.direction == "sell":
|
||||
amount = -trade.amount
|
||||
else:
|
||||
amount = trade.amount
|
||||
data = {
|
||||
"order": {
|
||||
# "price": "1.5000", - added later
|
||||
"timeInForce": trade.time_in_force.upper(),
|
||||
"instrument": trade.symbol,
|
||||
"units": str(amount),
|
||||
"type": trade.type.upper(),
|
||||
"positionFill": "DEFAULT",
|
||||
}
|
||||
}
|
||||
if trade.stop_loss is not None:
|
||||
data["order"]["stopLossOnFill"] = {
|
||||
"timeInForce": "GTC",
|
||||
"price": str(trade.stop_loss),
|
||||
}
|
||||
if trade.take_profit is not None:
|
||||
data["order"]["takeProfitOnFill"] = {"price": str(trade.take_profit)}
|
||||
if trade.price is not None:
|
||||
if trade.type == "limit":
|
||||
data["order"]["price"] = str(trade.price)
|
||||
elif trade.type == "market":
|
||||
data["order"]["priceBound"] = str(trade.price)
|
||||
if trade.trailing_stop_loss is not None:
|
||||
data["order"]["trailingStopLossOnFill"] = {
|
||||
"distance": str(trade.trailing_stop_loss),
|
||||
"timeInForce": "GTC",
|
||||
}
|
||||
r = orders.OrderCreate(self.account_id, data=data)
|
||||
response = self.call(r)
|
||||
trade.response = response
|
||||
trade.status = "posted"
|
||||
trade.order_id = str(int(response["id"]) + 1)
|
||||
trade.client_order_id = response["requestID"]
|
||||
trade.save()
|
||||
return response
|
||||
|
||||
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.
|
||||
"""
|
||||
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):
|
||||
r = accounts.TradeDetails(accountID=self.account_id, tradeID=trade_id)
|
||||
self.client.request(r)
|
||||
return r.response
|
||||
# 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
|
||||
|
||||
def get_position_info(self, asset_id):
|
||||
r = positions.PositionDetails(self.account_id, asset_id)
|
||||
self.client.request(r)
|
||||
return r.response
|
||||
def get_position_info(self, symbol):
|
||||
r = positions.PositionDetails(self.account_id, symbol)
|
||||
response = self.call(r)
|
||||
response["account"] = self.account.name
|
||||
response["account_id"] = self.account.id
|
||||
return response
|
||||
|
||||
def get_all_positions(self):
|
||||
items = []
|
||||
r = positions.OpenPositions(accountID=self.account_id)
|
||||
success, response = self.call("OpenPositions", r)
|
||||
if not success:
|
||||
return (success, response)
|
||||
response = self.call(r)
|
||||
|
||||
print("Positions", response)
|
||||
return (True, [])
|
||||
for item in response["itemlist"]:
|
||||
item["account"] = self.account.name
|
||||
item["account_id"] = self.account.id
|
||||
item["unrealized_pl"] = float(item["unrealized_pl"])
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
def get_all_open_trades(self):
|
||||
r = trades.OpenTrades(accountID=self.account_id)
|
||||
return self.call(r)["itemlist"]
|
||||
|
||||
def close_position(self, side, symbol):
|
||||
data = {
|
||||
f"{side}Units": "ALL",
|
||||
}
|
||||
r = positions.PositionClose(
|
||||
accountID=self.account_id, instrument=symbol, data=data
|
||||
)
|
||||
response = self.call(r)
|
||||
return response
|
||||
|
||||
def close_all_positions(self):
|
||||
all_positions = self.get_all_positions()
|
||||
responses = []
|
||||
for position in all_positions:
|
||||
side = position["side"]
|
||||
symbol = position["symbol"]
|
||||
data = {
|
||||
f"{side}Units": "ALL",
|
||||
}
|
||||
r = positions.PositionClose(
|
||||
accountID=self.account_id, instrument=symbol, data=data
|
||||
)
|
||||
response = self.call(r)
|
||||
responses.append(response)
|
||||
return responses
|
||||
|
||||
376
core/forms.py
376
core/forms.py
@@ -1,10 +1,26 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.forms import ModelForm
|
||||
from mixins.restrictions import RestrictedFormMixin
|
||||
|
||||
from .models import Account, Hook, Strategy, Trade, User
|
||||
from .models import ( # AssetRestriction,
|
||||
Account,
|
||||
ActiveManagementPolicy,
|
||||
AssetGroup,
|
||||
AssetRule,
|
||||
Hook,
|
||||
NotificationSettings,
|
||||
OrderSettings,
|
||||
RiskModel,
|
||||
Signal,
|
||||
Strategy,
|
||||
Trade,
|
||||
TradingTime,
|
||||
User,
|
||||
)
|
||||
|
||||
# Create your forms here.
|
||||
# flake8: noqa: E501
|
||||
|
||||
|
||||
class NewUserForm(UserCreationForm):
|
||||
@@ -35,17 +51,39 @@ class CustomUserCreationForm(UserCreationForm):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class HookForm(ModelForm):
|
||||
class HookForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Hook
|
||||
fields = (
|
||||
"name",
|
||||
"hook",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the hook. Informational only.",
|
||||
"hook": "The URL slug to use for the hook. Make it unique.",
|
||||
}
|
||||
|
||||
|
||||
class SignalForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Signal
|
||||
fields = (
|
||||
"name",
|
||||
"signal",
|
||||
"hook",
|
||||
"type",
|
||||
"direction",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the signal. Informational only.",
|
||||
"signal": "The name of the signal in Drakdoo. Copy it from there.",
|
||||
"hook": "The hook this signal belongs to.",
|
||||
"type": "Whether the signal is used for entering or exiting trades, or determining the trend.",
|
||||
"direction": "The direction of the signal. This is used to determine if the signal is a buy or sell.",
|
||||
}
|
||||
|
||||
|
||||
class AccountForm(ModelForm):
|
||||
class AccountForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = (
|
||||
@@ -53,40 +91,352 @@ class AccountForm(ModelForm):
|
||||
"exchange",
|
||||
"api_key",
|
||||
"api_secret",
|
||||
"initial_balance",
|
||||
"sandbox",
|
||||
"enabled",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the account. Informational only.",
|
||||
"exchange": "The exchange to use for this account.",
|
||||
"api_key": "The API key or username for the account.",
|
||||
"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.",
|
||||
"initial_balance": "The initial balance of the account.",
|
||||
}
|
||||
|
||||
|
||||
class StrategyForm(ModelForm):
|
||||
class StrategyForm(RestrictedFormMixin, ModelForm):
|
||||
fieldargs = {
|
||||
"entry_signals": {"type": "entry"},
|
||||
"exit_signals": {"type": "exit"},
|
||||
"trend_signals": {"type": "trend"},
|
||||
}
|
||||
|
||||
# Filter for enabled accounts
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StrategyForm, self).__init__(*args, **kwargs)
|
||||
self.fields["account"].queryset = Account.objects.filter(enabled=True)
|
||||
|
||||
class Meta:
|
||||
model = Strategy
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"account",
|
||||
"hooks",
|
||||
"asset_group",
|
||||
"risk_model",
|
||||
"trading_times",
|
||||
"order_settings",
|
||||
"entry_signals",
|
||||
"exit_signals",
|
||||
"trend_signals",
|
||||
"signal_trading_enabled",
|
||||
"active_management_enabled",
|
||||
"active_management_policy",
|
||||
"enabled",
|
||||
"take_profit_percent",
|
||||
"stop_loss_percent",
|
||||
"price_slippage_percent",
|
||||
"trade_size_percent",
|
||||
)
|
||||
|
||||
hooks = forms.ModelMultipleChoiceField(
|
||||
queryset=Hook.objects.all(), widget=forms.CheckboxSelectMultiple
|
||||
help_texts = {
|
||||
"name": "Name of the strategy. Informational only.",
|
||||
"description": "Description of the strategy. Informational only.",
|
||||
"account": "The account to use for this strategy.",
|
||||
"asset_group": "Asset groups determine which pairs can be traded.",
|
||||
"risk_model": "The risk model to use for this strategy. Highly recommended.",
|
||||
"trading_times": "When the strategy will place new trades.",
|
||||
"order_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.",
|
||||
}
|
||||
|
||||
entry_signals = forms.ModelMultipleChoiceField(
|
||||
queryset=Signal.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["entry_signals"],
|
||||
required=False,
|
||||
)
|
||||
exit_signals = forms.ModelMultipleChoiceField(
|
||||
queryset=Signal.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["exit_signals"],
|
||||
required=False,
|
||||
)
|
||||
trend_signals = forms.ModelMultipleChoiceField(
|
||||
queryset=Signal.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
help_text=Meta.help_texts["trend_signals"],
|
||||
required=False,
|
||||
)
|
||||
trading_times = forms.ModelMultipleChoiceField(
|
||||
queryset=TradingTime.objects.all(), widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
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(
|
||||
[
|
||||
"You cannot bet against yourself. Do not use the same signal for entry and exit."
|
||||
]
|
||||
)
|
||||
for exit in exit_signals.all():
|
||||
if exit in entry_signals.all():
|
||||
self._errors["exit_signals"] = self.error_class(
|
||||
[
|
||||
"You cannot bet against yourself. Do not use the same signal for entry and exit."
|
||||
]
|
||||
)
|
||||
# Get all the directions for entry and exit signals
|
||||
entries_set = set([x.direction for x in entry_signals.all()])
|
||||
exits_set = set([x.direction for x in exit_signals.all()])
|
||||
|
||||
class TradeForm(ModelForm):
|
||||
# Make sure both fields are filled before we check this
|
||||
if entries_set and exits_set:
|
||||
# Combine them into one set
|
||||
check_set = set()
|
||||
check_set.update(entries_set, exits_set)
|
||||
|
||||
# If the length is 1, they are all the same direction
|
||||
if len(check_set) == 1:
|
||||
self._errors["entry_signals"] = self.error_class(
|
||||
[
|
||||
"You cannot have entry and exit signals that are the same direction. At least one must be opposing."
|
||||
]
|
||||
)
|
||||
self._errors["exit_signals"] = self.error_class(
|
||||
[
|
||||
"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):
|
||||
class Meta:
|
||||
model = Trade
|
||||
fields = (
|
||||
"account",
|
||||
"symbol",
|
||||
"type",
|
||||
"time_in_force",
|
||||
"amount",
|
||||
"price",
|
||||
"stop_loss",
|
||||
"trailing_stop_loss",
|
||||
"take_profit",
|
||||
"direction",
|
||||
)
|
||||
help_texts = {
|
||||
"account": "The account to use for this trade.",
|
||||
"symbol": "The symbol to trade.",
|
||||
"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.",
|
||||
"amount": "The amount to trade in the quote currency (the second part of the symbol if applicable, otherwise your account base currency).",
|
||||
"price": "The price to trade at. Sets this price as the trigger price for limit orders. Sets a price bound (if supported) for market orders.",
|
||||
"stop_loss": "The stop loss price. This will be set at the specified price.",
|
||||
"trailing_stop_loss": "The trailing stop loss price. This will be set at the specified price and will follow the price as it moves in your favor.",
|
||||
"take_profit": "The take profit price. This will be set at the specified price.",
|
||||
"direction": "The direction of the trade. This is used to determine if the trade is a buy or sell.",
|
||||
}
|
||||
|
||||
|
||||
class TradingTimeForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = TradingTime
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"start_day",
|
||||
"start_time",
|
||||
"end_day",
|
||||
"end_time",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the trading time. Informational only.",
|
||||
"description": "Description of the trading time. Informational only.",
|
||||
"start_day": "The day of the week to start trading.",
|
||||
"start_time": "The time of day to start trading.",
|
||||
"end_day": "The day of the week to stop trading.",
|
||||
"end_time": "The time of day to stop trading.",
|
||||
}
|
||||
|
||||
|
||||
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = NotificationSettings
|
||||
fields = (
|
||||
"ntfy_topic",
|
||||
"ntfy_url",
|
||||
)
|
||||
help_texts = {
|
||||
"ntfy_topic": "The topic to send notifications to.",
|
||||
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
|
||||
}
|
||||
|
||||
|
||||
class RiskModelForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = RiskModel
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"max_loss_percent",
|
||||
"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.",
|
||||
"description": "Description of the risk model. Informational only.",
|
||||
"max_loss_percent": "The maximum percent of the account balance that can be lost before we cease trading.",
|
||||
"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.",
|
||||
}
|
||||
|
||||
|
||||
class AssetGroupForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = AssetGroup
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"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.",
|
||||
"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 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 = 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",
|
||||
"order_type",
|
||||
"time_in_force",
|
||||
"take_profit_percent",
|
||||
"stop_loss_percent",
|
||||
"trailing_stop_loss_percent",
|
||||
"trade_size_percent",
|
||||
)
|
||||
help_texts = {
|
||||
"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.",
|
||||
}
|
||||
|
||||
|
||||
class ActiveManagementPolicyForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = ActiveManagementPolicy
|
||||
fields = (
|
||||
"name",
|
||||
"description",
|
||||
"when_trading_time_violated",
|
||||
"when_trends_violated",
|
||||
"when_position_size_violated",
|
||||
"when_protection_violated",
|
||||
"when_asset_groups_violated",
|
||||
"when_max_open_trades_violated",
|
||||
"when_max_open_trades_per_symbol_violated",
|
||||
"when_max_loss_violated",
|
||||
"when_max_risk_violated",
|
||||
"when_crossfilter_violated",
|
||||
)
|
||||
help_texts = {
|
||||
"name": "Name of the active management policy. Informational only.",
|
||||
"description": "Description of the active management policy. Informational only.",
|
||||
"when_trading_time_violated": "The action to take when the trading time is violated.",
|
||||
"when_trends_violated": "The action to take a trade against the trend is discovered.",
|
||||
"when_position_size_violated": "The action to take when a trade exceeding the position size is discovered.",
|
||||
"when_protection_violated": "The action to take when a trade violating/lacking defined TP/SL/TSL is discovered.",
|
||||
"when_asset_groups_violated": "The action to take when a trade violating the asset group rules is discovered.",
|
||||
"when_max_open_trades_violated": "The action to take when a trade puts the account above the maximum open trades.",
|
||||
"when_max_open_trades_per_symbol_violated": "The action to take when a trade puts the account above the maximum open trades per symbol.",
|
||||
"when_max_loss_violated": "The action to take when the account exceeds its maximum loss. NOTE: The close action will close all trades.",
|
||||
"when_max_risk_violated": "The action to take when a trade exposes the account to more than the maximum risk.",
|
||||
"when_crossfilter_violated": "The action to take when a trade is deemed to conflict with another -- e.g. a buy and sell on the same asset.",
|
||||
}
|
||||
|
||||
111
core/lib/billing.py
Normal file
111
core/lib/billing.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from lago_python_client import Client
|
||||
from lago_python_client.exceptions import LagoApiError
|
||||
from lago_python_client.models import Customer, CustomerBillingConfiguration
|
||||
|
||||
client = Client(api_key=settings.LAGO_API_KEY, api_url=settings.LAGO_URL)
|
||||
|
||||
|
||||
def expand_name(first_name, last_name):
|
||||
"""
|
||||
Convert two name variables into one.
|
||||
Last name without a first name is ignored.
|
||||
:param first_name: The first name
|
||||
:param last_name: The last name
|
||||
:return: A string with the first and last name, or None if both are None
|
||||
"""
|
||||
name = None
|
||||
if first_name:
|
||||
name = first_name
|
||||
# We only want to put the last name if we have a first name
|
||||
if last_name:
|
||||
name += f" {last_name}"
|
||||
return name
|
||||
|
||||
|
||||
def get_or_create(email, first_name, last_name):
|
||||
"""
|
||||
Get a customer ID from Stripe if one with the given email exists.
|
||||
Create a customer if one does not.
|
||||
Raise an exception if two or more customers matching the given email exist.
|
||||
:param email: The email address of the customer
|
||||
:param first_name: The first name of the customer
|
||||
:param last_name: The last name of the customer
|
||||
:return: The customer ID
|
||||
"""
|
||||
# Let's see if we're just missing the ID
|
||||
matching_customers = stripe.Customer.list(email=email, limit=2)
|
||||
if len(matching_customers) == 2:
|
||||
# Something is horribly wrong
|
||||
raise Exception(f"Two customers found for email {email}")
|
||||
|
||||
elif len(matching_customers) == 1:
|
||||
# We found a customer. Let's copy the ID
|
||||
customer = matching_customers["data"][0]
|
||||
customer_id = customer["id"]
|
||||
return customer_id
|
||||
|
||||
else:
|
||||
# We didn't find anything. Create the customer
|
||||
|
||||
# Create a name, since we have 2 variables which could be null
|
||||
name = expand_name(first_name, last_name)
|
||||
cast = {"email": email}
|
||||
if name:
|
||||
cast["name"] = name
|
||||
customer = stripe.Customer.create(**cast)
|
||||
|
||||
return customer.id
|
||||
|
||||
|
||||
def update_customer_fields(user):
|
||||
"""
|
||||
Update the customer fields in Stripe.
|
||||
"""
|
||||
stripe.Customer.modify(user.stripe_id, email=user.email)
|
||||
name = expand_name(user.first_name, user.last_name)
|
||||
stripe.Customer.modify(user.stripe_id, name=name)
|
||||
|
||||
|
||||
def create_or_update_customer(user):
|
||||
"""
|
||||
Create or update a customer in Lago.
|
||||
"""
|
||||
try:
|
||||
customer = client.customers().find(str(user.customer_id))
|
||||
except LagoApiError:
|
||||
customer = None
|
||||
if not customer:
|
||||
customer = Customer(
|
||||
external_id=str(user.customer_id),
|
||||
name=f"{user.first_name} {user.last_name}",
|
||||
)
|
||||
|
||||
customer.external_id = str(user.customer_id)
|
||||
customer.email = user.email
|
||||
customer.name = f"{user.first_name} {user.last_name}"
|
||||
customer.billing_configuration = CustomerBillingConfiguration(
|
||||
payment_provider="stripe",
|
||||
provider_customer_id=str(user.stripe_id),
|
||||
)
|
||||
|
||||
try:
|
||||
created = client.customers().create(customer)
|
||||
except LagoApiError as e:
|
||||
print(e.response)
|
||||
|
||||
lago_id = created.lago_id
|
||||
|
||||
return lago_id
|
||||
|
||||
|
||||
def delete_customer(user):
|
||||
"""
|
||||
Delete a customer from Lago.
|
||||
:param user: User object to delete
|
||||
"""
|
||||
try:
|
||||
client.customers().destroy(str(user.customer_id))
|
||||
except LagoApiError:
|
||||
pass
|
||||
@@ -1,65 +0,0 @@
|
||||
import logging
|
||||
|
||||
import stripe
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def expand_name(first_name, last_name):
|
||||
"""
|
||||
Convert two name variables into one.
|
||||
Last name without a first name is ignored.
|
||||
"""
|
||||
name = None
|
||||
if first_name:
|
||||
name = first_name
|
||||
# We only want to put the last name if we have a first name
|
||||
if last_name:
|
||||
name += f" {last_name}"
|
||||
return name
|
||||
|
||||
|
||||
def get_or_create(email, first_name, last_name):
|
||||
"""
|
||||
Get a customer ID from Stripe if one with the given email exists.
|
||||
Create a customer if one does not.
|
||||
Raise an exception if two or more customers matching the given email exist.
|
||||
"""
|
||||
# Let's see if we're just missing the ID
|
||||
matching_customers = stripe.Customer.list(email=email, limit=2)
|
||||
if len(matching_customers) == 2:
|
||||
# Something is horribly wrong
|
||||
logger.error(f"Two customers found for email {email}")
|
||||
raise Exception(f"Two customers found for email {email}")
|
||||
|
||||
elif len(matching_customers) == 1:
|
||||
# We found a customer. Let's copy the ID
|
||||
customer = matching_customers["data"][0]
|
||||
customer_id = customer["id"]
|
||||
return customer_id
|
||||
|
||||
else:
|
||||
# We didn't find anything. Create the customer
|
||||
|
||||
# Create a name, since we have 2 variables which could be null
|
||||
name = expand_name(first_name, last_name)
|
||||
cast = {"email": email}
|
||||
if name:
|
||||
cast["name"] = name
|
||||
customer = stripe.Customer.create(**cast)
|
||||
logger.info(f"Created new Stripe customer {customer.id} with email {email}")
|
||||
|
||||
return customer.id
|
||||
|
||||
|
||||
def update_customer_fields(stripe_id, email=None, first_name=None, last_name=None):
|
||||
"""
|
||||
Update the customer fields in Stripe.
|
||||
"""
|
||||
if email:
|
||||
stripe.Customer.modify(stripe_id, email=email)
|
||||
logger.info(f"Modified Stripe customer {stripe_id} to have email {email}")
|
||||
if first_name or last_name:
|
||||
name = expand_name(first_name, last_name)
|
||||
stripe.Customer.modify(stripe_id, name=name)
|
||||
logger.info(f"Modified Stripe customer {stripe_id} to have email {name}")
|
||||
38
core/lib/elastic.py
Normal file
38
core/lib/elastic.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from elastic_transport import ConnectionError
|
||||
from elasticsearch import Elasticsearch
|
||||
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
client = None
|
||||
|
||||
|
||||
def initialise_elasticsearch():
|
||||
"""
|
||||
Initialise the Elasticsearch client.
|
||||
"""
|
||||
auth = (settings.ELASTICSEARCH_USERNAME, settings.ELASTICSEARCH_PASSWORD)
|
||||
client = Elasticsearch(
|
||||
settings.ELASTICSEARCH_HOST, http_auth=auth, verify_certs=False
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
def store_msg(index, msg):
|
||||
return
|
||||
# global client
|
||||
# if not client:
|
||||
# client = initialise_elasticsearch()
|
||||
# if "ts" not in msg:
|
||||
# msg["ts"] = datetime.utcnow().isoformat()
|
||||
# try:
|
||||
# result = client.index(index=index, body=msg)
|
||||
# except ConnectionError as e:
|
||||
# log.error(f"Error indexing '{msg}': {e}")
|
||||
# return
|
||||
# if not result["result"] == "created":
|
||||
# log.error(f"Indexing of '{msg}' failed: {result}")
|
||||
@@ -1,115 +0,0 @@
|
||||
from alpaca.common.exceptions import APIError
|
||||
|
||||
from core.models import Strategy, Trade
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
def get_balance(account):
|
||||
account_info = account.client.get_account()
|
||||
cash = account_info["equity"]
|
||||
try:
|
||||
return float(cash)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def get_market_value(account, symbol):
|
||||
try:
|
||||
position = account.client.get_position(symbol)
|
||||
return float(position["market_value"])
|
||||
except APIError:
|
||||
return False
|
||||
|
||||
|
||||
def execute_strategy(callback, strategy):
|
||||
success, cash_balance = strategy.account.client.get_balance()
|
||||
log.debug(f"Cash balance: {cash_balance}")
|
||||
if not success:
|
||||
return None
|
||||
|
||||
user = strategy.user
|
||||
account = strategy.account
|
||||
hook = callback.hook
|
||||
base = callback.base
|
||||
quote = callback.quote
|
||||
direction = hook.direction
|
||||
if quote not in ["usd", "usdt", "usdc", "busd"]:
|
||||
log.error(f"Quote not compatible with Dollar: {quote}")
|
||||
return False
|
||||
quote = "usd" # TODO: MASSIVE HACK
|
||||
symbol = f"{base.upper()}/{quote.upper()}"
|
||||
|
||||
if symbol not in account.supported_symbols:
|
||||
log.error(f"Symbol not supported by account: {symbol}")
|
||||
return False
|
||||
|
||||
print(f"Identified pair from callback {symbol}")
|
||||
|
||||
# market_from_alpaca = get_market_value(account, symbol)
|
||||
# change_percent = abs(((float(market_from_alpaca)-price)/price)*100)
|
||||
# if change_percent > strategy.price_slippage_percent:
|
||||
# log.error(f"Price slippage too high: {change_percent}")
|
||||
# return False
|
||||
|
||||
# type = "limit"
|
||||
type = "market"
|
||||
trade_size_as_ratio = strategy.trade_size_percent / 100
|
||||
log.debug(f"Trade size as ratio: {trade_size_as_ratio}")
|
||||
amount_usd = trade_size_as_ratio * cash_balance
|
||||
log.debug(f"Trade size: {amount_usd}")
|
||||
price = callback.price
|
||||
if not price:
|
||||
return
|
||||
log.debug(f"Extracted price of quote: {price}")
|
||||
|
||||
# We can do this because the quote IS in $ or equivalent
|
||||
trade_size_in_quote = amount_usd / price
|
||||
log.debug(f"Trade size in quote: {trade_size_in_quote}")
|
||||
|
||||
# calculate sl/tp
|
||||
stop_loss_as_ratio = strategy.stop_loss_percent / 100
|
||||
take_profit_as_ratio = strategy.take_profit_percent / 100
|
||||
log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}")
|
||||
log.debug(f"Take profit as ratio: {take_profit_as_ratio}")
|
||||
|
||||
stop_loss_subtract = price * stop_loss_as_ratio
|
||||
take_profit_add = price * take_profit_as_ratio
|
||||
log.debug(f"Stop loss subtract: {stop_loss_subtract}")
|
||||
log.debug(f"Take profit add: {take_profit_add}")
|
||||
|
||||
stop_loss = price - stop_loss_subtract
|
||||
take_profit = price + take_profit_add
|
||||
|
||||
log.debug(f"Stop loss: {stop_loss}")
|
||||
log.debug(f"Take profit: {take_profit}")
|
||||
|
||||
new_trade = Trade.objects.create(
|
||||
user=user,
|
||||
account=account,
|
||||
hook=hook,
|
||||
symbol=symbol,
|
||||
type=type,
|
||||
# amount_usd=amount_usd,
|
||||
amount=trade_size_in_quote,
|
||||
# price=price,
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
direction=direction,
|
||||
)
|
||||
new_trade.save()
|
||||
posted, info = new_trade.post()
|
||||
log.debug(f"Posted trade: {posted} - {info}")
|
||||
|
||||
|
||||
def process_callback(callback):
|
||||
log.info(f"Received callback for {callback.hook}")
|
||||
strategies = Strategy.objects.filter(hooks=callback.hook, enabled=True)
|
||||
log.debug(f"Matched strategies: {strategies}")
|
||||
for strategy in strategies:
|
||||
log.debug(f"Executing strategy {strategy}")
|
||||
if callback.hook.user != strategy.user:
|
||||
log.error("Ownership differs between callback and strategy.")
|
||||
return
|
||||
execute_strategy(callback, strategy)
|
||||
38
core/lib/notify.py
Normal file
38
core/lib/notify.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import requests
|
||||
|
||||
from core.util import logs
|
||||
|
||||
NTFY_URL = "https://ntfy.sh"
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
# Actual function to send a message to a topic
|
||||
def raw_sendmsg(msg, title=None, priority=None, tags=None, url=None, topic=None):
|
||||
if url is None:
|
||||
url = NTFY_URL
|
||||
headers = {"Title": "Fisk"}
|
||||
if title:
|
||||
headers["Title"] = title
|
||||
if priority:
|
||||
headers["Priority"] = priority
|
||||
if tags:
|
||||
headers["Tags"] = tags
|
||||
requests.post(
|
||||
f"{url}/{topic}",
|
||||
data=msg,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
# Sendmsg helper to send a message to a user's notification settings
|
||||
def sendmsg(user, *args, **kwargs):
|
||||
notification_settings = user.get_notification_settings()
|
||||
|
||||
if notification_settings.ntfy_topic is None:
|
||||
# No topic set, so don't send
|
||||
return
|
||||
else:
|
||||
topic = notification_settings.ntfy_topic
|
||||
|
||||
raw_sendmsg(*args, **kwargs, url=notification_settings.ntfy_url, topic=topic)
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from core.lib.schemas import alpaca_s, drakdoo_s, oanda_s, mexc_s # noqa
|
||||
|
||||
@@ -24,6 +24,32 @@ class GetAllAssets(BaseModel):
|
||||
itemlist: list[Asset]
|
||||
|
||||
|
||||
GetAllAssetsSchema = {
|
||||
"itemlist": (
|
||||
"itemlist",
|
||||
[
|
||||
{
|
||||
"id": "id",
|
||||
"class": "class_",
|
||||
"exchange": "exchange",
|
||||
"symbol": "symbol",
|
||||
"name": "name",
|
||||
"status": "status",
|
||||
"tradable": "tradable",
|
||||
"marginable": "marginable",
|
||||
"maintenance_margin_requirement": "maintenanceMarginRequirement",
|
||||
"shortable": "shortable",
|
||||
"easy_to_borrow": "easyToBorrow",
|
||||
"fractionable": "fractionable",
|
||||
"min_order_size": "minOrderSize",
|
||||
"min_trade_increment": "minTradeIncrement",
|
||||
"price_increment": "priceIncrement",
|
||||
}
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# get_open_position
|
||||
class GetOpenPosition(BaseModel):
|
||||
asset_id: str
|
||||
@@ -72,6 +98,23 @@ class GetAllPositions(BaseModel):
|
||||
itemlist: list[Position]
|
||||
|
||||
|
||||
GetAllPositionsSchema = {
|
||||
"itemlist": (
|
||||
"itemlist",
|
||||
[
|
||||
{
|
||||
"symbol": "symbol",
|
||||
"unrealized_pl": "unrealized_pl",
|
||||
"price": "current_price",
|
||||
"units": "qty",
|
||||
"side": "side",
|
||||
"value": "market_value",
|
||||
}
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# get_account
|
||||
class GetAccount(BaseModel):
|
||||
id: str
|
||||
@@ -108,3 +151,41 @@ class GetAccount(BaseModel):
|
||||
sma: str
|
||||
daytrade_count: int
|
||||
balance_asof: str
|
||||
|
||||
|
||||
GetAccountSchema = {
|
||||
"id": "id",
|
||||
"account_number": "account_number",
|
||||
"status": "status",
|
||||
"crypto_status": "crypto_status",
|
||||
"currency": "currency",
|
||||
"buying_power": "buying_power",
|
||||
"regt_buying_power": "regt_buying_power",
|
||||
"daytrading_buying_power": "daytrading_buying_power",
|
||||
"effective_buying_power": "effective_buying_power",
|
||||
"non_marginable_buying_power": "non_marginable_buying_power",
|
||||
"bod_dtbp": "bod_dtbp",
|
||||
"cash": "cash",
|
||||
"accrued_fees": "accrued_fees",
|
||||
"pending_transfer_in": "pending_transfer_in",
|
||||
"portfolio_value": "portfolio_value",
|
||||
"pattern_day_trader": "pattern_day_trader",
|
||||
"trading_blocked": "trading_blocked",
|
||||
"transfers_blocked": "transfers_blocked",
|
||||
"account_blocked": "account_blocked",
|
||||
"created_at": "created_at",
|
||||
"trade_suspended_by_user": "trade_suspended_by_user",
|
||||
"multiplier": "multiplier",
|
||||
"shorting_enabled": "shorting_enabled",
|
||||
"equity": "equity",
|
||||
"last_equity": "last_equity",
|
||||
"long_market_value": "long_market_value",
|
||||
"short_market_value": "short_market_value",
|
||||
"position_market_value": "position_market_value",
|
||||
"initial_margin": "initial_margin",
|
||||
"maintenance_margin": "maintenance_margin",
|
||||
"last_maintenance_margin": "last_maintenance_margin",
|
||||
"sma": "sma",
|
||||
"daytrade_count": "daytrade_count",
|
||||
"balance_asof": "balance_asof",
|
||||
}
|
||||
|
||||
@@ -17,5 +17,6 @@ class DrakdooCallback(BaseModel):
|
||||
title: str
|
||||
message: str
|
||||
period: str
|
||||
price: str | None
|
||||
market: DrakdooMarket
|
||||
timestamp: DrakdooTimestamp
|
||||
|
||||
1
core/lib/schemas/mexc_s.py
Normal file
1
core/lib/schemas/mexc_s.py
Normal file
@@ -0,0 +1 @@
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -1,62 +1,30 @@
|
||||
from pydantic import BaseModel
|
||||
from decimal import Decimal as D
|
||||
from typing import Optional
|
||||
|
||||
a = {
|
||||
"positions": [
|
||||
{
|
||||
"instrument": "EUR_USD",
|
||||
"long": {
|
||||
"units": "1",
|
||||
"averagePrice": "0.99361",
|
||||
"pl": "-0.1014",
|
||||
"resettablePL": "-0.1014",
|
||||
"financing": "0.0000",
|
||||
"dividendAdjustment": "0.0000",
|
||||
"guaranteedExecutionFees": "0.0000",
|
||||
"tradeIDs": ["71"],
|
||||
"unrealizedPL": "-0.0002",
|
||||
},
|
||||
"short": {
|
||||
"units": "0",
|
||||
"pl": "0.0932",
|
||||
"resettablePL": "0.0932",
|
||||
"financing": "0.0000",
|
||||
"dividendAdjustment": "0.0000",
|
||||
"guaranteedExecutionFees": "0.0000",
|
||||
"unrealizedPL": "0.0000",
|
||||
},
|
||||
"pl": "-0.0082",
|
||||
"resettablePL": "-0.0082",
|
||||
"financing": "0.0000",
|
||||
"commission": "0.0000",
|
||||
"dividendAdjustment": "0.0000",
|
||||
"guaranteedExecutionFees": "0.0000",
|
||||
"unrealizedPL": "-0.0002",
|
||||
"marginUsed": "0.0286",
|
||||
}
|
||||
],
|
||||
"lastTransactionID": "71",
|
||||
}
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PositionLong(BaseModel):
|
||||
units: str
|
||||
averagePrice: str
|
||||
averagePrice: Optional[str] = None
|
||||
pl: str
|
||||
resettablePL: str
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
tradeIDs: list[str]
|
||||
tradeIDs: Optional[list[str]] = []
|
||||
unrealizedPL: str
|
||||
|
||||
|
||||
class PositionShort(BaseModel):
|
||||
units: str
|
||||
averagePrice: Optional[str] = None
|
||||
pl: str
|
||||
resettablePL: str
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
tradeIDs: Optional[list[str]] = []
|
||||
unrealizedPL: str
|
||||
|
||||
|
||||
@@ -77,3 +45,710 @@ class Position(BaseModel):
|
||||
class OpenPositions(BaseModel):
|
||||
positions: list[Position]
|
||||
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.
|
||||
We implemented it this way in order to more easily support other exchanges.
|
||||
The actual direction is put into the root object with Grom.
|
||||
"""
|
||||
if float(x["long"]["units"]) > 0 and float(x["short"]["units"]) < 0:
|
||||
raise ValueError("Hedging not allowed")
|
||||
|
||||
|
||||
def parse_prices(x):
|
||||
prevent_hedging(x)
|
||||
if float(x["long"]["units"]) > 0:
|
||||
return x["long"]["averagePrice"]
|
||||
elif float(x["short"]["units"]) < 0:
|
||||
return x["short"]["averagePrice"]
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def parse_units(x):
|
||||
prevent_hedging(x)
|
||||
if float(x["long"]["units"]) > 0:
|
||||
return x["long"]["units"]
|
||||
elif float(x["short"]["units"]) < 0:
|
||||
return x["short"]["units"]
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def parse_value(x):
|
||||
prevent_hedging(x)
|
||||
if float(x["long"]["units"]) > 0:
|
||||
return D(x["long"]["units"]) * D(x["long"]["averagePrice"])
|
||||
elif float(x["short"]["units"]) < 0:
|
||||
return D(x["short"]["units"]) * D(x["short"]["averagePrice"])
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def parse_current_units_side(x):
|
||||
if float(x["currentUnits"]) > 0:
|
||||
return "long"
|
||||
elif float(x["currentUnits"]) < 0:
|
||||
return "short"
|
||||
|
||||
|
||||
def parse_side(x):
|
||||
prevent_hedging(x)
|
||||
if float(x["long"]["units"]) > 0:
|
||||
return "long"
|
||||
elif float(x["short"]["units"]) < 0:
|
||||
return "short"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def parse_trade_ids(x, sum=0):
|
||||
prevent_hedging(x)
|
||||
if float(x["long"]["units"]) > 0:
|
||||
return [str(int(y) + sum) for y in x["long"]["tradeIDs"]]
|
||||
elif float(x["short"]["units"]) < 0:
|
||||
return [str(int(y) + sum) for y in x["short"]["tradeIDs"]]
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
|
||||
OpenPositionsSchema = {
|
||||
"itemlist": (
|
||||
"positions",
|
||||
[
|
||||
{
|
||||
"symbol": "instrument",
|
||||
"unrealized_pl": "unrealizedPL",
|
||||
"trade_ids": parse_trade_ids, # actual value is lower by 1
|
||||
"price": parse_prices,
|
||||
"units": parse_units,
|
||||
"side": parse_side,
|
||||
"value": parse_value,
|
||||
}
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class AccountDetailsNested(BaseModel):
|
||||
guaranteedStopLossOrderMode: str
|
||||
hedgingEnabled: bool
|
||||
id: str
|
||||
createdTime: str
|
||||
currency: str
|
||||
createdByUserID: int
|
||||
alias: str
|
||||
marginRate: str
|
||||
lastTransactionID: str
|
||||
balance: str
|
||||
openTradeCount: int
|
||||
openPositionCount: int
|
||||
pendingOrderCount: int
|
||||
pl: str
|
||||
resettablePL: str
|
||||
resettablePLTime: str
|
||||
financing: str
|
||||
commission: str
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
orders: list # Order
|
||||
positions: list # Position
|
||||
trades: list # Trade
|
||||
unrealizedPL: str
|
||||
NAV: str
|
||||
marginUsed: str
|
||||
marginAvailable: str
|
||||
positionValue: str
|
||||
marginCloseoutUnrealizedPL: str
|
||||
marginCloseoutNAV: str
|
||||
marginCloseoutMarginUsed: str
|
||||
marginCloseoutPositionValue: str
|
||||
marginCloseoutPercent: str
|
||||
withdrawalLimit: str
|
||||
marginCallMarginUsed: str
|
||||
marginCallPercent: str
|
||||
|
||||
|
||||
class AccountDetails(BaseModel):
|
||||
account: AccountDetailsNested
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
AccountDetailsSchema = {
|
||||
"guaranteedSLOM": "account.guaranteedStopLossOrderMode",
|
||||
"hedgingEnabled": "account.hedgingEnabled",
|
||||
"id": "account.id",
|
||||
"created_at": "account.createdTime",
|
||||
"currency": "account.currency",
|
||||
"createdByUserID": "account.createdByUserID",
|
||||
"alias": "account.alias",
|
||||
"marginRate": "account.marginRate",
|
||||
"lastTransactionID": "account.lastTransactionID",
|
||||
"balance": "account.balance",
|
||||
"openTradeCount": "account.openTradeCount",
|
||||
"openPositionCount": "account.openPositionCount",
|
||||
"pendingOrderCount": "account.pendingOrderCount",
|
||||
"pl": "account.pl",
|
||||
"resettablePL": "account.resettablePL",
|
||||
"resettablePLTime": "account.resettablePLTime",
|
||||
"financing": "account.financing",
|
||||
"commission": "account.commission",
|
||||
"dividendAdjustment": "account.dividendAdjustment",
|
||||
"guaranteedExecutionFees": "account.guaranteedExecutionFees",
|
||||
# "orders": "account.orders",
|
||||
# "positions": "account.positions",
|
||||
# "trades": "account.trades",
|
||||
"unrealizedPL": "account.unrealizedPL",
|
||||
"NAV": "account.NAV",
|
||||
"marginUsed": "account.marginUsed",
|
||||
"marginAvailable": "account.marginAvailable",
|
||||
"positionValue": "account.positionValue",
|
||||
"marginCloseoutUnrealizedPL": "account.marginCloseoutUnrealizedPL",
|
||||
"marginCloseoutNAV": "account.marginCloseoutNAV",
|
||||
"marginCloseoutMarginUsed": "account.marginCloseoutMarginUsed",
|
||||
"marginCloseoutPositionValue": "account.marginCloseoutPositionValue",
|
||||
"marginCloseoutPercent": "account.marginCloseoutPercent",
|
||||
"withdrawalLimit": "account.withdrawalLimit",
|
||||
"marginCallMarginUsed": "account.marginCallMarginUsed",
|
||||
"marginCallPercent": "account.marginCallPercent",
|
||||
}
|
||||
|
||||
|
||||
class AccountSummaryNested(BaseModel):
|
||||
marginCloseoutNAV: str
|
||||
marginUsed: str
|
||||
currency: str
|
||||
resettablePL: str
|
||||
NAV: str
|
||||
marginCloseoutMarginUsed: str
|
||||
marginCloseoutPositionValue: str
|
||||
openTradeCount: int
|
||||
id: str
|
||||
hedgingEnabled: bool
|
||||
marginCloseoutPercent: str
|
||||
marginCallMarginUsed: str
|
||||
openPositionCount: int
|
||||
positionValue: str
|
||||
pl: str
|
||||
lastTransactionID: str
|
||||
marginAvailable: str
|
||||
marginRate: str
|
||||
marginCallPercent: str
|
||||
pendingOrderCount: int
|
||||
withdrawalLimit: str
|
||||
unrealizedPL: str
|
||||
alias: str
|
||||
createdByUserID: int
|
||||
marginCloseoutUnrealizedPL: str
|
||||
createdTime: str
|
||||
balance: str
|
||||
|
||||
|
||||
class AccountSummary(BaseModel):
|
||||
account: AccountSummaryNested
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
AccountSummarySchema = {
|
||||
"marginCloseoutNAV": "account.marginCloseoutNAV",
|
||||
"marginUsed": "account.marginUsed",
|
||||
"currency": "account.currency",
|
||||
"resettablePL": "account.resettablePL",
|
||||
"NAV": "account.NAV",
|
||||
"marginCloseoutMarginUsed": "account.marginCloseoutMarginUsed",
|
||||
"marginCloseoutPositionValue": "account.marginCloseoutPositionValue",
|
||||
"openTradeCount": "account.openTradeCount",
|
||||
"id": "account.id",
|
||||
"hedgingEnabled": "account.hedgingEnabled",
|
||||
"marginCloseoutPercent": "account.marginCloseoutPercent",
|
||||
"marginCallMarginUsed": "account.marginCallMarginUsed",
|
||||
"openPositionCount": "account.openPositionCount",
|
||||
"positionValue": "account.positionValue",
|
||||
"pl": "account.pl",
|
||||
"lastTransactionID": "account.lastTransactionID",
|
||||
"marginAvailable": "account.marginAvailable",
|
||||
"marginRate": "account.marginRate",
|
||||
"marginCallPercent": "account.marginCallPercent",
|
||||
"pendingOrderCount": "account.pendingOrderCount",
|
||||
"withdrawalLimit": "account.withdrawalLimit",
|
||||
"unrealizedPL": "account.unrealizedPL",
|
||||
"alias": "account.alias",
|
||||
"createdByUserID": "account.createdByUserID",
|
||||
"marginCloseoutUnrealizedPL": "account.marginCloseoutUnrealizedPL",
|
||||
"createdTime": "account.createdTime",
|
||||
"balance": "account.balance",
|
||||
}
|
||||
|
||||
|
||||
class PositionDetailsNested(BaseModel):
|
||||
instrument: str
|
||||
long: PositionLong
|
||||
short: PositionShort
|
||||
pl: str
|
||||
resettablePL: str
|
||||
financing: str
|
||||
commission: str
|
||||
dividendAdjustment: str
|
||||
guaranteedExecutionFees: str
|
||||
unrealizedPL: str
|
||||
marginUsed: Optional[str] = None
|
||||
|
||||
|
||||
class PositionDetails(BaseModel):
|
||||
position: PositionDetailsNested
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
PositionDetailsSchema = {
|
||||
"symbol": "position.instrument",
|
||||
"long": "position.long",
|
||||
"short": "position.short",
|
||||
"pl": "position.pl",
|
||||
"resettablePL": "position.resettablePL",
|
||||
"financing": "position.financing",
|
||||
"commission": "position.commission",
|
||||
"dividendAdjustment": "position.dividendAdjustment",
|
||||
"guaranteedExecutionFees": "position.guaranteedExecutionFees",
|
||||
"unrealizedPL": "position.unrealizedPL",
|
||||
"marginUsed": "position.marginUsed",
|
||||
"price": lambda x: parse_prices(x["position"]),
|
||||
"units": lambda x: parse_units(x["position"]),
|
||||
"side": lambda x: parse_side(x["position"]),
|
||||
"value": lambda x: parse_value(x["position"]),
|
||||
"trade_ids": lambda x: parse_trade_ids(
|
||||
x["position"], sum=0
|
||||
), # this value is correct
|
||||
}
|
||||
|
||||
|
||||
class InstrumentTag(BaseModel):
|
||||
type: str
|
||||
name: str
|
||||
|
||||
|
||||
class InstrumentFinancingDaysOfWeek(BaseModel):
|
||||
dayOfWeek: str
|
||||
daysCharged: int
|
||||
|
||||
|
||||
class InstrumentFinancing(BaseModel):
|
||||
longRate: str
|
||||
shortRate: str
|
||||
financingDaysOfWeek: list[InstrumentFinancingDaysOfWeek]
|
||||
|
||||
|
||||
class InstrumentGuaranteedRestriction(BaseModel):
|
||||
volume: str
|
||||
priceRange: str
|
||||
|
||||
|
||||
class Instrument(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
displayName: str
|
||||
pipLocation: int
|
||||
displayPrecision: int
|
||||
tradeUnitsPrecision: int
|
||||
minimumTradeSize: str
|
||||
maximumTrailingStopDistance: str
|
||||
minimumTrailingStopDistance: str
|
||||
maximumPositionSize: str
|
||||
maximumOrderUnits: str
|
||||
marginRate: str
|
||||
guaranteedStopLossOrderMode: str
|
||||
tags: list[InstrumentTag]
|
||||
financing: InstrumentFinancing
|
||||
guaranteedStopLossOrderLevelRestriction: Optional[
|
||||
InstrumentGuaranteedRestriction
|
||||
] = None
|
||||
|
||||
|
||||
class AccountInstruments(BaseModel):
|
||||
instruments: list[Instrument]
|
||||
|
||||
|
||||
AccountInstrumentsSchema = {
|
||||
"itemlist": (
|
||||
"instruments",
|
||||
[
|
||||
{
|
||||
"name": "name",
|
||||
"type": "type",
|
||||
"displayName": "displayName",
|
||||
"pipLocation": "pipLocation",
|
||||
"displayPrecision": "displayPrecision",
|
||||
"tradeUnitsPrecision": "tradeUnitsPrecision",
|
||||
"minimumTradeSize": "minimumTradeSize",
|
||||
"maximumTrailingStopDistance": "maximumTrailingStopDistance",
|
||||
"minimumTrailingStopDistance": "minimumTrailingStopDistance",
|
||||
"maximumPositionSize": "maximumPositionSize",
|
||||
"maximumOrderUnits": "maximumOrderUnits",
|
||||
"marginRate": "marginRate",
|
||||
"guaranteedSLOM": "guaranteedStopLossOrderMode",
|
||||
"tags": "tags",
|
||||
"financing": "financing",
|
||||
"guaranteedSLOLR": "guaranteedStopLossOrderLevelRestriction",
|
||||
}
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class PriceBid(BaseModel):
|
||||
price: str
|
||||
liquidity: int
|
||||
|
||||
|
||||
class PriceAsk(BaseModel):
|
||||
price: str
|
||||
liquidity: int
|
||||
|
||||
|
||||
class PriceQuoteHomeConversionFactors(BaseModel):
|
||||
positiveUnits: str
|
||||
negativeUnits: str
|
||||
|
||||
|
||||
class Price(BaseModel):
|
||||
type: str
|
||||
time: str
|
||||
bids: list[PriceBid]
|
||||
asks: list[PriceAsk]
|
||||
closeoutBid: str
|
||||
closeoutAsk: str
|
||||
status: str
|
||||
tradeable: bool
|
||||
quoteHomeConversionFactors: PriceQuoteHomeConversionFactors
|
||||
instrument: str
|
||||
|
||||
|
||||
class PricingInfo(BaseModel):
|
||||
time: str
|
||||
prices: list[Price]
|
||||
|
||||
|
||||
PricingInfoSchema = {
|
||||
"time": "time",
|
||||
"prices": (
|
||||
"prices",
|
||||
[
|
||||
{
|
||||
"type": "type",
|
||||
"time": "time",
|
||||
"bids": "bids",
|
||||
"asks": "asks",
|
||||
"closeoutBid": "closeoutBid",
|
||||
"closeoutAsk": "closeoutAsk",
|
||||
"status": "status",
|
||||
"tradeable": "tradeable",
|
||||
"quoteHomeConversionFactors": "quoteHomeConversionFactors",
|
||||
"symbol": "instrument",
|
||||
}
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class Trade(BaseModel):
|
||||
tradeID: str
|
||||
clientTradeID: str
|
||||
units: str
|
||||
realizedPL: str
|
||||
financing: str
|
||||
baseFinancing: str
|
||||
price: str
|
||||
guaranteedExecutionFee: str
|
||||
quoteGuaranteedExecutionFee: str
|
||||
halfSpreadCost: str
|
||||
# takeProfitOrder: TakeProfitOrder | None
|
||||
takeProfitOrder: Optional[dict] = None
|
||||
stopLossOrder: Optional[dict] = None
|
||||
trailingStopLossOrder: Optional[dict] = None
|
||||
|
||||
|
||||
class SideCarOrder(BaseModel):
|
||||
id: str
|
||||
createTime: str
|
||||
state: str
|
||||
price: Optional[str] = None
|
||||
timeInForce: str
|
||||
gtdTime: Optional[str] = None
|
||||
clientExtensions: Optional[dict] = None
|
||||
tradeID: str
|
||||
clientTradeID: Optional[str] = None
|
||||
type: str
|
||||
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):
|
||||
id: str
|
||||
instrument: str
|
||||
price: str
|
||||
openTime: str
|
||||
initialUnits: str
|
||||
initialMarginRequired: str
|
||||
state: str
|
||||
currentUnits: str
|
||||
realizedPL: str
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
unrealizedPL: str
|
||||
marginUsed: str
|
||||
takeProfitOrder: Optional[SideCarOrder] = None
|
||||
stopLossOrder: Optional[SideCarOrder] = None
|
||||
trailingStopLossOrder: Optional[SideCarOrder] = None
|
||||
trailingStopValue: Optional[dict] = None
|
||||
|
||||
|
||||
class OpenTrades(BaseModel):
|
||||
trades: list[OpenTradesTrade]
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
OpenTradesSchema = {
|
||||
"itemlist": (
|
||||
"trades",
|
||||
[
|
||||
{
|
||||
"id": "id",
|
||||
"symbol": "instrument",
|
||||
"price": "price",
|
||||
"openTime": parse_time,
|
||||
"initialUnits": "initialUnits",
|
||||
"initialMarginRequired": "initialMarginRequired",
|
||||
"state": "state",
|
||||
"currentUnits": "currentUnits",
|
||||
"realizedPL": "realizedPL",
|
||||
"financing": "financing",
|
||||
"dividendAdjustment": "dividendAdjustment",
|
||||
"unrealizedPL": "unrealizedPL",
|
||||
"marginUsed": "marginUsed",
|
||||
"takeProfitOrder": "takeProfitOrder",
|
||||
"stopLossOrder": "stopLossOrder",
|
||||
"trailingStopLossOrder": "trailingStopLossOrder",
|
||||
"trailingStopValue": "trailingStopValue",
|
||||
"side": parse_current_units_side,
|
||||
}
|
||||
],
|
||||
),
|
||||
"lastTransactionID": "lastTransactionID",
|
||||
}
|
||||
|
||||
|
||||
class HomeConversionFactors(BaseModel):
|
||||
gainQuoteHome: str
|
||||
lossQuoteHome: str
|
||||
gainBaseHome: str
|
||||
lossBaseHome: str
|
||||
|
||||
|
||||
class LongPositionCloseout(BaseModel):
|
||||
instrument: str
|
||||
units: str
|
||||
|
||||
|
||||
class OrderTransaction(BaseModel):
|
||||
id: str
|
||||
accountID: str
|
||||
userID: int
|
||||
batchID: str
|
||||
requestID: str
|
||||
time: str
|
||||
type: str
|
||||
instrument: Optional[str] = None
|
||||
units: Optional[str] = None
|
||||
timeInForce: Optional[str] = None
|
||||
positionFill: Optional[str] = None
|
||||
reason: str
|
||||
longPositionCloseout: LongPositionCloseout | None
|
||||
longOrderFillTransaction: Optional[dict] = None
|
||||
|
||||
|
||||
class OrderCreate(BaseModel):
|
||||
orderCreateTransaction: OrderTransaction
|
||||
|
||||
|
||||
OrderCreateSchema = {
|
||||
"id": "orderCreateTransaction.id",
|
||||
"accountID": "orderCreateTransaction.accountID",
|
||||
"userID": "orderCreateTransaction.userID",
|
||||
"batchID": "orderCreateTransaction.batchID",
|
||||
"requestID": "orderCreateTransaction.requestID",
|
||||
"time": "orderCreateTransaction.time",
|
||||
"type": "orderCreateTransaction.type",
|
||||
"symbol": "orderCreateTransaction.instrument",
|
||||
"units": "orderCreateTransaction.units",
|
||||
"timeInForce": "orderCreateTransaction.timeInForce",
|
||||
"positionFill": "orderCreateTransaction.positionFill",
|
||||
"reason": "orderCreateTransaction.reason",
|
||||
}
|
||||
|
||||
|
||||
class LongOrderFillTransaction(BaseModel):
|
||||
id: str
|
||||
accountID: str
|
||||
userID: int
|
||||
batchID: str
|
||||
requestID: str
|
||||
time: str
|
||||
type: str
|
||||
orderID: str
|
||||
instrument: str
|
||||
units: str
|
||||
requestedUnits: str
|
||||
price: str
|
||||
pl: str
|
||||
quotePL: str
|
||||
financing: str
|
||||
baseFinancing: str
|
||||
commission: str
|
||||
accountBalance: str
|
||||
gainQuoteHomeConversionFactor: str
|
||||
lossQuoteHomeConversionFactor: str
|
||||
guaranteedExecutionFee: str
|
||||
quoteGuaranteedExecutionFee: str
|
||||
halfSpreadCost: str
|
||||
fullVWAP: str
|
||||
reason: str
|
||||
tradesClosed: list[Trade]
|
||||
fullPrice: Price
|
||||
homeConversionFactors: HomeConversionFactors
|
||||
longPositionCloseout: LongPositionCloseout
|
||||
|
||||
|
||||
class PositionClose(BaseModel):
|
||||
longOrderCreateTransaction: OrderTransaction | None
|
||||
longOrderFillTransaction: OrderTransaction | None
|
||||
longOrderCancelTransaction: OrderTransaction | None
|
||||
shortOrderCreateTransaction: OrderTransaction | None
|
||||
shortOrderFillTransaction: OrderTransaction | None
|
||||
shortOrderCancelTransaction: OrderTransaction | None
|
||||
relatedTransactionIDs: list[str]
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
PositionCloseSchema = {
|
||||
"longOrderCreateTransaction": "longOrderCreateTransaction",
|
||||
"longOrderFillTransaction": "longOrderFillTransaction",
|
||||
"longOrderCancelTransaction": "longOrderCancelTransaction",
|
||||
"shortOrderCreateTransaction": "shortOrderCreateTransaction",
|
||||
"shortOrderFillTransaction": "shortOrderFillTransaction",
|
||||
"shortOrderCancelTransaction": "shortOrderCancelTransaction",
|
||||
"relatedTransactionIDs": "relatedTransactionIDs",
|
||||
"lastTransactionID": "lastTransactionID",
|
||||
}
|
||||
|
||||
|
||||
class ClientExtensions(BaseModel):
|
||||
id: str
|
||||
tag: str
|
||||
|
||||
|
||||
class TradeDetailsTrade(BaseModel):
|
||||
id: str
|
||||
instrument: str
|
||||
price: str
|
||||
openTime: str
|
||||
initialUnits: str
|
||||
initialMarginRequired: str
|
||||
state: str
|
||||
currentUnits: str
|
||||
realizedPL: str
|
||||
closingTransactionIDs: Optional[list[str]] = []
|
||||
financing: str
|
||||
dividendAdjustment: str
|
||||
closeTime: Optional[str] = None
|
||||
averageClosePrice: Optional[str] = None
|
||||
clientExtensions: Optional[ClientExtensions] = None
|
||||
|
||||
|
||||
class TradeDetails(BaseModel):
|
||||
trade: TradeDetailsTrade
|
||||
lastTransactionID: str
|
||||
|
||||
|
||||
TradeDetailsSchema = {
|
||||
"id": "trade.id",
|
||||
"symbol": "trade.instrument",
|
||||
"price": "trade.price",
|
||||
"openTime": parse_time,
|
||||
"initialUnits": "trade.initialUnits",
|
||||
"initialMarginRequired": "trade.initialMarginRequired",
|
||||
"state": "trade.state",
|
||||
"currentUnits": "trade.currentUnits",
|
||||
"realizedPL": "trade.realizedPL",
|
||||
"closingTransactionIDs": "trade.closingTransactionIDs",
|
||||
"financing": "trade.financing",
|
||||
"dividendAdjustment": "trade.dividendAdjustment",
|
||||
"closeTime": "trade.closeTime",
|
||||
"averageClosePrice": "trade.averageClosePrice",
|
||||
"clientExtensions": "trade.clientExtensions",
|
||||
"lastTransactionID": "lastTransactionID",
|
||||
}
|
||||
|
||||
|
||||
class TradeClose(BaseModel):
|
||||
orderCreateTransaction: OrderTransaction
|
||||
|
||||
|
||||
TradeCloseSchema = {
|
||||
"id": "orderCreateTransaction.id",
|
||||
"accountID": "orderCreateTransaction.accountID",
|
||||
"userID": "orderCreateTransaction.userID",
|
||||
"batchID": "orderCreateTransaction.batchID",
|
||||
"requestID": "orderCreateTransaction.requestID",
|
||||
"time": "orderCreateTransaction.time",
|
||||
"type": "orderCreateTransaction.type",
|
||||
"symbol": "orderCreateTransaction.instrument",
|
||||
"units": "orderCreateTransaction.units",
|
||||
"timeInForce": "orderCreateTransaction.timeInForce",
|
||||
"positionFill": "orderCreateTransaction.positionFill",
|
||||
"reason": "orderCreateTransaction.reason",
|
||||
"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",
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# Trade handling
|
||||
from alpaca.common.exceptions import APIError
|
||||
from alpaca.trading.enums import OrderSide, TimeInForce
|
||||
from alpaca.trading.requests import LimitOrderRequest, MarketOrderRequest
|
||||
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
def sync_trades_with_db(user):
|
||||
pass
|
||||
|
||||
|
||||
def post_trade(trade):
|
||||
# the trade is not placed yet
|
||||
trading_client = trade.account.get_client()
|
||||
if trade.direction == "buy":
|
||||
direction = OrderSide.BUY
|
||||
elif trade.direction == "sell":
|
||||
direction = OrderSide.SELL
|
||||
else:
|
||||
raise Exception("Unknown direction")
|
||||
|
||||
cast = {"symbol": trade.symbol, "side": direction, "time_in_force": TimeInForce.IOC}
|
||||
if trade.amount is not None:
|
||||
cast["qty"] = trade.amount
|
||||
if trade.amount_usd is not None:
|
||||
cast["notional"] = trade.amount_usd
|
||||
if not trade.amount and not trade.amount_usd:
|
||||
return (False, "No amount specified")
|
||||
if trade.take_profit:
|
||||
cast["take_profit"] = {"limit_price": trade.take_profit}
|
||||
if trade.stop_loss:
|
||||
stop_limit_price = trade.stop_loss - (trade.stop_loss * 0.005)
|
||||
cast["stop_loss"] = {
|
||||
"stop_price": trade.stop_loss,
|
||||
"limit_price": stop_limit_price,
|
||||
}
|
||||
if trade.type == "market":
|
||||
market_order_data = MarketOrderRequest(**cast)
|
||||
try:
|
||||
order = trading_client.submit_order(order_data=market_order_data)
|
||||
except APIError as e:
|
||||
log.error(f"Error placing market order: {e}")
|
||||
return (False, e)
|
||||
elif trade.type == "limit":
|
||||
if not trade.price:
|
||||
return (False, "Limit order with no price")
|
||||
cast["limit_price"] = trade.price
|
||||
limit_order_data = LimitOrderRequest(**cast)
|
||||
try:
|
||||
order = trading_client.submit_order(order_data=limit_order_data)
|
||||
except APIError as e:
|
||||
log.error(f"Error placing limit order: {e}")
|
||||
return (False, e)
|
||||
|
||||
else:
|
||||
raise Exception("Unknown trade type")
|
||||
trade.response = order
|
||||
trade.status = "posted"
|
||||
trade.order_id = order["id"]
|
||||
trade.client_order_id = order["client_order_id"]
|
||||
trade.save()
|
||||
return (True, order)
|
||||
|
||||
|
||||
def update_trade(self):
|
||||
pass
|
||||
|
||||
|
||||
def close_trade(trade):
|
||||
pass
|
||||
|
||||
|
||||
def get_position_info(account, asset_id):
|
||||
trading_client = account.get_client()
|
||||
try:
|
||||
position = trading_client.get_open_position(asset_id)
|
||||
except APIError as e:
|
||||
return (False, e)
|
||||
return (True, position)
|
||||
52
core/management/commands/scheduling.py
Normal file
52
core/management/commands/scheduling.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import asyncio
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import Strategy
|
||||
from core.trading import active_management
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("scheduling")
|
||||
|
||||
INTERVAL = 5
|
||||
|
||||
|
||||
async def job():
|
||||
"""
|
||||
Run all schedules matching the given interval.
|
||||
:param interval_seconds: The interval to run.
|
||||
"""
|
||||
strategies = await sync_to_async(list)(
|
||||
Strategy.objects.filter(enabled=True, active_management_enabled=True)
|
||||
)
|
||||
log.debug(f"Found {len(strategies)} strategies")
|
||||
for strategy in strategies:
|
||||
log.debug(f"Running strategy {strategy.name}")
|
||||
ams = active_management.ActiveManagement(strategy) # noqa
|
||||
ams.run_checks()
|
||||
ams.execute_actions()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Start the scheduling process.
|
||||
"""
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
log.debug(f"Scheduling checking process job every {INTERVAL} seconds")
|
||||
scheduler.add_job(job, "interval", seconds=INTERVAL)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
scheduler._eventloop = loop
|
||||
scheduler.start()
|
||||
try:
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
log.info("Process terminating")
|
||||
finally:
|
||||
scheduler.shutdown(wait=False)
|
||||
loop.close()
|
||||
@@ -30,8 +30,7 @@ class Migration(migrations.Migration):
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('stripe_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('last_payment', models.DateTimeField(blank=True, null=True)),
|
||||
('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
],
|
||||
@@ -44,32 +43,6 @@ class Migration(migrations.Migration):
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Plan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('cost', models.IntegerField()),
|
||||
('product_id', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('image', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('subscription_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.plan')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='plans',
|
||||
field=models.ManyToManyField(blank=True, to='core.plan'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Generated by Django 4.1.7 on 2023-02-24 13:18
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('exchange', models.CharField(choices=[('alpaca', 'Alpaca'), ('oanda', 'OANDA'), ('fake', 'Fake')], max_length=255)),
|
||||
('api_key', models.CharField(max_length=255)),
|
||||
('api_secret', models.CharField(max_length=255)),
|
||||
('sandbox', models.BooleanField(default=False)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('supported_symbols', models.JSONField(default=list)),
|
||||
('instruments', models.JSONField(default=list)),
|
||||
('currency', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('initial_balance', models.FloatField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ActiveManagementPolicy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('when_trading_time_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_trends_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_position_size_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only'), ('adjust', 'Adjust violating trades')], default='none', max_length=255)),
|
||||
('when_protection_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only'), ('adjust', 'Adjust violating trades')], default='none', max_length=255)),
|
||||
('when_asset_groups_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_open_trades_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_open_trades_per_symbol_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_loss_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_max_risk_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('when_crossfilter_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('webhook_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('when_no_data', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=7)),
|
||||
('when_no_match', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
|
||||
('when_no_aggregation', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
|
||||
('when_not_in_bounds', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
|
||||
('when_bullish', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=2)),
|
||||
('when_bearish', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=3)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Hook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=1024)),
|
||||
('hook', models.CharField(max_length=255, unique=True)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrderSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('order_type', models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], default='market', max_length=255)),
|
||||
('time_in_force', models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255)),
|
||||
('take_profit_percent', models.FloatField(default=1.5)),
|
||||
('stop_loss_percent', models.FloatField(default=1.0)),
|
||||
('trailing_stop_loss_percent', models.FloatField(blank=True, default=1.0, null=True)),
|
||||
('trade_size_percent', models.FloatField(default=0.5)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RiskModel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('max_loss_percent', models.FloatField(default=0.05)),
|
||||
('max_risk_percent', models.FloatField(default=0.05)),
|
||||
('max_open_trades', models.IntegerField(default=10)),
|
||||
('max_open_trades_per_symbol', models.IntegerField(default=2)),
|
||||
('price_slippage_percent', models.FloatField(default=2.5)),
|
||||
('callback_price_deviation_percent', models.FloatField(default=0.5)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Signal',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=1024)),
|
||||
('signal', models.CharField(max_length=256)),
|
||||
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('type', models.CharField(choices=[('entry', 'Entry'), ('exit', 'Exit'), ('trend', 'Trend')], max_length=255)),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TradingTime',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('start_day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')])),
|
||||
('end_day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')])),
|
||||
('start_time', models.TimeField()),
|
||||
('end_time', models.TimeField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Trade',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('symbol', models.CharField(max_length=255)),
|
||||
('time_in_force', models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255)),
|
||||
('type', models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], max_length=255)),
|
||||
('amount', models.FloatField(blank=True, null=True)),
|
||||
('amount_usd', models.FloatField(blank=True, null=True)),
|
||||
('price', models.FloatField(blank=True, null=True)),
|
||||
('stop_loss', models.FloatField(blank=True, null=True)),
|
||||
('trailing_stop_loss', models.FloatField(blank=True, null=True)),
|
||||
('take_profit', models.FloatField(blank=True, null=True)),
|
||||
('status', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('information', models.JSONField(blank=True, null=True)),
|
||||
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
|
||||
('order_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('client_order_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('response', models.JSONField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('signal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.signal')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Strategy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('enabled', models.BooleanField(default=False)),
|
||||
('signal_trading_enabled', models.BooleanField(default=False)),
|
||||
('active_management_enabled', models.BooleanField(default=False)),
|
||||
('trends', models.JSONField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('active_management_policy', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.activemanagementpolicy')),
|
||||
('asset_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.assetgroup')),
|
||||
('entry_signals', models.ManyToManyField(blank=True, related_name='entry_strategies', to='core.signal')),
|
||||
('exit_signals', models.ManyToManyField(blank=True, related_name='exit_strategies', to='core.signal')),
|
||||
('order_settings', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.ordersettings')),
|
||||
('risk_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.riskmodel')),
|
||||
('trading_times', models.ManyToManyField(to='core.tradingtime')),
|
||||
('trend_signals', models.ManyToManyField(blank=True, related_name='trend_strategies', to='core.signal')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'strategies',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Callback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('message', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('period', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('sent', models.BigIntegerField(blank=True, null=True)),
|
||||
('trade', models.BigIntegerField(blank=True, null=True)),
|
||||
('exchange', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('base', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('quote', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('contract', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('price', models.FloatField(blank=True, null=True)),
|
||||
('symbol', models.CharField(max_length=255)),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
('signal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.signal')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssetRule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('asset', models.CharField(max_length=64)),
|
||||
('aggregation', models.CharField(choices=[('none', 'None'), ('avg_sentiment', 'Average sentiment')], default='none', max_length=255)),
|
||||
('value', models.FloatField(blank=True, null=True)),
|
||||
('original_status', models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'Always allow'), (7, 'Always deny')], default=0)),
|
||||
('status', models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'Always allow'), (7, 'Always deny')], default=0)),
|
||||
('trigger_below', models.FloatField(blank=True, null=True)),
|
||||
('trigger_above', models.FloatField(blank=True, null=True)),
|
||||
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.assetgroup')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('asset', 'group')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-14 23:15
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_session_session'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Hook',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('hook', models.CharField(max_length=255)),
|
||||
('received', models.IntegerField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
20
core/migrations/0003_user_customer_id.py
Normal file
20
core/migrations/0003_user_customer_id.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.1.7 on 2023-02-24 13:21
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_account_activemanagementpolicy_assetgroup_hook_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='customer_id',
|
||||
field=models.UUIDField(blank=True, default=uuid.uuid4, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-15 18:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_hook'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Callback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('data', models.JSONField()),
|
||||
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 4.0.6 on 2022-10-12 09:08
|
||||
# Generated by Django 4.1.7 on 2023-02-24 16:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
('core', '0003_user_customer_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='session',
|
||||
model_name='user',
|
||||
name='stripe_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,77 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-15 22:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_callback'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='callback',
|
||||
name='data',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_contract',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_currency',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_exchange',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='market_item',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='message',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='period',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='timestamp_sent',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='timestamp_trade',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='hook',
|
||||
name='hook',
|
||||
field=models.CharField(max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='hook',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-16 13:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_remove_callback_data_callback_market_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='callback',
|
||||
name='market',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='callback',
|
||||
name='timestamp_sent',
|
||||
field=models.BigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='callback',
|
||||
name='timestamp_trade',
|
||||
field=models.BigIntegerField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 17:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_remove_callback_market_alter_callback_timestamp_sent_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('exchange', models.CharField(max_length=255)),
|
||||
('api_key', models.CharField(max_length=255)),
|
||||
('api_secret', models.CharField(max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 17:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_account'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Trade',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('symbol', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=255)),
|
||||
('amount', models.FloatField()),
|
||||
('price', models.FloatField()),
|
||||
('stop_loss', models.FloatField(blank=True, null=True)),
|
||||
('take_profit', models.FloatField(blank=True, null=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 18:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_trade'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='exchange_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-17 18:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_trade_exchange_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='sandbox',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='direction',
|
||||
field=models.CharField(blank=True, choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='symbol',
|
||||
field=models.CharField(choices=[('BTCUSD', 'Bitcoin/USD')], max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 08:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_account_sandbox_trade_direction_trade_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='exchange',
|
||||
field=models.CharField(choices=[('binance', 'Binance'), ('alpaca', 'Alpaca')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 13:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_alter_account_exchange'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='trade',
|
||||
old_name='exchange_id',
|
||||
new_name='client_order_id',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='order_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='response',
|
||||
field=models.JSONField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='symbol',
|
||||
field=models.CharField(choices=[('BTC/USD', 'Bitcoin/US Dollar'), ('LTC/USD', 'Litecoin/US Dollar')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 13:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_rename_exchange_id_trade_client_order_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='direction',
|
||||
field=models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], default='buy', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='price',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-21 22:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_alter_trade_direction_alter_trade_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='exchange',
|
||||
field=models.CharField(choices=[('alpaca', 'Alpaca')], max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-25 21:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_alter_account_exchange'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Strategy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('enabled', models.BooleanField(default=False)),
|
||||
('take_profit_percent', models.FloatField(default=300.0)),
|
||||
('stop_loss_percent', models.FloatField(default=100.0)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
|
||||
('hooks', models.ManyToManyField(to='core.hook')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-25 21:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_strategy'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-26 09:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0016_strategy_user_trade_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hook',
|
||||
name='direction',
|
||||
field=models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], default='buy', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='price_slippage_percent',
|
||||
field=models.FloatField(default=2.5),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-26 09:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_hook_direction_strategy_price_slippage_percent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='trade_size_percent',
|
||||
field=models.FloatField(default=2.5),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='trade',
|
||||
name='amount_usd',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='amount',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-27 16:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_strategy_trade_size_percent_trade_amount_usd_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='supported_symbols',
|
||||
field=models.JSONField(default=list),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='stop_loss_percent',
|
||||
field=models.FloatField(default=1.0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='strategy',
|
||||
name='take_profit_percent',
|
||||
field=models.FloatField(default=3.0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='trade',
|
||||
name='symbol',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-27 16:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_account_supported_symbols_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='callback',
|
||||
old_name='market_item',
|
||||
new_name='base',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='callback',
|
||||
old_name='market_contract',
|
||||
new_name='contract',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='callback',
|
||||
old_name='market_exchange',
|
||||
new_name='exchange',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='callback',
|
||||
old_name='market_currency',
|
||||
new_name='quote',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='callback',
|
||||
old_name='timestamp_sent',
|
||||
new_name='sent',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='callback',
|
||||
old_name='timestamp_trade',
|
||||
new_name='trade',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='price',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='callback',
|
||||
name='symbol',
|
||||
field=models.CharField(default='NUL/NUL', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
512
core/models.py
512
core/models.py
@@ -1,83 +1,169 @@
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
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 import trades
|
||||
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"),
|
||||
)
|
||||
DIRECTION_CHOICES = (
|
||||
("buy", "Buy"),
|
||||
("sell", "Sell"),
|
||||
)
|
||||
TIF_CHOICES = (
|
||||
("gtc", "GTC (Good Til Cancelled)"),
|
||||
("gfd", "GFD (Good For Day)"),
|
||||
("fok", "FOK (Fill Or Kill)"),
|
||||
("ioc", "IOC (Immediate Or Cancel)"),
|
||||
)
|
||||
DAY_CHOICES = (
|
||||
(1, "Monday"),
|
||||
(2, "Tuesday"),
|
||||
(3, "Wednesday"),
|
||||
(4, "Thursday"),
|
||||
(5, "Friday"),
|
||||
(6, "Saturday"),
|
||||
(7, "Sunday"),
|
||||
)
|
||||
SIGNAL_TYPE_CHOICES = (
|
||||
("entry", "Entry"),
|
||||
("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)
|
||||
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(max_length=255, null=True, blank=True)
|
||||
initial_balance = models.FloatField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
name = f"{self.name} ({self.exchange})"
|
||||
@@ -85,22 +171,34 @@ class Account(models.Model):
|
||||
name += " (sandbox)"
|
||||
return name
|
||||
|
||||
def update_info(self, save=True):
|
||||
client = self.get_client()
|
||||
if client:
|
||||
response = client.get_instruments()
|
||||
supported_symbols = client.get_supported_assets(response)
|
||||
acct_info = client.get_account()
|
||||
log.debug(f"Supported symbols for {self.name}: {supported_symbols}")
|
||||
self.supported_symbols = supported_symbols
|
||||
self.instruments = response
|
||||
if "currency" in acct_info.keys():
|
||||
currency = acct_info["currency"]
|
||||
self.currency = currency
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Override the save function to update supported symbols.
|
||||
"""
|
||||
client = self.get_client()
|
||||
if client:
|
||||
success, supported_symbols = client.get_supported_assets()
|
||||
if success:
|
||||
self.supported_symbols = supported_symbols
|
||||
if self.exchange != "fake":
|
||||
self.update_info(save=False)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_client(self):
|
||||
if self.exchange in EXCHANGE_MAP:
|
||||
return EXCHANGE_MAP[self.exchange](self)
|
||||
else:
|
||||
raise Exception("Exchange not supported")
|
||||
raise Exception(f"Exchange not supported : {self.exchange}")
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
@@ -120,50 +218,50 @@ class Account(models.Model):
|
||||
def get_by_id(cls, account_id, user):
|
||||
return cls.objects.get(id=account_id, user=user)
|
||||
|
||||
|
||||
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)
|
||||
@classmethod
|
||||
def get_by_id_no_user_check(cls, account_id):
|
||||
return cls.objects.get(id=account_id)
|
||||
|
||||
|
||||
class Hook(models.Model):
|
||||
DIRECTION_CHOICES = (
|
||||
("buy", "Buy"),
|
||||
("sell", "Sell"),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=1024, null=True, blank=True, unique=True)
|
||||
hook = models.CharField(max_length=255, unique=True)
|
||||
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
|
||||
name = models.CharField(max_length=1024)
|
||||
hook = models.CharField(max_length=255, unique=True) # hook URL
|
||||
received = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.hook})"
|
||||
|
||||
|
||||
class Signal(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=1024)
|
||||
signal = models.CharField(max_length=256) # signal name
|
||||
hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
|
||||
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
|
||||
received = models.IntegerField(default=0)
|
||||
type = models.CharField(choices=SIGNAL_TYPE_CHOICES, max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.hook.name}) - {self.direction}"
|
||||
|
||||
|
||||
class Trade(models.Model):
|
||||
TYPE_CHOICES = (
|
||||
("market", "Market"),
|
||||
("limit", "Limit"),
|
||||
)
|
||||
DIRECTION_CHOICES = (
|
||||
("buy", "Buy"),
|
||||
("sell", "Sell"),
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
|
||||
signal = models.ForeignKey(Signal, on_delete=models.CASCADE, null=True, blank=True)
|
||||
symbol = models.CharField(max_length=255)
|
||||
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
|
||||
type = models.CharField(choices=TYPE_CHOICES, max_length=255)
|
||||
amount = models.FloatField(null=True, blank=True)
|
||||
amount_usd = models.FloatField(null=True, blank=True)
|
||||
price = models.FloatField(null=True, blank=True)
|
||||
stop_loss = models.FloatField(null=True, blank=True)
|
||||
trailing_stop_loss = models.FloatField(null=True, blank=True)
|
||||
take_profit = models.FloatField(null=True, blank=True)
|
||||
status = models.CharField(max_length=255, null=True, blank=True)
|
||||
information = models.JSONField(null=True, blank=True)
|
||||
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
|
||||
|
||||
# To populate from the trade
|
||||
@@ -176,15 +274,38 @@ class Trade(models.Model):
|
||||
self._original = self
|
||||
|
||||
def post(self):
|
||||
if self.status in ["rejected", "close"]:
|
||||
log.debug(f"Trade {self.id} rejected. Not posting.")
|
||||
log.debug(f"Trade {self.id} information: {self.information}")
|
||||
else:
|
||||
return self.account.client.post_trade(self)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# close the trade
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, trade_id, user):
|
||||
return cls.objects.get(id=trade_id, user=user)
|
||||
|
||||
@classmethod
|
||||
def get_by_id_or_order(cls, trade_id, account_id, user):
|
||||
try:
|
||||
account = Account.objects.get(id=account_id, user=user)
|
||||
except Account.DoesNotExist:
|
||||
return None
|
||||
try:
|
||||
return cls.objects.get(id=trade_id, account=account, user=user)
|
||||
except cls.DoesNotExist:
|
||||
try:
|
||||
return cls.objects.get(order_id=trade_id, account=account, user=user)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class Callback(models.Model):
|
||||
hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
|
||||
signal = models.ForeignKey(Signal, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=1024, null=True, blank=True)
|
||||
message = models.CharField(max_length=1024, null=True, blank=True)
|
||||
period = models.CharField(max_length=255, null=True, blank=True)
|
||||
@@ -198,36 +319,239 @@ class Callback(models.Model):
|
||||
symbol = models.CharField(max_length=255)
|
||||
|
||||
|
||||
class TradingTime(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
start_day = models.IntegerField(choices=DAY_CHOICES)
|
||||
end_day = models.IntegerField(choices=DAY_CHOICES)
|
||||
start_time = models.TimeField()
|
||||
end_time = models.TimeField()
|
||||
|
||||
def within_range(self, ts):
|
||||
"""
|
||||
Check if the specified time is within the configured trading times.
|
||||
:param ts: Timestamp
|
||||
:type ts: datetime
|
||||
:return: whether or not the time is within the trading range
|
||||
:rtype: bool
|
||||
"""
|
||||
start_day = self.start_day
|
||||
end_day = self.end_day
|
||||
# Check the day is between the start and end day
|
||||
if not start_day <= ts.weekday() + 1 <= end_day:
|
||||
return False
|
||||
|
||||
start_time = self.start_time
|
||||
end_time = self.end_time
|
||||
|
||||
# Get what the start time would be this week
|
||||
ts_monday = ts - timedelta(days=ts.weekday())
|
||||
|
||||
# Now we need to add our day of week to monday
|
||||
# Let's set the offset now since it's off by one
|
||||
offset_start = start_day - 1
|
||||
# Datetime: monday=0, tuesday=1, us: monday=1, tuesday=2, so we need to subtract
|
||||
# one from ours to not be off by one
|
||||
offset_end = end_day - 1
|
||||
|
||||
# Now we can add the offset to the monday
|
||||
start = ts_monday + timedelta(days=offset_start)
|
||||
start = start.replace(
|
||||
hour=start_time.hour,
|
||||
minute=start_time.minute,
|
||||
second=start_time.second,
|
||||
microsecond=start_time.microsecond,
|
||||
)
|
||||
end = ts_monday + timedelta(days=offset_end)
|
||||
end = end.replace(
|
||||
hour=end_time.hour,
|
||||
minute=end_time.minute,
|
||||
second=end_time.second,
|
||||
microsecond=end_time.microsecond,
|
||||
)
|
||||
# Check if the ts is between the start and end times
|
||||
# ts must be more than start and less than end
|
||||
return ts >= start and ts <= end
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.name} ({self.get_start_day_display()} at {self.start_time} - "
|
||||
f"{self.get_end_day_display()} at {self.end_time})"
|
||||
)
|
||||
|
||||
|
||||
class Strategy(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
hooks = models.ManyToManyField(Hook)
|
||||
trading_times = models.ManyToManyField(TradingTime)
|
||||
entry_signals = models.ManyToManyField(
|
||||
Signal, related_name="entry_strategies", blank=True
|
||||
)
|
||||
exit_signals = models.ManyToManyField(
|
||||
Signal, related_name="exit_strategies", blank=True
|
||||
)
|
||||
trend_signals = models.ManyToManyField(
|
||||
Signal, related_name="trend_strategies", blank=True
|
||||
)
|
||||
enabled = models.BooleanField(default=False)
|
||||
take_profit_percent = models.FloatField(default=3.0)
|
||||
stop_loss_percent = models.FloatField(default=1.0)
|
||||
price_slippage_percent = models.FloatField(default=2.5)
|
||||
trade_size_percent = models.FloatField(default=2.5)
|
||||
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"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# class Perms(models.Model):
|
||||
# class Meta:
|
||||
# permissions = (
|
||||
# ("bypass_hashing", "Can bypass field hashing"), #
|
||||
# ("bypass_blacklist", "Can bypass the blacklist"), #
|
||||
# ("bypass_encryption", "Can bypass field encryption"), #
|
||||
# ("bypass_obfuscation", "Can bypass field obfuscation"), #
|
||||
# ("bypass_delay", "Can bypass data delay"), #
|
||||
# ("bypass_randomisation", "Can bypass data randomisation"), #
|
||||
# ("post_irc", "Can post to IRC"),
|
||||
# ("post_discord", "Can post to Discord"),
|
||||
# ("query_search", "Can search with query strings"), #
|
||||
# ("use_insights", "Can use the Insights page"),
|
||||
# ("index_int", "Can use the internal index"),
|
||||
# ("index_meta", "Can use the meta index"),
|
||||
# ("restricted_sources", "Can access restricted sources"),
|
||||
# )
|
||||
class NotificationSettings(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
|
||||
ntfy_url = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification settings for {self.user}"
|
||||
|
||||
|
||||
class RiskModel(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
# Maximum amount of money to have lost from the initial balance to stop trading
|
||||
max_loss_percent = models.FloatField(default=0.05)
|
||||
# Maximum amount of money to risk on all open trades
|
||||
max_risk_percent = models.FloatField(default=0.05)
|
||||
# Maximum number of trades
|
||||
max_open_trades = models.IntegerField(default=10)
|
||||
# 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
|
||||
|
||||
|
||||
class AssetGroup(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
|
||||
webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
|
||||
when_no_data = models.IntegerField(choices=MAPPING_CHOICES, default=7)
|
||||
when_no_match = models.IntegerField(choices=MAPPING_CHOICES, default=6)
|
||||
when_no_aggregation = models.IntegerField(choices=MAPPING_CHOICES, default=6)
|
||||
when_not_in_bounds = models.IntegerField(choices=MAPPING_CHOICES, default=6)
|
||||
|
||||
when_bullish = models.IntegerField(choices=MAPPING_CHOICES, default=2)
|
||||
when_bearish = models.IntegerField(choices=MAPPING_CHOICES, default=3)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def matches(self):
|
||||
"""
|
||||
Get the total number of matches for this group.
|
||||
"""
|
||||
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 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)
|
||||
order_type = models.CharField(
|
||||
choices=TYPE_CHOICES, max_length=255, default="market"
|
||||
)
|
||||
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
|
||||
take_profit_percent = models.FloatField(default=1.5)
|
||||
stop_loss_percent = models.FloatField(default=1.0)
|
||||
trailing_stop_loss_percent = models.FloatField(default=1.0, null=True, blank=True)
|
||||
trade_size_percent = models.FloatField(default=0.5)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class ActiveManagementPolicy(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
when_trading_time_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_trends_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_position_size_violated = models.CharField(
|
||||
choices=ADJUST_CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_protection_violated = models.CharField(
|
||||
choices=ADJUST_CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_asset_groups_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_open_trades_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_open_trades_per_symbol_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_loss_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_max_risk_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
when_crossfilter_violated = models.CharField(
|
||||
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"description": "Search for anything.",
|
||||
"description": "Cryptocurrency/Forex/Stocks trading bot",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
@@ -9,7 +9,7 @@
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "Pathogen Data Analytics",
|
||||
"short_name": "Pathogen",
|
||||
"name": "Fisk Trading Desk",
|
||||
"short_name": "Fisk",
|
||||
"start_url": "/"
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
{% load static %}
|
||||
{% load has_plan %}
|
||||
{% load cache %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-GB">
|
||||
{% cache 600 head request.path_info %}
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -180,9 +181,25 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
<!-- Piwik --> {# Yes it's in the source, fight me #}
|
||||
<script type="text/javascript">
|
||||
var _paq = _paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
_paq.push(['setTrackerUrl', 'https://api-f98632bdcf666e3217c6c1a2bafc6c09.s.zm.is']);
|
||||
_paq.push(['setSiteId', 5]);
|
||||
_paq.push(['setApiToken', 'En6AFpSwq4vx3fuXEjSUY6jhUPi_MRinYBQw1FxOqsy']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://c87zpt9a74m181wto33r.s.zm.is/embed.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Piwik Code -->
|
||||
</head>
|
||||
{% endcache %}
|
||||
<body>
|
||||
|
||||
{% cache 600 nav request.user.id %}
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
@@ -202,40 +219,74 @@
|
||||
Home
|
||||
</a>
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Manage
|
||||
Exchange
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'profit' type='page' %}">
|
||||
Profit
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'positions' type='page' %}">
|
||||
Positions
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'trades' type='page' %}">
|
||||
Bot Trades
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user.is_authenticated %}
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Setup
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
|
||||
Hooks
|
||||
Trades
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
|
||||
Accounts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Setup
|
||||
</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'strategies' type='page' %}">
|
||||
Strategies
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'ordersettings' type='page' %}">
|
||||
Order Settings
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'signals' type='page' %}">
|
||||
Signals
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
|
||||
Hooks
|
||||
</a>
|
||||
|
||||
<a class="navbar-item" href="{% url 'tradingtimes' type='page' %}">
|
||||
Trading Times
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'risks' type='page' %}">
|
||||
Risk Management
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'assetgroups' type='page' %}">
|
||||
Asset Groups
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'ams' type='page' %}">
|
||||
Active Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Account
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
||||
Security
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||
Notifications
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings.STRIPE_ENABLED %}
|
||||
{% if settings.BILLING_ENABLED %}
|
||||
{% if user.is_authenticated %}
|
||||
<a class="navbar-item" href="{% url 'billing' %}">
|
||||
Billing
|
||||
@@ -251,16 +302,19 @@
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
{% if not user.is_authenticated %}
|
||||
<a class="button is-info" href="{% url 'signup' %}">
|
||||
<a class="button" href="{% url 'signup' %}">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button is-light" href="{% url 'login' %}">
|
||||
<a class="button" href="{% url 'two_factor:login' %}">
|
||||
Log in
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
<a class="button is-dark" 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>
|
||||
@@ -268,6 +322,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endcache %}
|
||||
<script>
|
||||
let deferredPrompt;
|
||||
const addBtn = document.querySelector('.add-button');
|
||||
@@ -301,8 +356,10 @@
|
||||
{% endblock %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% block content_wrapper %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="windows-here">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<article class="panel is-info">
|
||||
<article class="panel">
|
||||
<p class="panel-heading">
|
||||
User information
|
||||
</p>
|
||||
@@ -8,21 +8,7 @@
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-id-card" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag is-info">{{ 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 is-info">{{ 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>
|
||||
<span class="tag">{{ user.first_name }} {{ user.last_name }}</span>
|
||||
</a>
|
||||
<a class="panel-block" href="{% url 'portal' %}">
|
||||
<span class="panel-icon">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
|
||||
<section>
|
||||
<p>Forgot to add something to your cart? Shop around then come back to pay!</p>
|
||||
<p class="subtitle">Forgot to add something to your cart? Shop around then come back to pay!</p>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load joinsep %}
|
||||
{% block outer_content %}
|
||||
{% block content %}
|
||||
|
||||
<div class="grid-stack" id="grid-stack-main">
|
||||
<div class="grid-stack-item" gs-w="7" gs-h="25" gs-y="0" gs-x="1">
|
||||
<!-- <div class="grid-stack-item" gs-w="5" gs-h="14" gs-y="0" gs-x="1">
|
||||
<div class="grid-stack-item-content">
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
|
||||
Controls
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
{% include 'window-content/controls.html' %}
|
||||
</article>
|
||||
</nav>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- <div class="grid-stack-item" gs-w="4" gs-h="25" gs-y="0" gs-x="6">
|
||||
<div class="grid-stack-item-content">
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
@@ -16,8 +29,7 @@
|
||||
</article>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<script>
|
||||
var grid = GridStack.init({
|
||||
@@ -34,10 +46,15 @@
|
||||
|
||||
// a widget is ready to be loaded
|
||||
document.addEventListener('load-widget', function(event) {
|
||||
let container = htmx.find('#widget');
|
||||
let containers = htmx.findAll('#widget');
|
||||
console.log("CONTAINERS", containers);
|
||||
for (let x = 0, len = containers.length; x < len; x++) {
|
||||
container = containers[x];
|
||||
console.log("CONTAINER", container);
|
||||
// get the scripts, they won't be run on the new element so we need to eval them
|
||||
var scripts = htmx.findAll(container, "script");
|
||||
let widgetelement = container.firstElementChild.cloneNode(true);
|
||||
console.log(widgetelement);
|
||||
var scripts = htmx.findAll(widgetelement, "script");
|
||||
var new_id = widgetelement.id;
|
||||
|
||||
// check if there's an existing element like the one we want to swap
|
||||
@@ -55,6 +72,7 @@
|
||||
}
|
||||
// clear the queue element
|
||||
container.outerHTML = "";
|
||||
// container.firstElementChild.outerHTML = "";
|
||||
grid.addWidget(widgetelement);
|
||||
|
||||
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
|
||||
@@ -79,10 +97,37 @@
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
eval(scripts[i].innerHTML);
|
||||
}
|
||||
}
|
||||
// clear the containers we just added
|
||||
// for (let x = 0, len = containers.length; x < len; x++) {
|
||||
// container = containers[x];
|
||||
// container.inner = "";
|
||||
// }
|
||||
grid.compact();
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
<div>
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'positions' type='widget' %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'strategies' type='widget' %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
<div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'profit' type='widget' %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{% include 'partials/notify.html' %}
|
||||
|
||||
<table
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Account' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% 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"
|
||||
@@ -12,8 +15,11 @@
|
||||
<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 %}
|
||||
@@ -22,6 +28,8 @@
|
||||
<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 %}
|
||||
@@ -34,6 +42,17 @@
|
||||
</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
|
||||
@@ -42,7 +61,7 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-info">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
@@ -56,16 +75,16 @@
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button is-danger">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<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 is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -80,7 +99,7 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -93,4 +112,5 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
61
core/templates/partials/activemanagement-list.html
Normal file
61
core/templates/partials/activemanagement-list.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetManagementPolicy' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_active_management request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ams_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'ams_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
59
core/templates/partials/asset-filter-list.html
Normal file
59
core/templates/partials/asset-filter-list.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetGroup' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_assetgroups_field request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>symbol</th>
|
||||
<th>status</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for key, item in object_list.items %}
|
||||
<tr class="
|
||||
{% if item is True %}has-background-success-light
|
||||
{% elif item is False %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ item }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetfilter_flip' group_id=group_id symbol=key %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon" data-tooltip="Flip direction">
|
||||
<i class="fa-solid fa-arrows-repeat"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'assetfilter_delete' group_id=group_id symbol=key %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
78
core/templates/partials/assetgroup-list.html
Normal file
78
core/templates/partials/assetgroup-list.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetGroup' 'core.AssetRule' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% 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>
|
||||
</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>
|
||||
{% endcache %}
|
||||
77
core/templates/partials/assetrule-list.html
Normal file
77
core/templates/partials/assetrule-list.html
Normal file
@@ -0,0 +1,77 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.AssetRule' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_assetrules request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>asset</th>
|
||||
<th>group</th>
|
||||
<th>aggregation</th>
|
||||
<th>value</th>
|
||||
<th>original status</th>
|
||||
<th>status</th>
|
||||
<th>trigger above</th>
|
||||
<th>trigger below</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.status == 2 %}has-background-success-light
|
||||
{% elif item.status == 3 %}has-background-danger-light
|
||||
{% elif item.status == 0 %}has-background-grey-light
|
||||
{% endif %}">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.asset }}</td>
|
||||
<td>{{ item.group }}</td>
|
||||
<td>{{ item.get_aggregation_display }}</td>
|
||||
<td>{{ item.value }}</td>
|
||||
<td>{{ item.get_original_status_display }}</td>
|
||||
<td>{{ item.get_status_display }}</td>
|
||||
<td>{{ item.trigger_above }}</td>
|
||||
<td>{{ item.trigger_below }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'assetrule_update' type=type group=item.group.id pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'assetrule_delete' type=type group=item.group.id pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.asset }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,6 +1,9 @@
|
||||
{% include 'partials/notify.html' %}
|
||||
|
||||
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
|
||||
{% 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>
|
||||
@@ -47,4 +50,5 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1 +0,0 @@
|
||||
<button class="modal-close is-large" aria-label="close"></button>
|
||||
@@ -1,3 +0,0 @@
|
||||
<i
|
||||
class="fa-solid fa-xmark has-text-grey-light float-right"
|
||||
onclick='grid.removeWidget("widget-{{ unique }}");'></i>
|
||||
@@ -1,3 +0,0 @@
|
||||
<i
|
||||
class="fa-solid fa-xmark has-text-grey-light float-right"
|
||||
data-script="on click remove the closest <nav/>"></i>
|
||||
5
core/templates/partials/error.html
Normal file
5
core/templates/partials/error.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,9 @@
|
||||
{% include 'partials/notify.html' %}
|
||||
|
||||
<table
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Hook' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% 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"
|
||||
@@ -12,7 +15,6 @@
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>hook</th>
|
||||
<th>direction</th>
|
||||
<th>received hooks</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
@@ -21,8 +23,15 @@
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td><code>{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/</code></td>
|
||||
<td>{{ item.direction }}</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">
|
||||
@@ -32,7 +41,7 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-info">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
@@ -46,16 +55,16 @@
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button is-danger">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'callbacks' type='page' pk=item.id %}"><button
|
||||
class="button is-success">
|
||||
<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>
|
||||
@@ -66,11 +75,11 @@
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'callbacks' type=type pk=item.id %}"
|
||||
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 is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -83,4 +92,5 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,5 +0,0 @@
|
||||
{% if message is not None %}
|
||||
<div class="notification is-{{ class }}" hx-ext="remove-me" remove-me="3s">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
69
core/templates/partials/ordersettings-list.html
Normal file
69
core/templates/partials/ordersettings-list.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.OrderSettings' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_ordersettings request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>TP</th>
|
||||
<th>SL</th>
|
||||
<th>TSL</th>
|
||||
<th>size</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description|truncatechars:80 }}</td>
|
||||
<td>{{ item.take_profit_percent}}</td>
|
||||
<td>{{ item.stop_loss_percent }}</td>
|
||||
<td>{{ item.trailing_stop_loss_percent }}</td>
|
||||
<td>{{ item.trade_size_percent }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'ordersettings_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'ordersettings_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
36
core/templates/partials/position-detail.html
Normal file
36
core/templates/partials/position-detail.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends 'mixins/partials/generic-detail.html' %}
|
||||
{% load cache %}
|
||||
|
||||
{% block tbody %}
|
||||
{% cache 600 object_position_detail request.user.id object type %}
|
||||
{% for key, item in object.items %}
|
||||
<tr>
|
||||
{% if key == 'trade_ids' %}
|
||||
<th>{{ key }}</th>
|
||||
<td>
|
||||
{% if item is not None %}
|
||||
{% for trade_id in item %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'trade_action' type=type account_id=object.account_id trade_id=trade_id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-small {% if trade_id in valid_trade_ids %}is-primary{% else %}is-warning{% endif %}">
|
||||
{{ trade_id }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<th>{{ key }}</th>
|
||||
<td>
|
||||
{% if item is not None %}
|
||||
{{ item }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endcache %}
|
||||
{% endblock %}
|
||||
@@ -1,59 +1,79 @@
|
||||
{% include 'partials/notify.html' %}
|
||||
|
||||
<table class="table is-fullwidth is-hoverable" id="positions-table">
|
||||
{% load cache %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% 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>quantity</th>
|
||||
<th>value</th>
|
||||
<th>units</th>
|
||||
<th>quote</th>
|
||||
<th>P/L</th>
|
||||
<th>side</th>
|
||||
<th>trades</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in items %}
|
||||
{% 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_id }}</td>
|
||||
<td>{{ item.account }}</td>
|
||||
<td>{{ item.symbol }}</td>
|
||||
<td>{{ item.current_price }}</td>
|
||||
<td>{{ item.qty }}</td>
|
||||
<td>{{ item.market_value }}</td>
|
||||
<td>{{ item.price }}</td>
|
||||
<td>{{ item.units }}</td>
|
||||
<td>{{ item.value }}</td>
|
||||
<td>{{ item.unrealized_pl }}</td>
|
||||
<td>{{ item.side }}</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
|
||||
<!-- <button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
class="button is-info">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</button> -->
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="#trade-close-confirm"
|
||||
hx-delete="{% url 'position_action' side=item.side account_id=item.account_id symbol=item.symbol %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#positions-table"
|
||||
hx-target="#notification"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you wish to close {{ item.symbol }}?"
|
||||
class="button is-danger">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<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 asset_id=item.asset_id %}">
|
||||
<a href="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}">
|
||||
<button
|
||||
class="button is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -64,11 +84,11 @@
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'position_action' type=type account_id=item.account_id asset_id=item.asset_id %}"
|
||||
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 is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -81,4 +101,5 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,8 +1,10 @@
|
||||
{% 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 %}
|
||||
|
||||
|
||||
{% for plan in plans %}
|
||||
<div class="box">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
@@ -12,7 +14,7 @@
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<p>
|
||||
<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>
|
||||
@@ -43,6 +45,6 @@
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% endcache %}
|
||||
34
core/templates/partials/profit-list.html
Normal file
34
core/templates/partials/profit-list.html
Normal file
@@ -0,0 +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>
|
||||
{% endcache %}
|
||||
73
core/templates/partials/risk-list.html
Normal file
73
core/templates/partials/risk-list.html
Normal file
@@ -0,0 +1,73 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.RiskModel' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_risk request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>max loss percent</th>
|
||||
<th>max risk percent</th>
|
||||
<th>max open trades</th>
|
||||
<th>max open trades per symbol</th>
|
||||
<th>max price slippage percent</th>
|
||||
<th>max callback price deviation percent</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.max_loss_percent }}</td>
|
||||
<td>{{ item.max_risk_percent }}</td>
|
||||
<td>{{ item.max_open_trades }}</td>
|
||||
<td>{{ item.max_open_trades_per_symbol }}</td>
|
||||
<td>{{ item.price_slippage_percent }}</td>
|
||||
<td>{{ item.callback_price_deviation_percent }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'risk_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'risk_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
103
core/templates/partials/signal-list.html
Normal file
103
core/templates/partials/signal-list.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Signal' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_signals request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>signal</th>
|
||||
<th>hook</th>
|
||||
<th>direction</th>
|
||||
<th>received hooks</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr class="
|
||||
{% if item.direction == 'buy' %}has-background-success-light
|
||||
{% elif item.direction == 'sell' %}has-background-danger-light
|
||||
{% endif %}">
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.signal }}</td>
|
||||
<td>
|
||||
<a
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML">{{ item.hook.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ item.direction }}</td>
|
||||
<td>{{ item.received }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'signal_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'signal_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="{% url 'callbacks' type='page' object_type='signal' object_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'callbacks' type=type object_type='signal' object_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,6 +1,9 @@
|
||||
{% include 'partials/notify.html' %}
|
||||
|
||||
<table
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Strategy' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_strategies request.user.id object_list type last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
@@ -9,20 +12,44 @@
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th>account</th>
|
||||
<th>signal trading</th>
|
||||
<th>active management</th>
|
||||
<th>enabled</th>
|
||||
<th>TP</th>
|
||||
<th>SL</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.description|truncatechars:80 }}</td>
|
||||
<td>{{ item.account }}</td>
|
||||
<td>
|
||||
{% if item.signal_trading_enabled %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.active_management_enabled %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.enabled %}
|
||||
<span class="icon">
|
||||
@@ -34,8 +61,6 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.take_profit_percent }}</td>
|
||||
<td>{{ item.stop_loss_percent }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
@@ -44,7 +69,7 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-info">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
@@ -58,19 +83,19 @@
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button is-danger">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="#"><button
|
||||
class="button is-success">
|
||||
<a href="{% url 'trenddirections' type=type strategy_id=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<span class="icon" data-tooltip="View trends">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -78,14 +103,14 @@
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-get="{% url 'trenddirections' type=type strategy_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
<span class="icon" data-tooltip="View trends">
|
||||
<i class="fa-solid fa-arrows-up-down"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -95,4 +120,5 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
@@ -1,6 +1,9 @@
|
||||
{% include 'partials/notify.html' %}
|
||||
|
||||
<table
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Trade' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_trades 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"
|
||||
@@ -9,6 +12,7 @@
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>user</th>
|
||||
<th>status</th>
|
||||
<th>account id</th>
|
||||
<th>symbol</th>
|
||||
@@ -22,6 +26,7 @@
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.user }}</td>
|
||||
<td>{{ item.status }}</td>
|
||||
<td>{{ item.account.id }}</td>
|
||||
<td>{{ item.symbol }}</td>
|
||||
@@ -38,7 +43,7 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-info">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
@@ -51,16 +56,17 @@
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-danger">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if type == 'page' %}
|
||||
<a href="#"><button
|
||||
class="button is-success">
|
||||
<a href="{% url 'trade_action' type=type trade_id=item.id %}">
|
||||
<button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -71,11 +77,11 @@
|
||||
{% else %}
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-get="{% url 'trade_action' type=type account_id=item.account.id trade_id=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button is-success">
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
@@ -88,4 +94,5 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
</table>
|
||||
{% endcache %}
|
||||
65
core/templates/partials/trading-time-list.html
Normal file
65
core/templates/partials/trading-time-list.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.TradingTime' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_tradingtimes 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>start</th>
|
||||
<th>end</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.get_start_day_display }} at {{ item.start_time }}</td>
|
||||
<td>{{ item.get_end_day_display }} at {{ item.end_time }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'tradingtime_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 'tradingtime_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
59
core/templates/partials/trend-direction-list.html
Normal file
59
core/templates/partials/trend-direction-list.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Strategy' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_trenddirections 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>direction</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for key, item in object_list.items %}
|
||||
<tr class="
|
||||
{% if item == 'buy' %}has-background-success-light
|
||||
{% elif item == 'sell' %}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 'trenddirection_flip' strategy_id=strategy_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 'trenddirection_delete' strategy_id=strategy_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 %}
|
||||
@@ -7,12 +7,12 @@
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
||||
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||
<form method="POST" class="box">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="field">
|
||||
<button class="button is-success">
|
||||
<button class="button">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
||||
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||
<div class="box">
|
||||
<p class="has-text-danger">Registration closed.</p>
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
||||
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||
<form method="POST" class="box">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="field">
|
||||
<button class="button is-success">
|
||||
<button class="button">
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
|
||||
<section>
|
||||
<p>Subscription {{ plan }} cancelled!</p>
|
||||
<p class="subtitle">Subscription {{ plan }} cancelled!</p>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
1
core/templates/two_factor/_base.html
Normal file
1
core/templates/two_factor/_base.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}
|
||||
16
core/templates/two_factor/_base_focus.html
Normal file
16
core/templates/two_factor/_base_focus.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "two_factor/_base.html" %}
|
||||
|
||||
{% block content_wrapper %}
|
||||
<section class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column box is-5-tablet is-5-desktop is-4-widescreen">
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
16
core/templates/two_factor/_wizard_actions.html
Normal file
16
core/templates/two_factor/_wizard_actions.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="buttons">
|
||||
{% if cancel_url %}
|
||||
<a href="{{ cancel_url }}"
|
||||
class="button">{% trans "Cancel" %}</a>
|
||||
{% endif %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit"
|
||||
value="{{ wizard.steps.prev }}"
|
||||
class="button">{% trans "Back" %}</button>
|
||||
{% else %}
|
||||
<button disabled name="" type="button" class="button">{% trans "Back" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="button">{% trans "Next" %}</button>
|
||||
</div>
|
||||
6
core/templates/two_factor/_wizard_forms.html
Normal file
6
core/templates/two_factor/_wizard_forms.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<table class="is-3">
|
||||
{{ wizard.management_form|crispy }}
|
||||
{{ wizard.form|crispy }}
|
||||
</table>
|
||||
28
core/templates/two_factor/core/backup_tokens.html
Normal file
28
core/templates/two_factor/core/backup_tokens.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1>
|
||||
<p class="subtitle">{% blocktrans trimmed %}Backup tokens can be used when your primary and backup
|
||||
phone numbers aren't available. The backup tokens below can be used
|
||||
for login verification. If you've used up all your backup tokens, you
|
||||
can generate a new set of backup tokens. Only the backup tokens shown
|
||||
below will be valid.{% endblocktrans %}</p>
|
||||
|
||||
{% if device.token_set.count %}
|
||||
<ul>
|
||||
{% for token in device.token_set.all %}
|
||||
<li>{{ token.token }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="subtitle">{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% trans "You don't have any backup codes yet." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">{% csrf_token %}{{ form }}
|
||||
<a href="{% url 'two_factor:profile'%}"
|
||||
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||
<button class="button" type="submit">{% trans "Generate Tokens" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
52
core/templates/two_factor/core/login.html
Normal file
52
core/templates/two_factor/core/login.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Login" %}{% endblock %}</h1>
|
||||
|
||||
{% if wizard.steps.current == 'auth' %}
|
||||
<p class="subtitle">{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'token' %}
|
||||
{% if device.method == 'call' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
|
||||
digits you hear.{% endblocktrans %}</p>
|
||||
{% elif device.method == 'sms' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
|
||||
sent.{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please enter the tokens generated by your token
|
||||
generator.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% elif wizard.steps.current == 'backup' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Use this form for entering backup tokens for logging in.
|
||||
These tokens have been generated for you to print and keep safe. Please
|
||||
enter one of these backup tokens to login to your account.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% include "two_factor/_wizard_forms.html" %}
|
||||
|
||||
{# hidden submit button to enable [enter] key #}
|
||||
<input type="submit" value="" style="display:none" />
|
||||
|
||||
{% if other_devices %}
|
||||
<p class="subtitle">{% trans "Or, alternatively, use one of your backup phones:" %}</p>
|
||||
<p class="subtitle">
|
||||
{% for other in other_devices %}
|
||||
<button name="challenge_device" value="{{ other.persistent_id }}"
|
||||
class="button" type="submit">
|
||||
{{ other.generate_challenge_button_title }}
|
||||
</button>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if backup_tokens %}
|
||||
<p class="subtitle">{% trans "As a last resort, you can use a backup token:" %}</p>
|
||||
<p class="subtitle">
|
||||
<button name="wizard_goto_step" type="submit" value="backup"
|
||||
class="button">{% trans "Use Backup Token" %}</button>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% include "two_factor/_wizard_actions.html" %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
22
core/templates/two_factor/core/otp_required.html
Normal file
22
core/templates/two_factor/core/otp_required.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
|
||||
|
||||
<p class="subtitle">{% blocktrans trimmed %}The page you requested, enforces users to verify using
|
||||
two-factor authentication for security reasons. You need to enable these
|
||||
security features in order to access this page.{% endblocktrans %}</p>
|
||||
|
||||
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
|
||||
account. Enable two-factor authentication for enhanced account
|
||||
security.{% endblocktrans %}</p>
|
||||
<div class="buttons">
|
||||
|
||||
<a href="javascript:history.go(-1)"
|
||||
class="float-right button">{% trans "Go back" %}</a>
|
||||
<a href="{% url 'two_factor:setup' %}" class="button">
|
||||
{% trans "Enable Two-Factor Authentication" %}</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
24
core/templates/two_factor/core/phone_register.html
Normal file
24
core/templates/two_factor/core/phone_register.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>
|
||||
|
||||
{% if wizard.steps.current == 'setup' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}You'll be adding a backup phone number to your
|
||||
account. This number will be used if your primary method of
|
||||
registration is not available.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'validation' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We've sent a token to your phone number. Please
|
||||
enter the token you've received.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% include "two_factor/_wizard_forms.html" %}
|
||||
|
||||
{# hidden submit button to enable [enter] key #}
|
||||
<input type="submit" value="" style="display:none" />
|
||||
|
||||
{% include "two_factor/_wizard_actions.html" %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
56
core/templates/two_factor/core/setup.html
Normal file
56
core/templates/two_factor/core/setup.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
|
||||
{% if wizard.steps.current == 'welcome' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}You are about to take your account security to the
|
||||
next level. Follow the steps in this wizard to enable two-factor
|
||||
authentication.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'method' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please select which authentication method you would
|
||||
like to use.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'generator' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}To start using a token generator, please use your
|
||||
smartphone to scan the QR code below. For example, use Google
|
||||
Authenticator. Then, enter the token generated by the app.
|
||||
{% endblocktrans %}</p>
|
||||
<p class="subtitle"><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
|
||||
{% elif wizard.steps.current == 'sms' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to receive the
|
||||
text messages on. This number will be validated in the next step.
|
||||
{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'call' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to be called on.
|
||||
This number will be validated in the next step. {% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'validation' %}
|
||||
{% if challenge_succeeded %}
|
||||
{% if device.method == 'call' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
|
||||
digits you hear.{% endblocktrans %}</p>
|
||||
{% elif device.method == 'sms' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
|
||||
sent.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've
|
||||
encountered an issue with the selected authentication method. Please
|
||||
go back and verify that you entered your information correctly, try
|
||||
again, or use a different authentication method instead. If the issue
|
||||
persists, contact the site administrator.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% elif wizard.steps.current == 'yubikey' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a
|
||||
token in the field below. Your YubiKey will be linked to your
|
||||
account.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% include "two_factor/_wizard_forms.html" %}
|
||||
|
||||
{# hidden submit button to enable [enter] key #}
|
||||
<input type="submit" value="" style="display:none" />
|
||||
|
||||
{% include "two_factor/_wizard_actions.html" %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
24
core/templates/two_factor/core/setup_complete.html
Normal file
24
core/templates/two_factor/core/setup_complete.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
|
||||
|
||||
<p class="subtitle">{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
|
||||
authentication.{% endblocktrans %}</p>
|
||||
|
||||
{% if not phone_methods %}
|
||||
<p class="subtitle"><a href="{% url 'two_factor:profile' %}"
|
||||
class="button">{% trans "Back to Account Security" %}</a></p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
|
||||
your primary token device. To enable account recovery, add a phone
|
||||
number.{% endblocktrans %}</p>
|
||||
|
||||
<a href="{% url 'two_factor:profile' %}"
|
||||
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||
class="button">{% trans "Add Phone Number" %}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
14
core/templates/two_factor/profile/disable.html
Normal file
14
core/templates/two_factor/profile/disable.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1>
|
||||
<p class="subtitle">{% blocktrans trimmed %}You are about to disable two-factor authentication. This
|
||||
weakens your account security, are you sure?{% endblocktrans %}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<table>{{ form }}</table>
|
||||
<button class="button"
|
||||
type="submit">{% trans "Disable" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
63
core/templates/two_factor/profile/profile.html
Normal file
63
core/templates/two_factor/profile/profile.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "two_factor/_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Account Security" %}{% endblock %}</h1>
|
||||
|
||||
{% if default_device %}
|
||||
{% if default_device_type == 'TOTPDevice' %}
|
||||
<p class="subtitle">{% trans "Tokens will be generated by your token generator." %}</p>
|
||||
{% elif default_device_type == 'PhoneDevice' %}
|
||||
<p class="subtitle">{% blocktrans with primary=default_device.generate_challenge_button_title %}Primary method: {{ primary }}{% endblocktrans %}</p>
|
||||
{% elif default_device_type == 'RemoteYubikeyDevice' %}
|
||||
<p class="subtitle">{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if available_phone_methods %}
|
||||
<h2 class="title is-4">{% trans "Backup Phone Numbers" %}</h2>
|
||||
<p class="subtitle">{% blocktrans trimmed %}If your primary method is not available, we are able to
|
||||
send backup tokens to the phone numbers listed below.{% endblocktrans %}</p>
|
||||
<ul>
|
||||
{% for phone in backup_phones %}
|
||||
<li>
|
||||
{{ phone.generate_challenge_button_title }}
|
||||
<form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
|
||||
onsubmit="return confirm({% trans 'Are you sure?' %})">
|
||||
{% csrf_token %}
|
||||
<button class="button is-warning"
|
||||
type="submit">{% trans "Unregister" %}</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||
class="button">{% trans "Add Phone Number" %}</a></p>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="title is-4">{% trans "Backup Tokens" %}</h2>
|
||||
<p class="subtitle">
|
||||
{% blocktrans trimmed %}If you don't have any device with you, you can access
|
||||
your account using backup tokens.{% endblocktrans %}
|
||||
{% blocktrans trimmed count counter=backup_tokens %}
|
||||
You have only one backup token remaining.
|
||||
{% plural %}
|
||||
You have {{ counter }} backup tokens remaining.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:backup_tokens' %}"
|
||||
class="button">{% trans "Show Codes" %}</a></p>
|
||||
|
||||
<h3 class="title is-5">{% trans "Disable Two-Factor Authentication" %}</h3>
|
||||
<p class="subtitle">{% blocktrans trimmed %}However we strongly discourage you to do so, you can
|
||||
also disable two-factor authentication for your account.{% endblocktrans %}</p>
|
||||
<p class="subtitle"><a class="button" href="{% url 'two_factor:disable' %}">
|
||||
{% trans "Disable Two-Factor Authentication" %}</a></p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
|
||||
account. Enable two-factor authentication for enhanced account
|
||||
security.{% endblocktrans %}</p>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:setup' %}" class="button">
|
||||
{% trans "Enable Two-Factor Authentication" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user