Compare commits

...

195 Commits

Author SHA1 Message Date
e94d693a39 Update compose to work with Podman 2025-01-23 11:40:30 +00:00
7a44660fc1 Resolve conflict with Redis 2025-01-23 11:40:04 +00:00
9ac3ffa540 Add static directory generated by collectstatic to ignore 2025-01-23 11:38:53 +00:00
6ccf84be26 Make project work with Podman 2024-12-28 13:20:55 +00:00
6d9c78d2e1 Remove dev compose 2024-12-22 17:23:31 +00:00
4079207a05 Begin implementing MEXC 2024-12-03 14:12:42 +00:00
761b084704 Fix Redis, begin implementing MEXC 2024-11-16 17:31:43 +00:00
95a4a6930c Fix changed OANDA API 2024-06-10 06:42:46 +01:00
a788a65ba6 Change Redis cache 2024-06-10 05:28:31 +01:00
e10c6f5c46 Fix price extraction bug and remove debugging statements 2023-08-26 11:05:28 +00:00
cd32dff779 Narrowing down 2023-08-24 17:59:17 +00:00
a2f3170ab5 Even more... 2023-08-24 17:55:53 +00:00
3d91fb394a More debugging 2023-08-24 17:52:18 +00:00
771a944a13 Add more debugging 2023-08-24 17:50:55 +00:00
542dca8324 Add debugging information 2023-08-24 17:47:02 +00:00
a68ade9efe Fix development 2023-08-10 17:11:40 +00:00
aca9897f44 Add development Makefile 2023-07-29 16:34:29 +00:00
9474a516ac Undo Podman changes 2023-07-29 16:28:12 +00:00
8ef39ffe48 Migrate to Podman 2023-07-06 16:11:02 +00:00
b4424a7782 Begin work on increasing position size 2023-02-28 07:20:12 +00:00
5843000df6 Add comments and clean up Lago customers 2023-02-27 07:20:42 +00:00
9d37e2bfb8 Integrate Lago with Stripe 2023-02-24 07:20:51 +00:00
cde1392e68 Consolidate migrations 2023-02-24 07:20:31 +00:00
be10375f60 Amend admin for user 2023-02-24 07:20:31 +00:00
ac4c248175 Begin implementing billing 2023-02-24 07:20:31 +00:00
0937f7299a Remove old models from admin 2023-02-24 07:20:31 +00:00
c6dd0ff286 Remove new ID field 2023-02-24 07:20:31 +00:00
86ace02de8 Attempt to fix migrations 2023-02-24 07:20:31 +00:00
fb5521c9f7 Migrate user id to UUID 2023-02-24 07:20:31 +00:00
682c42c0e8 Separate live tests for active management 2023-02-22 07:20:21 +00:00
9c537187f0 Finish AMS tests 2023-02-22 07:20:58 +00:00
ed63085e10 Implement updating protection 2023-02-22 07:20:37 +00:00
ba8eb69309 Begin protection checks 2023-02-20 23:57:20 +00:00
314d4022ea Add description to AMS policy form 2023-02-20 17:21:33 +00:00
89ef8408e6 Amend asset filter matching to be more explicit 2023-02-20 07:20:01 +00:00
9e22abe057 Implement adjusting positions and begin writing live tests for AMS 2023-02-20 07:20:03 +00:00
a840be3834 Adjust initial balance in live tests 2023-02-20 07:20:37 +00:00
8bb5c2c91b Remove empty functions from checks 2023-02-20 07:20:22 +00:00
db58fb34eb Ensure an account only has one strategy with active management 2023-02-18 21:42:56 +00:00
ea0a6f21ce Remove some comments 2023-02-18 21:39:06 +00:00
8d9fe15346 Fix returning the balance 2023-02-18 21:36:46 +00:00
2b6f00a889 Run checks and actions from the management command 2023-02-18 21:36:38 +00:00
0bf3329b61 Remove comment 2023-02-18 21:25:11 +00:00
911ccde37b Implement trade mutation pipeline and active management actions 2023-02-18 21:23:59 +00:00
ae104f446a Start implementing active management actions 2023-02-18 17:55:39 +00:00
15a8bec105 Simplify active management by only specifying trade IDs for violations 2023-02-18 14:36:58 +00:00
466b17400f Finish implementing active management hooks 2023-02-18 11:54:30 +00:00
3e35214e82 Fix open trades checks 2023-02-17 22:23:12 +00:00
d262f208b5 Write crossfilter, asset groups and max open trades implementation and tests 2023-02-17 22:11:46 +00:00
67117f0978 Write protection check tests 2023-02-17 17:05:52 +00:00
1dbb3fcf79 Add more hooks to active management 2023-02-17 07:20:15 +00:00
dd3b3521d9 Move more checks from market into checks library 2023-02-17 07:20:28 +00:00
da67177a18 Begin work on scheduling management command 2023-02-17 07:20:19 +00:00
ffdbcecc8d Do profit calculation the right way around 2023-02-16 07:20:41 +00:00
c0f266da73 Add signals and active management to strategy list 2023-02-15 20:04:23 +00:00
3854bdcc7d Add signals trading enabled 2023-02-15 20:02:38 +00:00
5c090433a3 Add migration to remove order settings from strategies 2023-02-15 19:13:16 +00:00
eefd704800 Add user field to all list templates 2023-02-15 18:44:19 +00:00
b4afa32a6e Move order settings to OrderSettings 2023-02-15 18:41:08 +00:00
69cf8dcc10 Add order settings to strategy 2023-02-15 18:35:46 +00:00
660aca44db Begin adding order settings 2023-02-15 18:33:38 +00:00
1974b19157 Move risk model to strategy 2023-02-15 18:15:36 +00:00
9a5ed32be9 Add Lago 2023-02-15 07:20:53 +00:00
b37c62f5f1 Fix delete confirmation 2023-02-15 07:20:53 +00:00
bc60eabb05 Fix caching with different types 2023-02-15 07:20:53 +00:00
b6952767d5 Fix asset filter 2023-02-14 07:20:47 +00:00
0a89d96b86 Log assetfilter messages to console 2023-02-14 07:20:47 +00:00
73cf56c50e Use correct template for position details 2023-02-14 07:20:47 +00:00
b6126a8454 Remove subtitle for positions 2023-02-14 07:20:47 +00:00
7a593b902b Add help texts to AssetRule 2023-02-14 07:20:47 +00:00
74fdd8a735 Fix asset_group reference 2023-02-14 07:20:47 +00:00
f4ae8fbc5f Check equality with None instead of truthfulness 2023-02-14 07:20:47 +00:00
27de8090de Add instruments to account readout 2023-02-14 07:20:47 +00:00
1fc969177d Send all precision errors to the user 2023-02-14 07:20:47 +00:00
68a33cea7d Send the user a more detailed precision error message 2023-02-14 07:20:47 +00:00
c915fd1e41 Improve get precision error messages 2023-02-14 07:20:47 +00:00
507708574c Add original status 2023-02-14 07:20:47 +00:00
6385339b7b Don't print the JSON of webhooks 2023-02-14 07:20:47 +00:00
6464b6de05 Filter for enabled accounts 2023-02-13 21:02:59 +00:00
6ff5f718ba Implement asset rules as Asset Group children objects 2023-02-13 20:45:23 +00:00
b48af50620 Rename pairs to assets 2023-02-13 17:50:46 +00:00
0321aff9d5 Implement checking direction with assetfilter 2023-02-13 17:47:47 +00:00
dcfb963be6 Remove asset restrictions and make asset groups smarter 2023-02-13 07:20:40 +00:00
287facbab2 Allow changing the asset filter list 2023-02-11 18:46:26 +00:00
da9f32e882 Send the user a message when an asset restriction is hit 2023-02-11 18:25:09 +00:00
313c7f79d0 Write tests for asset filter 2023-02-11 18:22:49 +00:00
ce0b75ae2d Make account on AssetGroup optional 2023-02-11 18:18:07 +00:00
bdf8f04210 Re-add property fields 2023-02-11 18:07:05 +00:00
7afdd39af7 Fix adding asset restrictions 2023-02-11 17:45:22 +00:00
33d8e26c9b Use cachalot to invalidate caches 2023-02-11 17:22:25 +00:00
dea1cfe889 Use Hiredis 2023-02-11 16:01:26 +00:00
7d693ad1fa Vary cache on URL 2023-02-11 15:48:53 +00:00
a0c94b2097 Cache all object list templates 2023-02-11 14:52:00 +00:00
0acddb2048 Remove comments from settings 2023-02-11 14:04:21 +00:00
8455d64e31 Reformat 2023-02-11 14:00:19 +00:00
57078c10c1 Optimise performance with caching 2023-02-11 14:00:09 +00:00
1f43a00c7a Add all models to admin site and tweak some documentation 2023-02-10 23:47:55 +00:00
010aba7f81 Implement storing asset restriction callbacks 2023-02-10 23:26:30 +00:00
c283c6c192 Add asset restriction webhook API 2023-02-10 22:04:01 +00:00
aa227c53ac Remove unused function in callbacks 2023-02-10 21:16:27 +00:00
0b7dc001bf Add a warning about hooks to the asset restriction form 2023-02-10 21:14:17 +00:00
1d01368570 Add webhook ID to asset restriction model 2023-02-10 21:13:12 +00:00
101a4933c9 Make long IDs copyable 2023-02-10 21:13:00 +00:00
119acdd734 Use django-crud-mixins for CRUD helpers 2023-02-10 20:49:35 +00:00
659b73e695 Add mixins 2023-02-10 07:20:36 +00:00
8750e999b3 Remove unused log 2023-02-10 07:20:36 +00:00
f81d632df3 Add asset groups and restrictions 2023-02-10 14:33:17 +00:00
7938bffc8d Implement extra permission checks and mutations on restriction mixin 2023-02-10 14:32:51 +00:00
72055181bc Implement CRUD for asset groups 2023-02-10 07:20:19 +00:00
e00cdc906e Update pre-commit versions 2023-02-09 07:20:17 +00:00
f4ef280f80 Fix circular import 2023-01-11 21:12:43 +00:00
70d1fdbbd3 Adjust comment 2023-01-11 20:53:04 +00:00
7d0f979a96 Check risk management when opening trades with strategies 2023-01-11 20:48:17 +00:00
3f05553c71 Add initial balance to template and adjust PL calculation to use initial balance 2023-01-11 20:43:11 +00:00
9a69120695 Remove leftover comments and debug statements 2023-01-11 19:59:27 +00:00
23faeb6f71 Write tests for open trade checks 2023-01-11 19:55:09 +00:00
e55f903f42 Continue implementing live risk checks 2023-01-11 19:46:47 +00:00
93be9e6ffe Check the max risk relative to the account balance 2023-01-06 07:20:55 +00:00
ae42d9b223 Fix profit/loss calculation 2023-01-06 08:52:15 +00:00
1bab2a729b Fix SL polarity for losses 2023-01-05 23:58:13 +00:00
db870c39c6 Fix TP/SL calculation and make more tests for profit/loss 2023-01-05 23:37:50 +00:00
2dfaef324c Add more tests for risk checking 2023-01-05 19:48:56 +00:00
483333bf28 Test checking maximum risk with market data 2023-01-05 19:46:18 +00:00
d3e2bc8648 Implement TP/SL price to percent conversion 2023-01-05 19:27:59 +00:00
a6f9e74ee1 Close all positions on demo account when exiting tests 2023-01-05 17:25:22 +00:00
b8b39ea8d3 Implement closing all positions 2023-01-05 17:25:06 +00:00
9dda0e8b4a Write live open/close and list trades tests 2023-01-02 18:42:55 +00:00
72671aa87f Refactor OANDA schemas and refactor existing ones to make use of more objects 2023-01-02 18:42:33 +00:00
9835219e51 Implement closing trades on OANDA 2023-01-02 18:42:03 +00:00
2fa61fb195 Print the response if validation fails 2023-01-02 18:41:49 +00:00
e0ea4c86fa Implement check risk function 2023-01-01 21:52:51 +00:00
46aaff43c0 Begin writing live tests 2023-01-01 21:52:43 +00:00
b2361bda77 Add enabled checkbox to account list 2023-01-01 15:52:04 +00:00
b31a2d1464 Allow disabling accounts 2023-01-01 15:46:40 +00:00
a18c150fe2 Begin implementing get all open trades 2022-12-22 07:20:49 +00:00
b818e7e3f5 Write risk checking helpers and tests 2022-12-13 07:20:49 +00:00
c81cb62aca Finish implementing risk models 2022-12-13 07:20:49 +00:00
4e24ceac72 Add risk model and views 2022-12-21 21:35:59 +00:00
52ddef4c8f Simplify protection options and allow none 2022-12-20 23:20:07 +00:00
29125d5087 Reshuffle the menu to minimise confusion 2022-12-13 07:20:49 +00:00
3a7b5c3ffd Bring Alpaca and OANDA implementation functions into alignment 2022-12-13 07:20:49 +00:00
a41a1e76a5 Refactor market into two files 2022-12-20 07:20:26 +00:00
b7c46ba1d3 Add more user feedback on rejected and dropped trades 2022-12-20 07:20:26 +00:00
a96c99b9e4 Remove dot in notifications title 2022-12-19 07:20:56 +00:00
8afe638f0d Add Elasticsearch support 2022-12-13 07:20:49 +00:00
50820172b1 Send notification on new user creations 2022-12-18 17:49:42 +00:00
8de99c1bcd Make variables passed to CRUD helpers consistent 2022-12-18 17:37:34 +00:00
246674b03e Hide the cancel button and add title/subtitle to notification page 2022-12-18 17:27:40 +00:00
7ee698f457 Implement custom notification settings 2022-12-18 17:21:52 +00:00
4c463e88f2 Implement notifications 2022-12-18 16:55:09 +00:00
f4772a3c7d Remove some comments from limits views 2022-12-18 16:37:27 +00:00
3a39181261 Implement viewing and altering trends 2022-12-18 15:10:28 +00:00
b882ba15d0 Write tests for crossfilter sub-functions 2022-12-12 19:53:32 +00:00
1793b5cc5d Prevent betting against ourselves via inverted pairs 2022-12-12 19:53:20 +00:00
4218fdedbc Fix running tests 2022-12-12 19:52:48 +00:00
d6ab0ffd0e Add all models to admin site 2022-12-12 07:20:20 +00:00
06d8c9f4b2 Add tracker 2022-12-09 17:09:03 +00:00
d1c44cee92 Improve professionalism by removing color 2022-12-08 07:20:46 +00:00
cf4b8a0195 Lowercase trade in profit table 2022-12-08 07:20:46 +00:00
05f94e6e93 Rename trade action title 2022-12-08 07:20:46 +00:00
a18572ebda Add unrealized PL to profit screen 2022-12-08 07:20:46 +00:00
103a15f0e3 Add account ID to trade page buttons 2022-12-08 07:20:46 +00:00
cd89b11611 Pass account ID to trade CRUD helper 2022-12-08 07:20:46 +00:00
633894ae75 Make account required for trade information 2022-12-08 07:20:46 +00:00
312ddb4dc1 Adjust titles for CRUD panels 2022-12-08 07:20:46 +00:00
0aef440229 Correctly check if no trend signals are defined 2022-12-08 07:20:46 +00:00
8840b04059 Use ObjectRead helper for all list and detail views 2022-12-08 07:20:46 +00:00
1e85e830b2 Fix positions CRUD 2022-12-08 07:20:07 +00:00
d396abca84 Use CRUD helper for position list 2022-12-08 07:20:07 +00:00
aa8ee887d3 Replace numbers with question marks in signal name 2022-12-07 07:20:05 +00:00
575b6a240f Handle errors in checking for open positions 2022-12-07 07:20:47 +00:00
af69b886ba Add debug statement in crossfilter 2022-12-07 07:20:47 +00:00
e388624f65 Move type field to signal 2022-12-07 07:20:11 +00:00
2ee5f7b937 Don't show all signals on strategy page 2022-12-01 07:20:35 +00:00
baed991eca Compact grid after closing widget 2022-12-07 07:20:32 +00:00
d59d571679 Show hook name in signal string method 2022-12-06 22:43:43 +00:00
3b8e5dbdd1 Fix hook test method 2022-12-06 20:12:24 +00:00
15e00112af Truncate strategy description 2022-12-06 20:06:35 +00:00
43caab5bf7 Make widgets appear lower to prevent pushdown 2022-12-06 20:04:22 +00:00
62c37a7a45 Color signals 2022-12-06 20:00:41 +00:00
6a549f3fd7 Allow spaces in signal name 2022-12-06 19:59:10 +00:00
3b3faecdf1 Implement trend signals 2022-12-06 19:46:06 +00:00
242c9fbaed Make a proper front page 2022-11-29 07:20:39 +00:00
a39a5c3857 Implement profit view and fix auto refresh 2022-11-29 07:20:39 +00:00
2b13802009 Make more fields optional and fix crash 2022-11-29 07:20:39 +00:00
b3bacde8df Remove unnecessary fields from trade response 2022-12-02 07:20:37 +00:00
62476e5da3 Remap trade details schema to expand trade variable 2022-12-02 07:20:37 +00:00
c5d289ce85 Raise an error if a position has both short and long sides 2022-12-02 07:20:37 +00:00
1ce6c3fafa Implement closing positions 2022-12-02 07:20:37 +00:00
5aac60a7ee Remove some useless buttons 2022-12-02 07:20:37 +00:00
077768975d Get the right signal name from the callback and fix position close schema 2022-12-02 07:20:37 +00:00
848f69da5e Exits are exits, not entries 2022-12-01 21:13:21 +00:00
66a18a6406 Implement closing positions and refuse to post rejected trades 2022-12-01 20:36:58 +00:00
5c2eeae043 Implement crossfilter to protect against stupidity 2022-12-01 20:36:32 +00:00
c0c1ccde8b Add schemas for more commands 2022-12-01 20:36:09 +00:00
164 changed files with 10396 additions and 3998 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,22 +1,22 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.6.0 rev: 23.1.0
hooks: hooks:
- id: black - id: black
exclude: ^core/migrations exclude: ^core/migrations
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.10.1 rev: 5.11.5
hooks: hooks:
- id: isort - id: isort
args: ["--profile", "black"] args: ["--profile", "black"]
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 4.0.1 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
args: [--max-line-length=88] args: [--max-line-length=88]
exclude: ^core/migrations exclude: ^core/migrations
- repo: https://github.com/rtts/djhtml - repo: https://github.com/rtts/djhtml
rev: 'v1.5.2' rev: v2.0.0
hooks: hooks:
- id: djhtml - id: djhtml
args: [-t 2] args: [-t 2]
@@ -28,8 +28,3 @@ repos:
rev: v0.1.5 rev: v0.1.5
hooks: hooks:
- id: ripsecrets - id: ripsecrets
# - repo: https://github.com/thibaudcolas/curlylint
# rev: v0.13.1
# hooks:
# - id: curlylint
# files: \.(html|sls)$

View File

@@ -1,26 +0,0 @@
run:
docker-compose --env-file=stack.env up -d
build:
docker-compose --env-file=stack.env build
stop:
docker-compose --env-file=stack.env down
log:
docker-compose --env-file=stack.env logs -f
test:
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES)"
migrate:
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
makemigrations:
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"
auth:
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py createsuperuser"
token:
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"

26
Makefile-dev Normal file
View File

@@ -0,0 +1,26 @@
run:
docker-compose --env-file=stack.env up -d
build:
docker-compose --env-file=stack.env build
stop:
docker-compose --env-file=stack.env down
log:
docker-compose --env-file=stack.env logs -f
test:
docker-compose --env-file=stack.env run -e LIVE=$(LIVE) --rm app_dev sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
migrate:
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py migrate"
makemigrations:
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py makemigrations"
auth:
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py createsuperuser"
token:
docker-compose --env-file=stack.env run --rm app_dev sh -c ". /venv/bin/activate && python manage.py addstatictoken m"

26
Makefile-prod Normal file
View File

@@ -0,0 +1,26 @@
run:
docker-compose -f docker-compose.prod.yml --env-file=stack.env up -d
build:
docker-compose -f docker-compose.prod.yml --env-file=stack.env build
stop:
docker-compose -f docker-compose.prod.yml --env-file=stack.env down
log:
docker-compose -f docker-compose.prod.yml --env-file=stack.env logs -f
test:
docker-compose -f docker-compose.prod.yml --env-file=stack.env run -e LIVE=$(LIVE) --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
migrate:
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
makemigrations:
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"
auth:
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py createsuperuser"
token:
docker-compose -f docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py addstatictoken m"

View File

@@ -13,7 +13,8 @@ ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",")
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",") CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",")
# Stripe # 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_TEST = getenv("STRIPE_TEST", "true").lower() in trues
STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "") STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "")
STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "") STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "")
@@ -31,17 +32,34 @@ REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues
# Hook URL, do not include leading or trailing slash # Hook URL, do not include leading or trailing slash
HOOK_PATH = "hook" 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 DEBUG = getenv("DEBUG", "false").lower() in trues
PROFILER = getenv("PROFILER", "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: if DEBUG:
import socket # only if you haven't already imported this import socket # only if you haven't already imported this
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [ INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
"127.0.0.1", "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"]

View File

@@ -52,7 +52,10 @@ INSTALLED_APPS = [
# "two_factor.plugins.email", # "two_factor.plugins.email",
# "two_factor.plugins.yubikey", # "two_factor.plugins.yubikey",
# "otp_yubikey", # "otp_yubikey",
"mixins",
"cachalot",
] ]
CRISPY_TEMPLATE_PACK = "bulma" CRISPY_TEMPLATE_PACK = "bulma"
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",) CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html" DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
@@ -61,7 +64,9 @@ MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
# 'django.middleware.cache.UpdateCacheMiddleware',
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
# 'django.middleware.cache.FetchFromCacheMiddleware',
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django_otp.middleware.OTPMiddleware", "django_otp.middleware.OTPMiddleware",
@@ -162,7 +167,7 @@ REST_FRAMEWORK = {
INTERNAL_IPS = [ INTERNAL_IPS = [
"127.0.0.1", "127.0.0.1",
"10.1.10.11", # "10.1.10.11",
] ]
DEBUG_TOOLBAR_PANELS = [ DEBUG_TOOLBAR_PANELS = [
@@ -181,10 +186,29 @@ DEBUG_TOOLBAR_PANELS = [
"debug_toolbar.panels.logging.LoggingPanel", "debug_toolbar.panels.logging.LoggingPanel",
"debug_toolbar.panels.redirects.RedirectsPanel", "debug_toolbar.panels.redirects.RedirectsPanel",
"debug_toolbar.panels.profiling.ProfilingPanel", "debug_toolbar.panels.profiling.ProfilingPanel",
"cachalot.panels.CachalotPanel",
] ]
from app.local_settings import * # noqa 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 if PROFILER: # noqa - trust me its there
import pyroscope import pyroscope

View File

@@ -18,37 +18,43 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.views import LogoutView from django.contrib.auth.views import LogoutView
from django.urls import include, path from django.urls import include, path
from django.views.generic import TemplateView
from two_factor.urls import urlpatterns as tf_urls from two_factor.urls import urlpatterns as tf_urls
from core.views import ( from core.views import (
accounts, accounts,
assets,
base, base,
callbacks, callbacks,
hooks, hooks,
limits, limits,
notifications,
ordersettings,
policies,
positions, positions,
profit,
risk,
signals, signals,
strategies, strategies,
trades, trades,
) )
from core.views.stripe_callbacks import Callback
# from core.views.stripe_callbacks import Callback
urlpatterns = [ urlpatterns = [
path("__debug__/", include("debug_toolbar.urls")), path("__debug__/", include("debug_toolbar.urls")),
path("", base.Home.as_view(), name="home"), 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("billing/", base.Billing.as_view(), name="billing"),
path("order/<str:plan_name>/", base.Order.as_view(), name="order"), # path("order/<str:plan_name>/", base.Order.as_view(), name="order"),
path( # path(
"cancel_subscription/<str:plan_name>/", # "cancel_subscription/<str:plan_name>/",
base.Cancel.as_view(), # base.Cancel.as_view(),
name="cancel_subscription", # name="cancel_subscription",
), # ),
path( # path(
"success/", TemplateView.as_view(template_name="success.html"), name="success" # "success/", TemplateView.as_view(template_name="success.html"), name="success"
), # ),
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"), # path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
path("portal", base.Portal.as_view(), name="portal"), path("portal", base.Portal.as_view(), name="portal"),
path("sapp/", admin.site.urls), path("sapp/", admin.site.urls),
# 2FA login urls # 2FA login urls
@@ -70,6 +76,11 @@ urlpatterns = [
path( path(
f"{settings.HOOK_PATH}/<str:hook_name>/", hooks.HookAPI.as_view(), name="hook" f"{settings.HOOK_PATH}/<str:hook_name>/", hooks.HookAPI.as_view(), name="hook"
), ),
path(
f"{settings.ASSET_PATH}/<str:webhook_id>/",
assets.AssetGroupAPI.as_view(),
name="asset",
),
path("signals/<str:type>/", signals.SignalList.as_view(), name="signals"), path("signals/<str:type>/", signals.SignalList.as_view(), name="signals"),
path( path(
"signals/<str:type>/create/", "signals/<str:type>/create/",
@@ -123,7 +134,7 @@ urlpatterns = [
name="trade_update", name="trade_update",
), ),
path( path(
"trades/<str:type>/view/<str:trade_id>/", "trades/<str:type>/view/<str:account_id>/<str:trade_id>/",
trades.TradeAction.as_view(), trades.TradeAction.as_view(),
name="trade_action", name="trade_action",
), ),
@@ -137,12 +148,18 @@ urlpatterns = [
trades.TradeDeleteAll.as_view(), trades.TradeDeleteAll.as_view(),
name="trade_delete_all", 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>/", positions.Positions.as_view(), name="positions"),
path( path(
"positions/<str:type>/<str:account_id>/", "positions/<str:type>/<str:account_id>/",
positions.Positions.as_view(), positions.Positions.as_view(),
name="positions", name="positions",
), ),
path(
"positions/close/<str:account_id>/<str:side>/<str:symbol>/",
positions.PositionAction.as_view(),
name="position_action",
),
path( path(
"positions/<str:type>/<str:account_id>/<str:symbol>/", "positions/<str:type>/<str:account_id>/<str:symbol>/",
positions.PositionAction.as_view(), positions.PositionAction.as_view(),
@@ -186,4 +203,129 @@ urlpatterns = [
limits.TradingTimeDelete.as_view(), limits.TradingTimeDelete.as_view(),
name="tradingtime_delete", 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) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -2,7 +2,19 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm 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
@@ -13,27 +25,91 @@ from .models import Plan, Session, User
# Register your models here. # Register your models here.
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
list_filter = ["plans"] # list_filter = ["plans"]
model = User model = User
add_form = CustomUserCreationForm add_form = CustomUserCreationForm
fieldsets = ( fieldsets = (
*UserAdmin.fieldsets, *UserAdmin.fieldsets,
( (
"Stripe information", "Billing information",
{"fields": ("stripe_id",)}, {"fields": ("billing_provider_id", "customer_id", "stripe_id")},
), ),
( # (
"Payment information", # "Payment information",
{ # {
"fields": ( # "fields": (
"plans", # # "plans",
"last_payment", # "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(User, CustomUserAdmin)
admin.site.register(Plan) # admin.site.register(Plan)
admin.site.register(Session) # 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)

View File

@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
from alpaca.common.exceptions import APIError from alpaca.common.exceptions import APIError
from glom import glom from glom import glom
from oandapyV20.exceptions import V20Error from oandapyV20.exceptions import V20Error
from pydantic.error_wrappers import ValidationError
from core.lib import schemas from core.lib import schemas
from core.util import logs from core.util import logs
@@ -131,7 +132,12 @@ class BaseExchange(ABC):
def validate_response(self, response, method): def validate_response(self, response, method):
schema = self.get_schema(method) schema = self.get_schema(method)
# Return a dict of the validated response # Return a dict of the validated response
try:
response_valid = schema(**response).dict() 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 return response_valid
def call(self, method, *args, **kwargs): def call(self, method, *args, **kwargs):
@@ -198,6 +204,10 @@ class BaseExchange(ABC):
def post_trade(self, trade): def post_trade(self, trade):
pass pass
@abstractmethod
def close_trade(self, trade_id, units=None):
pass
@abstractmethod @abstractmethod
def get_trade(self, trade_id): def get_trade(self, trade_id):
pass pass
@@ -218,6 +228,14 @@ class BaseExchange(ABC):
def get_all_positions(self): def get_all_positions(self):
pass pass
@abstractmethod
def get_all_open_trades(self):
pass
@abstractmethod
def close_position(self, side, symbol):
pass
@abstractmethod @abstractmethod
def close_all_positions(self): def close_all_positions(self):
pass pass

View File

@@ -7,7 +7,7 @@ from alpaca.trading.requests import (
MarketOrderRequest, MarketOrderRequest,
) )
from core.exchanges import BaseExchange, ExchangeError, GenericAPIError from core.exchanges import BaseExchange, ExchangeError, GenericAPIError, common
class AlpacaExchange(BaseExchange): class AlpacaExchange(BaseExchange):
@@ -22,9 +22,16 @@ class AlpacaExchange(BaseExchange):
def get_account(self): def get_account(self):
return self.call("get_account") return self.call("get_account")
def get_supported_assets(self): def get_instruments(self):
request = GetAssetsRequest(status="active", asset_class="crypto") request = GetAssetsRequest(status="active", asset_class="crypto")
assets = self.call("get_all_assets", filter=request) 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"] assets = assets["itemlist"]
asset_list = [x["symbol"] for x in assets if "symbol" in x] asset_list = [x["symbol"] for x in assets if "symbol" in x]
@@ -38,6 +45,13 @@ class AlpacaExchange(BaseExchange):
except ValueError: except ValueError:
raise GenericAPIError(f"Balance is not a float: {equity}") raise GenericAPIError(f"Balance is not a float: {equity}")
common.get_balance_hook(
self.account.user.id,
self.account.user.username,
self.account.id,
self.account.name,
balance,
)
return balance return balance
def get_market_value(self, symbol): # TODO: pydantic def get_market_value(self, symbol): # TODO: pydantic
@@ -60,7 +74,7 @@ class AlpacaExchange(BaseExchange):
cast = { cast = {
"symbol": trade.symbol, "symbol": trade.symbol,
"side": direction, "side": direction,
"time_in_force": TimeInForce.IOC, "time_in_force": TimeInForce.IOC, # TODO
} }
if trade.amount is not None: if trade.amount is not None:
cast["qty"] = trade.amount cast["qty"] = trade.amount
@@ -107,8 +121,13 @@ class AlpacaExchange(BaseExchange):
trade.save() trade.save()
return order return order
def close_trade(self, trade_id, units=None): # TODO
"""
Close a trade
"""
def get_trade(self, trade_id): def get_trade(self, trade_id):
pass pass # TODO
def update_trade(self, trade): def update_trade(self, trade):
pass pass
@@ -118,7 +137,7 @@ class AlpacaExchange(BaseExchange):
def get_position_info(self, symbol): def get_position_info(self, symbol):
position = self.call("get_open_position", symbol) position = self.call("get_open_position", symbol)
return position return position # TODO: check
def get_all_positions(self): def get_all_positions(self):
items = [] items = []
@@ -130,3 +149,9 @@ class AlpacaExchange(BaseExchange):
item["unrealized_pl"] = float(item["unrealized_pl"]) item["unrealized_pl"] = float(item["unrealized_pl"])
items.append(item) items.append(item)
return 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
View 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
View 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
View File

@@ -0,0 +1,57 @@
from core.exchanges import BaseExchange
class FakeExchange(BaseExchange):
def call_method(self, request):
pass
def connect(self):
pass
def get_account(self):
pass
def get_instruments(self):
pass
def get_currencies(self, currencies):
pass
def get_supported_assets(self, response=None):
pass
def get_balance(self, return_usd=False):
pass
def get_market_value(self, symbol):
pass
def post_trade(self, trade):
pass
def close_trade(self, trade_id):
pass
def get_trade(self, trade_id):
pass
def update_trade(self, trade):
pass
def cancel_trade(self, trade_id):
pass
def get_position_info(self, symbol):
pass
def get_all_positions(self):
pass
def get_all_open_trades(self):
pass
def close_position(self, side, symbol):
pass
def close_all_positions(self):
pass

82
core/exchanges/mexc.py Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,10 @@
from oandapyV20 import API from oandapyV20 import API
from oandapyV20.endpoints import accounts, orders, positions, pricing, trades from oandapyV20.endpoints import accounts, orders, positions, pricing, trades
from core.exchanges import BaseExchange from core.exchanges import BaseExchange, common
from core.util import logs
log = logs.get_logger("oanda")
class OANDAExchange(BaseExchange): class OANDAExchange(BaseExchange):
@@ -36,10 +39,23 @@ class OANDAExchange(BaseExchange):
response = self.get_instruments() response = self.get_instruments()
return [x["name"] for x in response["itemlist"]] return [x["name"] for x in response["itemlist"]]
def get_balance(self): def get_balance(self, return_usd=False):
r = accounts.AccountSummary(self.account_id) r = accounts.AccountSummary(self.account_id)
response = self.call(r) response = self.call(r)
return float(response["balance"]) 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): def get_market_value(self, symbol):
raise NotImplementedError raise NotImplementedError
@@ -85,25 +101,68 @@ class OANDAExchange(BaseExchange):
trade.save() trade.save()
return response 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): def get_trade(self, trade_id):
# OANDA is off by one... # OANDA is off by one...
r = trades.TradeDetails(accountID=self.account_id, tradeID=trade_id) r = trades.TradeDetails(accountID=self.account_id, tradeID=trade_id)
return self.call(r) return self.call(r)
def update_trade(self, trade): def update_trade(self, trade_id, take_profit_price, stop_loss_price):
raise NotImplementedError data = {}
# r = orders.OrderReplace( if take_profit_price:
# accountID=self.account_id, orderID=trade.order_id, data=data data["takeProfit"] = {"price": str(take_profit_price)}
# ) if stop_loss_price:
# self.client.request(r) data["stopLoss"] = {"price": str(stop_loss_price)}
# return r.response r = trades.TradeCRCDO(accountID=self.account_id, tradeID=trade_id, data=data)
return self.call(r)
def cancel_trade(self, trade_id): def cancel_trade(self, trade_id):
raise NotImplementedError raise NotImplementedError
def get_position_info(self, symbol): def get_position_info(self, symbol):
r = positions.PositionDetails(self.account_id, symbol) r = positions.PositionDetails(self.account_id, symbol)
return self.call(r) response = self.call(r)
response["account"] = self.account.name
response["account_id"] = self.account.id
return response
def get_all_positions(self): def get_all_positions(self):
items = [] items = []
@@ -117,12 +176,32 @@ class OANDAExchange(BaseExchange):
items.append(item) items.append(item)
return items return items
def close_all_positions(self): def get_all_open_trades(self):
# all_positions = self.get_all_positions() r = trades.OpenTrades(accountID=self.account_id)
return self.call(r)["itemlist"]
# for position in all_positions["itemlist"]: def close_position(self, side, symbol):
# print("POS ITER", position) data = {
r = positions.PositionClose(accountID=self.account_id) f"{side}Units": "ALL",
}
r = positions.PositionClose(
accountID=self.account_id, instrument=symbol, data=data
)
response = self.call(r) response = self.call(r)
print("CLOSE ALL POSITIONS", response)
return response 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

View File

@@ -2,37 +2,27 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.forms import ModelForm from django.forms import ModelForm
from mixins.restrictions import RestrictedFormMixin
from .models import Account, Hook, Signal, Strategy, Trade, TradingTime, User from .models import ( # AssetRestriction,
Account,
ActiveManagementPolicy,
AssetGroup,
AssetRule,
Hook,
NotificationSettings,
OrderSettings,
RiskModel,
Signal,
Strategy,
Trade,
TradingTime,
User,
)
# flake8: noqa: E501 # flake8: noqa: E501
class RestrictedFormMixin:
"""
This mixin is used to restrict the queryset of a form to the current user.
The request object is passed from the view."""
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
for field in self.fields:
# Check it's not something like a CharField which has no queryset
if not hasattr(self.fields[field], "queryset"):
continue
model = self.fields[field].queryset.model
# Check if the model has a user field
try:
model._meta.get_field("user")
# Add the user to the queryset filters
self.fields[field].queryset = model.objects.filter(
user=self.request.user
)
except FieldDoesNotExist:
pass
class NewUserForm(UserCreationForm): class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True) email = forms.EmailField(required=True)
@@ -81,12 +71,14 @@ class SignalForm(RestrictedFormMixin, ModelForm):
"name", "name",
"signal", "signal",
"hook", "hook",
"type",
"direction", "direction",
) )
help_texts = { help_texts = {
"name": "Name of the signal. Informational only.", "name": "Name of the signal. Informational only.",
"signal": "The name of the signal in Drakdoo. Copy it from there.", "signal": "The name of the signal in Drakdoo. Copy it from there.",
"hook": "The hook this signal belongs to.", "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.", "direction": "The direction of the signal. This is used to determine if the signal is a buy or sell.",
} }
@@ -99,7 +91,9 @@ class AccountForm(RestrictedFormMixin, ModelForm):
"exchange", "exchange",
"api_key", "api_key",
"api_secret", "api_secret",
"initial_balance",
"sandbox", "sandbox",
"enabled",
) )
help_texts = { help_texts = {
"name": "Name of the account. Informational only.", "name": "Name of the account. Informational only.",
@@ -107,46 +101,57 @@ class AccountForm(RestrictedFormMixin, ModelForm):
"api_key": "The API key or username for the account.", "api_key": "The API key or username for the account.",
"api_secret": "The API secret or password/token for the account.", "api_secret": "The API secret or password/token for the account.",
"sandbox": "Whether to use the sandbox/demo or not.", "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(RestrictedFormMixin, 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: class Meta:
model = Strategy model = Strategy
fields = ( fields = (
"name", "name",
"description", "description",
"account", "account",
"asset_group",
"risk_model",
"trading_times", "trading_times",
"order_type", "order_settings",
"time_in_force",
"entry_signals", "entry_signals",
"exit_signals", "exit_signals",
"trend_signals",
"signal_trading_enabled",
"active_management_enabled",
"active_management_policy",
"enabled", "enabled",
"take_profit_percent",
"stop_loss_percent",
"trailing_stop_loss_percent",
"price_slippage_percent",
"callback_price_deviation_percent",
"trade_size_percent",
) )
help_texts = { help_texts = {
"name": "Name of the strategy. Informational only.", "name": "Name of the strategy. Informational only.",
"description": "Description of the strategy. Informational only.", "description": "Description of the strategy. Informational only.",
"account": "The account to use for this strategy.", "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.", "trading_times": "When the strategy will place new trades.",
"order_type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.", "order_settings": "Order settings to use for this strategy.",
"time_in_force": "The time in force controls how the order is executed.", "entry_signals": "Callbacks received to these signals will trigger a trade.",
"entry_signals": "The entry signals to attach to this strategy. 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.",
"exit_signals": "The exit signals to attach to this strategy. 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.", "enabled": "Whether the strategy is enabled.",
"take_profit_percent": "The take profit will be set at this percentage above/below the entry price.",
"stop_loss_percent": "The stop loss will be set at this percentage above/below the entry price.",
"trailing_stop_loss_percent": "The trailing stop loss will be set at this percentage above/below the entry price. A trailing stop loss will follow the price as it moves in your favor.",
"price_slippage_percent": "The price slippage is the maximum percent the price can move against you before the trade is cancelled. Limit orders will be set at this percentage against your favour. Market orders will have a price bound set if this is supported.",
"callback_price_deviation_percent": "The callback price deviation is the maximum percent the price can change between receiving the callback and acting on it. This protects against rogue or delayed callbacks. Keep it low.",
"trade_size_percent": "Percentage of the account balance to use for each trade.",
} }
entry_signals = forms.ModelMultipleChoiceField( entry_signals = forms.ModelMultipleChoiceField(
@@ -161,14 +166,20 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
help_text=Meta.help_texts["exit_signals"], help_text=Meta.help_texts["exit_signals"],
required=False, 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( trading_times = forms.ModelMultipleChoiceField(
queryset=TradingTime.objects.all(), widget=forms.CheckboxSelectMultiple queryset=TradingTime.objects.all(), widget=forms.CheckboxSelectMultiple
) )
def clean(self): def clean(self):
super(StrategyForm, self).clean() cleaned_data = super(StrategyForm, self).clean()
entry_signals = self.cleaned_data.get("entry_signals") entry_signals = cleaned_data.get("entry_signals")
exit_signals = self.cleaned_data.get("exit_signals") exit_signals = cleaned_data.get("exit_signals")
for entry in entry_signals.all(): for entry in entry_signals.all():
if entry in exit_signals.all(): if entry in exit_signals.all():
self._errors["entry_signals"] = self.error_class( self._errors["entry_signals"] = self.error_class(
@@ -205,6 +216,29 @@ class StrategyForm(RestrictedFormMixin, ModelForm):
"You cannot have entry and exit signals that are the same direction. At least one must be opposing." "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 TradeForm(RestrictedFormMixin, ModelForm):
@@ -255,3 +289,154 @@ class TradingTimeForm(RestrictedFormMixin, ModelForm):
"end_day": "The day of the week to stop trading.", "end_day": "The day of the week to stop trading.",
"end_time": "The time of day 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
View File

@@ -0,0 +1,111 @@
import stripe
from django.conf import settings
from lago_python_client import Client
from lago_python_client.exceptions import LagoApiError
from lago_python_client.models import Customer, CustomerBillingConfiguration
client = Client(api_key=settings.LAGO_API_KEY, api_url=settings.LAGO_URL)
def expand_name(first_name, last_name):
"""
Convert two name variables into one.
Last name without a first name is ignored.
:param first_name: The first name
:param last_name: The last name
:return: A string with the first and last name, or None if both are None
"""
name = None
if first_name:
name = first_name
# We only want to put the last name if we have a first name
if last_name:
name += f" {last_name}"
return name
def get_or_create(email, first_name, last_name):
"""
Get a customer ID from Stripe if one with the given email exists.
Create a customer if one does not.
Raise an exception if two or more customers matching the given email exist.
:param email: The email address of the customer
:param first_name: The first name of the customer
:param last_name: The last name of the customer
:return: The customer ID
"""
# Let's see if we're just missing the ID
matching_customers = stripe.Customer.list(email=email, limit=2)
if len(matching_customers) == 2:
# Something is horribly wrong
raise Exception(f"Two customers found for email {email}")
elif len(matching_customers) == 1:
# We found a customer. Let's copy the ID
customer = matching_customers["data"][0]
customer_id = customer["id"]
return customer_id
else:
# We didn't find anything. Create the customer
# Create a name, since we have 2 variables which could be null
name = expand_name(first_name, last_name)
cast = {"email": email}
if name:
cast["name"] = name
customer = stripe.Customer.create(**cast)
return customer.id
def update_customer_fields(user):
"""
Update the customer fields in Stripe.
"""
stripe.Customer.modify(user.stripe_id, email=user.email)
name = expand_name(user.first_name, user.last_name)
stripe.Customer.modify(user.stripe_id, name=name)
def create_or_update_customer(user):
"""
Create or update a customer in Lago.
"""
try:
customer = client.customers().find(str(user.customer_id))
except LagoApiError:
customer = None
if not customer:
customer = Customer(
external_id=str(user.customer_id),
name=f"{user.first_name} {user.last_name}",
)
customer.external_id = str(user.customer_id)
customer.email = user.email
customer.name = f"{user.first_name} {user.last_name}"
customer.billing_configuration = CustomerBillingConfiguration(
payment_provider="stripe",
provider_customer_id=str(user.stripe_id),
)
try:
created = client.customers().create(customer)
except LagoApiError as e:
print(e.response)
lago_id = created.lago_id
return lago_id
def delete_customer(user):
"""
Delete a customer from Lago.
:param user: User object to delete
"""
try:
client.customers().destroy(str(user.customer_id))
except LagoApiError:
pass

View File

@@ -1,65 +0,0 @@
import logging
import stripe
logger = logging.getLogger(__name__)
def expand_name(first_name, last_name):
"""
Convert two name variables into one.
Last name without a first name is ignored.
"""
name = None
if first_name:
name = first_name
# We only want to put the last name if we have a first name
if last_name:
name += f" {last_name}"
return name
def get_or_create(email, first_name, last_name):
"""
Get a customer ID from Stripe if one with the given email exists.
Create a customer if one does not.
Raise an exception if two or more customers matching the given email exist.
"""
# Let's see if we're just missing the ID
matching_customers = stripe.Customer.list(email=email, limit=2)
if len(matching_customers) == 2:
# Something is horribly wrong
logger.error(f"Two customers found for email {email}")
raise Exception(f"Two customers found for email {email}")
elif len(matching_customers) == 1:
# We found a customer. Let's copy the ID
customer = matching_customers["data"][0]
customer_id = customer["id"]
return customer_id
else:
# We didn't find anything. Create the customer
# Create a name, since we have 2 variables which could be null
name = expand_name(first_name, last_name)
cast = {"email": email}
if name:
cast["name"] = name
customer = stripe.Customer.create(**cast)
logger.info(f"Created new Stripe customer {customer.id} with email {email}")
return customer.id
def update_customer_fields(stripe_id, email=None, first_name=None, last_name=None):
"""
Update the customer fields in Stripe.
"""
if email:
stripe.Customer.modify(stripe_id, email=email)
logger.info(f"Modified Stripe customer {stripe_id} to have email {email}")
if first_name or last_name:
name = expand_name(first_name, last_name)
stripe.Customer.modify(stripe_id, name=name)
logger.info(f"Modified Stripe customer {stripe_id} to have email {name}")

38
core/lib/elastic.py Normal file
View 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}")

View File

@@ -1,408 +0,0 @@
from datetime import datetime
from decimal import Decimal as D
from core.exchanges import GenericAPIError
from core.models import Account, Strategy, Trade
from core.util import logs
log = logs.get_logger(__name__)
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.
"""
# Currently we only have two exchanges with different pair separators
if account.exchange == "alpaca":
separator = "/"
elif account.exchange == "oanda":
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 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
"""
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")
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"])
# 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
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 get_trade_size_in_base(direction, account, strategy, cash_balance, base):
"""
Get the trade size in the base currency.
:param direction: Direction of the trade
:param account: Account object
:param strategy: Strategy object
:param cash_balance: Cash balance in the Account's base currency
:param base: Base currency
:return: Trade size in the base currency
"""
# Convert the trade size in percent to a ratio
trade_size_as_ratio = D(strategy.trade_size_percent) / D(100)
log.debug(f"Trade size as ratio: {trade_size_as_ratio}")
# Multiply with cash balance to get the trade size in the account's
# base currency
amount_fiat = D(trade_size_as_ratio) * D(cash_balance)
log.debug(f"Trade size: {amount_fiat}")
# Convert the trade size to the base currency
if account.currency.lower() == base.lower():
trade_size_in_base = amount_fiat
else:
trade_size_in_base = to_currency(
direction, account, amount_fiat, account.currency, base
)
log.debug(f"Trade size in base: {trade_size_in_base}")
return trade_size_in_base
def get_tp(direction, take_profit_percent, price):
"""
Get the take profit price.
:param direction: Direction of the trade
:param strategy: Strategy object
:param price: Entry price
"""
# Convert to ratio
take_profit_as_ratio = D(take_profit_percent) / D(100)
log.debug(f"Take profit as ratio: {take_profit_as_ratio}")
take_profit_var = D(price) * D(take_profit_as_ratio)
log.debug(f"Take profit var: {take_profit_var}")
if direction == "buy":
take_profit = D(price) + D(take_profit_var)
elif direction == "sell":
take_profit = D(price) - D(take_profit_var)
log.debug(f"Take profit: {take_profit}")
return take_profit
def get_sl(direction, stop_loss_percent, price, return_var=False):
"""
Get the stop loss price.
Also used for trailing stop loss.
:param direction: Direction of the trade
:param strategy: Strategy object
:param price: Entry price
"""
# Convert to ratio
stop_loss_as_ratio = D(stop_loss_percent) / D(100)
log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}")
stop_loss_var = D(price) * D(stop_loss_as_ratio)
log.debug(f"Stop loss var: {stop_loss_var}")
if return_var:
return stop_loss_var
if direction == "buy":
stop_loss = D(price) - D(stop_loss_var)
elif direction == "sell":
stop_loss = D(price) + D(stop_loss_var)
log.debug(f"Stop loss: {stop_loss}")
return stop_loss
def get_tp_sl(direction, strategy, price):
"""
Get the take profit and stop loss prices.
:param direction: Direction of the trade
:param strategy: Strategy object
:param price: Price of the trade
:return: Take profit and stop loss prices
"""
take_profit = get_tp(direction, strategy.take_profit_percent, price)
stop_loss = get_sl(direction, strategy.stop_loss_percent, price)
cast = {"tp": take_profit, "sl": stop_loss}
# Look up the TSL if required by the strategy
if strategy.trailing_stop_loss_percent:
trailing_stop_loss = get_sl(
direction, strategy.trailing_stop_loss_percent, price, return_var=True
)
cast["tsl"] = trailing_stop_loss
return cast
def get_price_bound(direction, strategy, price, current_price):
"""
Get the price bound for a given price using the slippage from the strategy.
* Check that the price of the callback is within the callback price deviation of the
current price
* Calculate the price bounds such that the maximum slippage should be within the
price slippage relative to the current price.
Note that the maximum actual slippage may be as high as the sum of these two values.
:param direction: Direction of the trade
:param strategy: Strategy object
:param price: Price of the trade
:param current_price: current price from the exchange
:return: Price bound
"""
# Convert the callback price deviation to a ratio
callback_price_deviation_as_ratio = D(
strategy.callback_price_deviation_percent
) / D(100)
log.debug(f"Callback price deviation as ratio: {callback_price_deviation_as_ratio}")
maximum_price_deviation = D(current_price) * D(callback_price_deviation_as_ratio)
# Ensure the current price is within price_slippage_as_ratio of the callback price
if abs(current_price - price) <= maximum_price_deviation:
log.debug("Current price is within price deviation of callback price")
else:
log.error("Current price is not within price deviation of callback price")
log.debug(f"Difference: {abs(current_price - price)}")
return None
# Convert the maximum price slippage to a ratio
price_slippage_as_ratio = D(strategy.price_slippage_percent) / D(100)
log.debug(f"Maximum price slippage as ratio: {price_slippage_as_ratio}")
# Calculate the price bound by multiplying with the price
# The price bound is the worst price we are willing to pay for the trade
price_slippage = D(current_price) * D(price_slippage_as_ratio)
log.debug(f"Maximum deviation from callback price: {price_slippage}")
current_price_slippage = D(current_price) * D(price_slippage_as_ratio)
log.debug(f"Maximum deviation from current price: {current_price_slippage}")
# Price bound is the worst price we are willing to pay for the trade
# For buys, a higher price is worse
if direction == "buy":
price_bound = D(current_price) + D(price_slippage)
# For sells, a lower price is worse
elif direction == "sell":
price_bound = D(current_price) - D(price_slippage)
log.debug(f"Price bound: {price_bound}")
return price_bound
def execute_strategy(callback, strategy):
"""
Execute a strategy.
:param callback: Callback object
:param strategy: Strategy object
"""
# Check if we can trade now!
now_utc = datetime.utcnow()
trading_times = strategy.trading_times.all()
if not trading_times:
log.error("No trading times set for strategy")
return
matches = [x.within_range(now_utc) for x in trading_times]
if not any(matches):
log.debug("Not within trading time range")
return
# Get the account's balance in the native account currency
cash_balance = strategy.account.client.get_balance()
log.debug(f"Cash balance: {cash_balance}")
# Instruments supported by the account
if not strategy.account.instruments:
strategy.account.update_info()
# Refresh account object
strategy.account = Account.objects.get(id=strategy.account.id)
instruments = strategy.account.instruments
if not instruments:
log.error("No instruments found")
return
# Shorten some hook, strategy and callback vars for convenience
user = strategy.user
account = strategy.account
hook = callback.hook
signal = callback.signal
base = callback.base
quote = callback.quote
direction = signal.direction
# Don't be silly
if callback.exchange != account.exchange:
log.error("Market exchange differs from account exchange.")
return
# Get the pair we are trading
symbol = get_pair(account, base, quote)
if not symbol:
log.error(f"Symbol not supported by account: {symbol}")
return
# Extract the information for the symbol
instrument = strategy.account.client.extract_instrument(instruments, symbol)
if not instrument:
log.error(f"Symbol not found: {symbol}")
return
# Get the required precision
try:
trade_precision = instrument["tradeUnitsPrecision"]
display_precision = instrument["displayPrecision"]
except KeyError:
log.error(f"Precision not found for {symbol}")
return
# Round the received price to the display precision
price = round(D(callback.price), display_precision)
log.debug(f"Extracted price of quote: {price}")
type = strategy.order_type
current_price = get_price(account, direction, symbol)
log.debug(f"Callback price: {price}")
log.debug(f"Current price: {current_price}")
# Convert the trade size, which is currently in the account's base currency,
# to the base currency of the pair we are trading
trade_size_in_base = get_trade_size_in_base(
direction, account, strategy, cash_balance, base
)
# Calculate TP/SL/TSL
protection = get_tp_sl(direction, strategy, current_price)
stop_loss = protection["sl"]
take_profit = protection["tp"]
trailing_stop_loss = None
if "tsl" in protection:
trailing_stop_loss = protection["tsl"]
# Calculate price bound and round to the display precision
price_bound = get_price_bound(direction, strategy, price, current_price)
if not price_bound:
return
price_bound = round(price_bound, display_precision)
# Create object, note that the amount is rounded to the trade precision
new_trade = Trade.objects.create(
user=user,
account=account,
hook=hook,
signal=signal,
symbol=symbol,
type=type,
time_in_force=strategy.time_in_force,
# amount_fiat=amount_fiat,
amount=float(round(trade_size_in_base, trade_precision)),
# price=price_bound,
price=price_bound,
stop_loss=float(round(stop_loss, display_precision)),
take_profit=float(round(take_profit, display_precision)),
direction=direction,
)
# Add TSL if applicable
if trailing_stop_loss:
new_trade.trailing_stop_loss = float(
round(trailing_stop_loss, display_precision)
)
new_trade.save()
info = new_trade.post()
log.debug(f"Posted trade: {info}")
def process_callback(callback):
log.info(f"Received callback for {callback.hook} - {callback.signal}")
# Scan for entry
log.debug("Scanning for entry strategies...")
strategies = Strategy.objects.filter(entry_signals=callback.signal, 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.")
continue
execute_strategy(callback, strategy)
# Scan for exit
log.debug("Scanning for entry strategies...")
strategies = Strategy.objects.filter(exit_signals=callback.signal, 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.")
continue
execute_strategy(callback, strategy)

38
core/lib/notify.py Normal file
View 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)

View File

@@ -1,21 +1,21 @@
from asgiref.sync import sync_to_async # from asgiref.sync import sync_to_async
from core.models import Plan # from core.models import Plan
async def assemble_plan_map(product_id_filter=None): # async def assemble_plan_map(product_id_filter=None):
""" # """
Get all the plans from the database and create an object Stripe wants. # Get all the plans from the database and create an object Stripe wants.
""" # """
line_items = [] # line_items = []
for plan in await sync_to_async(list)(Plan.objects.all()): # for plan in await sync_to_async(list)(Plan.objects.all()):
if product_id_filter: # if product_id_filter:
if plan.product_id != product_id_filter: # if plan.product_id != product_id_filter:
continue # continue
line_items.append( # line_items.append(
{ # {
"price": plan.product_id, # "price": plan.product_id,
"quantity": 1, # "quantity": 1,
} # }
) # )
return line_items # return line_items

View File

@@ -1 +1 @@
from core.lib.schemas import alpaca_s, drakdoo_s, oanda_s # noqa from core.lib.schemas import alpaca_s, drakdoo_s, oanda_s, mexc_s # noqa

View File

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

View File

@@ -1,29 +1,30 @@
from decimal import Decimal as D from decimal import Decimal as D
from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
class PositionLong(BaseModel): class PositionLong(BaseModel):
units: str units: str
averagePrice: str | None averagePrice: Optional[str] = None
pl: str pl: str
resettablePL: str resettablePL: str
financing: str financing: str
dividendAdjustment: str dividendAdjustment: str
guaranteedExecutionFees: str guaranteedExecutionFees: str
tradeIDs: list[str] | None tradeIDs: Optional[list[str]] = []
unrealizedPL: str unrealizedPL: str
class PositionShort(BaseModel): class PositionShort(BaseModel):
units: str units: str
averagePrice: str | None averagePrice: Optional[str] = None
pl: str pl: str
resettablePL: str resettablePL: str
financing: str financing: str
dividendAdjustment: str dividendAdjustment: str
guaranteedExecutionFees: str guaranteedExecutionFees: str
tradeIDs: list[str] | None tradeIDs: Optional[list[str]] = []
unrealizedPL: str unrealizedPL: str
@@ -46,7 +47,32 @@ class OpenPositions(BaseModel):
lastTransactionID: str 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): def parse_prices(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0: if float(x["long"]["units"]) > 0:
return x["long"]["averagePrice"] return x["long"]["averagePrice"]
elif float(x["short"]["units"]) < 0: elif float(x["short"]["units"]) < 0:
@@ -56,6 +82,7 @@ def parse_prices(x):
def parse_units(x): def parse_units(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0: if float(x["long"]["units"]) > 0:
return x["long"]["units"] return x["long"]["units"]
elif float(x["short"]["units"]) < 0: elif float(x["short"]["units"]) < 0:
@@ -65,6 +92,7 @@ def parse_units(x):
def parse_value(x): def parse_value(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0: if float(x["long"]["units"]) > 0:
return D(x["long"]["units"]) * D(x["long"]["averagePrice"]) return D(x["long"]["units"]) * D(x["long"]["averagePrice"])
elif float(x["short"]["units"]) < 0: elif float(x["short"]["units"]) < 0:
@@ -73,7 +101,15 @@ def parse_value(x):
return 0 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): def parse_side(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0: if float(x["long"]["units"]) > 0:
return "long" return "long"
elif float(x["short"]["units"]) < 0: elif float(x["short"]["units"]) < 0:
@@ -83,6 +119,7 @@ def parse_side(x):
def parse_trade_ids(x, sum=0): def parse_trade_ids(x, sum=0):
prevent_hedging(x)
if float(x["long"]["units"]) > 0: if float(x["long"]["units"]) > 0:
return [str(int(y) + sum) for y in x["long"]["tradeIDs"]] return [str(int(y) + sum) for y in x["long"]["tradeIDs"]]
elif float(x["short"]["units"]) < 0: elif float(x["short"]["units"]) < 0:
@@ -270,7 +307,7 @@ class PositionDetailsNested(BaseModel):
dividendAdjustment: str dividendAdjustment: str
guaranteedExecutionFees: str guaranteedExecutionFees: str
unrealizedPL: str unrealizedPL: str
marginUsed: str marginUsed: Optional[str] = None
class PositionDetails(BaseModel): class PositionDetails(BaseModel):
@@ -337,7 +374,9 @@ class Instrument(BaseModel):
guaranteedStopLossOrderMode: str guaranteedStopLossOrderMode: str
tags: list[InstrumentTag] tags: list[InstrumentTag]
financing: InstrumentFinancing financing: InstrumentFinancing
guaranteedStopLossOrderLevelRestriction: InstrumentGuaranteedRestriction | None guaranteedStopLossOrderLevelRestriction: Optional[
InstrumentGuaranteedRestriction
] = None
class AccountInstruments(BaseModel): class AccountInstruments(BaseModel):
@@ -371,41 +410,6 @@ AccountInstrumentsSchema = {
} }
class OrderTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
instrument: str
units: str
timeInForce: str
positionFill: str
reason: str
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 PriceBid(BaseModel): class PriceBid(BaseModel):
price: str price: str
liquidity: int liquidity: int
@@ -459,3 +463,292 @@ PricingInfoSchema = {
], ],
), ),
} }
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",
}

View File

@@ -0,0 +1,52 @@
import asyncio
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from asgiref.sync import sync_to_async
from django.core.management.base import BaseCommand
from core.models import Strategy
from core.trading import active_management
from core.util import logs
log = logs.get_logger("scheduling")
INTERVAL = 5
async def job():
"""
Run all schedules matching the given interval.
:param interval_seconds: The interval to run.
"""
strategies = await sync_to_async(list)(
Strategy.objects.filter(enabled=True, active_management_enabled=True)
)
log.debug(f"Found {len(strategies)} strategies")
for strategy in strategies:
log.debug(f"Running strategy {strategy.name}")
ams = active_management.ActiveManagement(strategy) # noqa
ams.run_checks()
ams.execute_actions()
class Command(BaseCommand):
def handle(self, *args, **options):
"""
Start the scheduling process.
"""
scheduler = AsyncIOScheduler()
log.debug(f"Scheduling checking process job every {INTERVAL} seconds")
scheduler.add_job(job, "interval", seconds=INTERVAL)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
scheduler._eventloop = loop
scheduler.start()
try:
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
log.info("Process terminating")
finally:
scheduler.shutdown(wait=False)
loop.close()

View File

@@ -30,8 +30,7 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('stripe_id', models.CharField(blank=True, max_length=255, null=True)), ('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
('last_payment', models.DateTimeField(blank=True, null=True)),
('email', models.EmailField(max_length=254, unique=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')), ('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()), ('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( migrations.AddField(
model_name='user', model_name='user',
name='user_permissions', name='user_permissions',

View File

@@ -0,0 +1,231 @@
# Generated by Django 4.1.7 on 2023-02-24 13:18
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('exchange', models.CharField(choices=[('alpaca', 'Alpaca'), ('oanda', 'OANDA'), ('fake', 'Fake')], max_length=255)),
('api_key', models.CharField(max_length=255)),
('api_secret', models.CharField(max_length=255)),
('sandbox', models.BooleanField(default=False)),
('enabled', models.BooleanField(default=True)),
('supported_symbols', models.JSONField(default=list)),
('instruments', models.JSONField(default=list)),
('currency', models.CharField(blank=True, max_length=255, null=True)),
('initial_balance', models.FloatField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ActiveManagementPolicy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('when_trading_time_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_trends_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_position_size_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only'), ('adjust', 'Adjust violating trades')], default='none', max_length=255)),
('when_protection_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only'), ('adjust', 'Adjust violating trades')], default='none', max_length=255)),
('when_asset_groups_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_max_open_trades_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_max_open_trades_per_symbol_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_max_loss_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_max_risk_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('when_crossfilter_violated', models.CharField(choices=[('none', 'None'), ('close', 'Close violating trades'), ('notify', 'Notify only')], default='none', max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='AssetGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('webhook_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('when_no_data', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=7)),
('when_no_match', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
('when_no_aggregation', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
('when_not_in_bounds', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=6)),
('when_bullish', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=2)),
('when_bearish', models.IntegerField(choices=[(6, 'Always allow'), (7, 'Always deny'), (2, 'Bullish'), (3, 'Bearish')], default=3)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Hook',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=1024)),
('hook', models.CharField(max_length=255, unique=True)),
('received', models.IntegerField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='OrderSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('order_type', models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], default='market', max_length=255)),
('time_in_force', models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255)),
('take_profit_percent', models.FloatField(default=1.5)),
('stop_loss_percent', models.FloatField(default=1.0)),
('trailing_stop_loss_percent', models.FloatField(blank=True, default=1.0, null=True)),
('trade_size_percent', models.FloatField(default=0.5)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='RiskModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('max_loss_percent', models.FloatField(default=0.05)),
('max_risk_percent', models.FloatField(default=0.05)),
('max_open_trades', models.IntegerField(default=10)),
('max_open_trades_per_symbol', models.IntegerField(default=2)),
('price_slippage_percent', models.FloatField(default=2.5)),
('callback_price_deviation_percent', models.FloatField(default=0.5)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Signal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=1024)),
('signal', models.CharField(max_length=256)),
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
('received', models.IntegerField(default=0)),
('type', models.CharField(choices=[('entry', 'Entry'), ('exit', 'Exit'), ('trend', 'Trend')], max_length=255)),
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='TradingTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('start_day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')])),
('end_day', models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')])),
('start_time', models.TimeField()),
('end_time', models.TimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Trade',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('symbol', models.CharField(max_length=255)),
('time_in_force', models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255)),
('type', models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], max_length=255)),
('amount', models.FloatField(blank=True, null=True)),
('amount_usd', models.FloatField(blank=True, null=True)),
('price', models.FloatField(blank=True, null=True)),
('stop_loss', models.FloatField(blank=True, null=True)),
('trailing_stop_loss', models.FloatField(blank=True, null=True)),
('take_profit', models.FloatField(blank=True, null=True)),
('status', models.CharField(blank=True, max_length=255, null=True)),
('information', models.JSONField(blank=True, null=True)),
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
('order_id', models.CharField(blank=True, max_length=255, null=True)),
('client_order_id', models.CharField(blank=True, max_length=255, null=True)),
('response', models.JSONField(blank=True, null=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
('signal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.signal')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Strategy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True, null=True)),
('enabled', models.BooleanField(default=False)),
('signal_trading_enabled', models.BooleanField(default=False)),
('active_management_enabled', models.BooleanField(default=False)),
('trends', models.JSONField(blank=True, null=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
('active_management_policy', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.activemanagementpolicy')),
('asset_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.assetgroup')),
('entry_signals', models.ManyToManyField(blank=True, related_name='entry_strategies', to='core.signal')),
('exit_signals', models.ManyToManyField(blank=True, related_name='exit_strategies', to='core.signal')),
('order_settings', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.ordersettings')),
('risk_model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.riskmodel')),
('trading_times', models.ManyToManyField(to='core.tradingtime')),
('trend_signals', models.ManyToManyField(blank=True, related_name='trend_strategies', to='core.signal')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'strategies',
},
),
migrations.CreateModel(
name='NotificationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Callback',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(blank=True, max_length=1024, null=True)),
('message', models.CharField(blank=True, max_length=1024, null=True)),
('period', models.CharField(blank=True, max_length=255, null=True)),
('sent', models.BigIntegerField(blank=True, null=True)),
('trade', models.BigIntegerField(blank=True, null=True)),
('exchange', models.CharField(blank=True, max_length=255, null=True)),
('base', models.CharField(blank=True, max_length=255, null=True)),
('quote', models.CharField(blank=True, max_length=255, null=True)),
('contract', models.CharField(blank=True, max_length=255, null=True)),
('price', models.FloatField(blank=True, null=True)),
('symbol', models.CharField(max_length=255)),
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
('signal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.signal')),
],
),
migrations.CreateModel(
name='AssetRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('asset', models.CharField(max_length=64)),
('aggregation', models.CharField(choices=[('none', 'None'), ('avg_sentiment', 'Average sentiment')], default='none', max_length=255)),
('value', models.FloatField(blank=True, null=True)),
('original_status', models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'Always allow'), (7, 'Always deny')], default=0)),
('status', models.IntegerField(choices=[(0, 'No data'), (1, 'No match'), (2, 'Bullish'), (3, 'Bearish'), (4, 'No aggregation'), (5, 'Not in bounds'), (6, 'Always allow'), (7, 'Always deny')], default=0)),
('trigger_below', models.FloatField(blank=True, null=True)),
('trigger_above', models.FloatField(blank=True, null=True)),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.assetgroup')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('asset', 'group')},
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.6 on 2022-10-12 09:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='session',
name='session',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-14 23:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_session_session'),
]
operations = [
migrations.CreateModel(
name='Hook',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=1024, null=True)),
('hook', models.CharField(max_length=255)),
('received', models.IntegerField(default=0)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.1.7 on 2023-02-24 13:21
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_account_activemanagementpolicy_assetgroup_hook_and_more'),
]
operations = [
migrations.AddField(
model_name='user',
name='customer_id',
field=models.UUIDField(blank=True, default=uuid.uuid4, null=True),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-15 18:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_hook'),
]
operations = [
migrations.CreateModel(
name='Callback',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.JSONField()),
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
],
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.1.2 on 2022-10-17 18:07 # Generated by Django 4.1.7 on 2023-02-24 16:09
from django.db import migrations, models from django.db import migrations, models
@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('core', '0008_trade'), ('core', '0003_user_customer_id'),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='trade', model_name='user',
name='exchange_id', name='stripe_id',
field=models.CharField(blank=True, max_length=255, null=True), field=models.CharField(blank=True, max_length=255, null=True),
), ),
] ]

View File

@@ -1,77 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-15 22:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_callback'),
]
operations = [
migrations.RemoveField(
model_name='callback',
name='data',
),
migrations.AddField(
model_name='callback',
name='market',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='callback',
name='market_contract',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='callback',
name='market_currency',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='callback',
name='market_exchange',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='callback',
name='market_item',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='callback',
name='message',
field=models.CharField(blank=True, max_length=1024, null=True),
),
migrations.AddField(
model_name='callback',
name='period',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='callback',
name='timestamp_sent',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='callback',
name='timestamp_trade',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='callback',
name='title',
field=models.CharField(blank=True, max_length=1024, null=True),
),
migrations.AlterField(
model_name='hook',
name='hook',
field=models.CharField(max_length=255, unique=True),
),
migrations.AlterField(
model_name='hook',
name='name',
field=models.CharField(blank=True, max_length=1024, null=True, unique=True),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-16 13:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_remove_callback_data_callback_market_and_more'),
]
operations = [
migrations.RemoveField(
model_name='callback',
name='market',
),
migrations.AlterField(
model_name='callback',
name='timestamp_sent',
field=models.BigIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='callback',
name='timestamp_trade',
field=models.BigIntegerField(blank=True, null=True),
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-17 17:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0006_remove_callback_market_alter_callback_timestamp_sent_and_more'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('exchange', models.CharField(max_length=255)),
('api_key', models.CharField(max_length=255)),
('api_secret', models.CharField(max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -1,28 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-17 17:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0007_account'),
]
operations = [
migrations.CreateModel(
name='Trade',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('symbol', models.CharField(max_length=255)),
('type', models.CharField(max_length=255)),
('amount', models.FloatField()),
('price', models.FloatField()),
('stop_loss', models.FloatField(blank=True, null=True)),
('take_profit', models.FloatField(blank=True, null=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.account')),
('hook', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
],
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-17 18:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0009_trade_exchange_id'),
]
operations = [
migrations.AddField(
model_name='account',
name='sandbox',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='trade',
name='direction',
field=models.CharField(blank=True, choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255, null=True),
),
migrations.AddField(
model_name='trade',
name='status',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AlterField(
model_name='trade',
name='symbol',
field=models.CharField(choices=[('BTCUSD', 'Bitcoin/USD')], max_length=255),
),
migrations.AlterField(
model_name='trade',
name='type',
field=models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], max_length=255),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-18 08:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0010_account_sandbox_trade_direction_trade_status_and_more'),
]
operations = [
migrations.AlterField(
model_name='account',
name='exchange',
field=models.CharField(choices=[('binance', 'Binance'), ('alpaca', 'Alpaca')], max_length=255),
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-18 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0011_alter_account_exchange'),
]
operations = [
migrations.RenameField(
model_name='trade',
old_name='exchange_id',
new_name='client_order_id',
),
migrations.AddField(
model_name='trade',
name='order_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='trade',
name='response',
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name='trade',
name='symbol',
field=models.CharField(choices=[('BTC/USD', 'Bitcoin/US Dollar'), ('LTC/USD', 'Litecoin/US Dollar')], max_length=255),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-18 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0012_rename_exchange_id_trade_client_order_id_and_more'),
]
operations = [
migrations.AlterField(
model_name='trade',
name='direction',
field=models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], default='buy', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='trade',
name='price',
field=models.FloatField(blank=True, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-21 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0013_alter_trade_direction_alter_trade_price'),
]
operations = [
migrations.AlterField(
model_name='account',
name='exchange',
field=models.CharField(choices=[('alpaca', 'Alpaca')], max_length=255),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-10 18:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0020_rename_market_item_callback_base_and_more'),
]
operations = [
migrations.AddField(
model_name='account',
name='instruments',
field=models.JSONField(default=list),
),
migrations.AlterField(
model_name='account',
name='exchange',
field=models.CharField(choices=[('alpaca', 'Alpaca'), ('oanda', 'OANDA')], max_length=255),
),
migrations.AlterField(
model_name='strategy',
name='take_profit_percent',
field=models.FloatField(default=1.5),
),
migrations.AlterField(
model_name='strategy',
name='trade_size_percent',
field=models.FloatField(default=0.5),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-10 18:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0021_account_instruments_alter_account_exchange_and_more'),
]
operations = [
migrations.AddField(
model_name='account',
name='currency',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-15 15:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0022_account_currency'),
]
operations = [
migrations.AlterModelOptions(
name='strategy',
options={'verbose_name_plural': 'strategies'},
),
migrations.AddField(
model_name='strategy',
name='callback_price_deviation_percent',
field=models.FloatField(default=0.5),
),
migrations.AddField(
model_name='strategy',
name='order_type',
field=models.CharField(choices=[('market', 'Market'), ('limit', 'Limit')], default='market', max_length=255),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-15 15:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0023_alter_strategy_options_and_more'),
]
operations = [
migrations.AddField(
model_name='strategy',
name='time_in_force',
field=models.CharField(choices=[('gtc', 'Good Til Cancelled'), ('gfd', 'Good For Day'), ('fok', 'Fill Or Kill'), ('ioc', 'Immediate Or Cancel')], default='gtc', max_length=255),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-15 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0024_strategy_time_in_force'),
]
operations = [
migrations.AlterField(
model_name='strategy',
name='time_in_force',
field=models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-15 15:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0025_alter_strategy_time_in_force'),
]
operations = [
migrations.AddField(
model_name='trade',
name='time_in_force',
field=models.CharField(choices=[('gtc', 'GTC (Good Til Cancelled)'), ('gfd', 'GFD (Good For Day)'), ('fok', 'FOK (Fill Or Kill)'), ('ioc', 'IOC (Immediate Or Cancel)')], default='gtc', max_length=255),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-15 15:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0026_trade_time_in_force'),
]
operations = [
migrations.AddField(
model_name='strategy',
name='trailing_stop_loss_percent',
field=models.FloatField(blank=True, default=1.0, null=True),
),
migrations.AddField(
model_name='trade',
name='trailing_stop_loss',
field=models.FloatField(blank=True, null=True),
),
]

View File

@@ -1,26 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 17:39
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0027_strategy_trailing_stop_loss_percent_and_more'),
]
operations = [
migrations.CreateModel(
name='TradingTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=255, null=True)),
('description', models.TextField(blank=True, null=True)),
('start_ts', models.DateTimeField()),
('end_ts', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 17:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0028_tradingtime'),
]
operations = [
migrations.AlterField(
model_name='tradingtime',
name='name',
field=models.CharField(default='DEFAULT', max_length=255),
preserve_default=False,
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 17:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0029_alter_tradingtime_name'),
]
operations = [
migrations.RemoveField(
model_name='tradingtime',
name='end_ts',
),
migrations.RemoveField(
model_name='tradingtime',
name='start_ts',
),
migrations.AddField(
model_name='tradingtime',
name='end_day',
field=models.CharField(choices=[('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday')], default='monday', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='tradingtime',
name='end_time',
field=models.TimeField(default='12:00'),
preserve_default=False,
),
migrations.AddField(
model_name='tradingtime',
name='start_day',
field=models.CharField(choices=[('monday', 'Monday'), ('tuesday', 'Tuesday'), ('wednesday', 'Wednesday'), ('thursday', 'Thursday'), ('friday', 'Friday'), ('saturday', 'Saturday'), ('sunday', 'Sunday')], default='monday', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='tradingtime',
name='start_time',
field=models.TimeField(default='12:00'),
preserve_default=False,
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 18:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0030_remove_tradingtime_end_ts_and_more'),
]
operations = [
migrations.AddField(
model_name='strategy',
name='trading_times',
field=models.ManyToManyField(blank=True, to='core.tradingtime'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 18:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0031_strategy_trading_times'),
]
operations = [
migrations.AlterField(
model_name='tradingtime',
name='end_day',
field=models.CharField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], max_length=255),
),
migrations.AlterField(
model_name='tradingtime',
name='start_day',
field=models.CharField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], max_length=255),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 18:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0032_alter_tradingtime_end_day_and_more'),
]
operations = [
migrations.AlterField(
model_name='tradingtime',
name='end_day',
field=models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]),
),
migrations.AlterField(
model_name='tradingtime',
name='start_day',
field=models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')]),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 18:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0033_alter_tradingtime_end_day_and_more'),
]
operations = [
migrations.AlterField(
model_name='strategy',
name='trading_times',
field=models.ManyToManyField(to='core.tradingtime'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-25 19:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0034_alter_strategy_trading_times'),
]
operations = [
migrations.AlterField(
model_name='tradingtime',
name='end_day',
field=models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')]),
),
migrations.AlterField(
model_name='tradingtime',
name='start_day',
field=models.IntegerField(choices=[(1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday'), (7, 'Sunday')]),
),
]

View File

@@ -1,37 +0,0 @@
# Generated by Django 4.1.3 on 2022-12-01 18:22
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0035_alter_tradingtime_end_day_and_more'),
]
operations = [
migrations.RemoveField(
model_name='hook',
name='direction',
),
migrations.AlterField(
model_name='hook',
name='name',
field=models.CharField(default='Unknown', max_length=1024),
preserve_default=False,
),
migrations.CreateModel(
name='Signal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=1024)),
('signal', models.CharField(max_length=256)),
('direction', models.CharField(choices=[('buy', 'Buy'), ('sell', 'Sell')], max_length=255)),
('received', models.IntegerField(default=0)),
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 4.1.3 on 2022-12-01 18:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0036_remove_hook_direction_alter_hook_name_signal'),
]
operations = [
migrations.AddField(
model_name='callback',
name='signal',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='core.signal'),
preserve_default=False,
),
]

View File

@@ -1,33 +0,0 @@
# Generated by Django 4.1.3 on 2022-12-01 18:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0037_callback_signal'),
]
operations = [
migrations.RemoveField(
model_name='strategy',
name='hooks',
),
migrations.AddField(
model_name='strategy',
name='entry_signals',
field=models.ManyToManyField(related_name='entry_strategies', to='core.signal'),
),
migrations.AddField(
model_name='strategy',
name='exit_signals',
field=models.ManyToManyField(related_name='exit_signals', to='core.signal'),
),
migrations.AddField(
model_name='trade',
name='signal',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.signal'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.3 on 2022-12-01 18:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0038_remove_strategy_hooks_strategy_entry_signals_and_more'),
]
operations = [
migrations.AlterField(
model_name='strategy',
name='exit_signals',
field=models.ManyToManyField(related_name='exit_strategies', to='core.signal'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.3 on 2022-12-01 18:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0039_alter_strategy_exit_signals'),
]
operations = [
migrations.AlterField(
model_name='strategy',
name='entry_signals',
field=models.ManyToManyField(blank=True, null=True, related_name='entry_strategies', to='core.signal'),
),
migrations.AlterField(
model_name='strategy',
name='exit_signals',
field=models.ManyToManyField(blank=True, null=True, related_name='exit_strategies', to='core.signal'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 4.1.3 on 2022-12-01 18:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0040_alter_strategy_entry_signals_and_more'),
]
operations = [
migrations.AlterField(
model_name='strategy',
name='entry_signals',
field=models.ManyToManyField(blank=True, related_name='entry_strategies', to='core.signal'),
),
migrations.AlterField(
model_name='strategy',
name='exit_signals',
field=models.ManyToManyField(blank=True, related_name='exit_strategies', to='core.signal'),
),
]

View File

@@ -1,3 +1,4 @@
import uuid
from datetime import timedelta from datetime import timedelta
import stripe import stripe
@@ -6,12 +7,21 @@ from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from core.exchanges.alpaca import AlpacaExchange 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.exchanges.oanda import OANDAExchange
from core.lib.customers import get_or_create, update_customer_fields
# from core.lib.customers import get_or_create, update_customer_fields
from core.lib import billing
from core.util import logs from core.util import logs
log = logs.get_logger(__name__) log = logs.get_logger(__name__)
EXCHANGE_MAP = {"alpaca": AlpacaExchange, "oanda": OANDAExchange} EXCHANGE_MAP = {
"alpaca": AlpacaExchange,
"oanda": OANDAExchange,
"mexc": MEXCExchange,
"fake": FakeExchange,
}
TYPE_CHOICES = ( TYPE_CHOICES = (
("market", "Market"), ("market", "Market"),
("limit", "Limit"), ("limit", "Limit"),
@@ -35,75 +45,125 @@ DAY_CHOICES = (
(6, "Saturday"), (6, "Saturday"),
(7, "Sunday"), (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): MAPPING_CHOICES = (
name = models.CharField(max_length=255, unique=True) (6, "Always allow"),
description = models.CharField(max_length=1024, null=True, blank=True) (7, "Always deny"),
cost = models.IntegerField() (2, "Bullish"),
product_id = models.CharField(max_length=255, unique=True, null=True, blank=True) (3, "Bearish"),
image = models.CharField(max_length=1024, null=True, blank=True) )
def __str__(self): CLOSE_NOTIFY_CHOICES = (
return f"{self.name}{self.cost})" ("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): class User(AbstractUser):
# Stripe customer ID # Stripe customer ID
stripe_id = models.CharField(max_length=255, null=True, blank=True) stripe_id = models.CharField(max_length=255, null=True, blank=True)
last_payment = models.DateTimeField(null=True, blank=True) customer_id = models.UUIDField(default=uuid.uuid4, null=True, blank=True)
plans = models.ManyToManyField(Plan, 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) 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): def delete(self, *args, **kwargs):
if settings.STRIPE_ENABLED: if settings.BILLING_ENABLED:
if self.stripe_id: if self.stripe_id:
stripe.Customer.delete(self.stripe_id) stripe.Customer.delete(self.stripe_id)
log.info(f"Deleted Stripe customer {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) super().delete(*args, **kwargs)
def has_plan(self, plan): # Override save to update attributes in Lago
plan_list = [plan.name for plan in self.plans.all()] def save(self, *args, **kwargs):
return plan in plan_list 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): 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) user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
exchange = models.CharField(choices=EXCHANGE_CHOICES, max_length=255) exchange = models.CharField(choices=EXCHANGE_CHOICES, max_length=255)
api_key = models.CharField(max_length=255) api_key = models.CharField(max_length=255)
api_secret = models.CharField(max_length=255) api_secret = models.CharField(max_length=255)
sandbox = models.BooleanField(default=False) sandbox = models.BooleanField(default=False)
enabled = models.BooleanField(default=True)
supported_symbols = models.JSONField(default=list) supported_symbols = models.JSONField(default=list)
instruments = models.JSONField(default=list) instruments = models.JSONField(default=list)
currency = models.CharField(max_length=255, null=True, blank=True) currency = models.CharField(max_length=255, null=True, blank=True)
initial_balance = models.FloatField(default=0)
def __str__(self): def __str__(self):
name = f"{self.name} ({self.exchange})" name = f"{self.name} ({self.exchange})"
@@ -116,10 +176,12 @@ class Account(models.Model):
if client: if client:
response = client.get_instruments() response = client.get_instruments()
supported_symbols = client.get_supported_assets(response) supported_symbols = client.get_supported_assets(response)
currency = client.get_account()["currency"] acct_info = client.get_account()
log.debug(f"Supported symbols for {self.name}: {supported_symbols}") log.debug(f"Supported symbols for {self.name}: {supported_symbols}")
self.supported_symbols = supported_symbols self.supported_symbols = supported_symbols
self.instruments = response self.instruments = response
if "currency" in acct_info.keys():
currency = acct_info["currency"]
self.currency = currency self.currency = currency
if save: if save:
self.save() self.save()
@@ -128,6 +190,7 @@ class Account(models.Model):
""" """
Override the save function to update supported symbols. Override the save function to update supported symbols.
""" """
if self.exchange != "fake":
self.update_info(save=False) self.update_info(save=False)
super().save(*args, **kwargs) super().save(*args, **kwargs)
@@ -155,13 +218,9 @@ class Account(models.Model):
def get_by_id(cls, account_id, user): def get_by_id(cls, account_id, user):
return cls.objects.get(id=account_id, user=user) return cls.objects.get(id=account_id, user=user)
@classmethod
class Session(models.Model): def get_by_id_no_user_check(cls, account_id):
user = models.ForeignKey(User, on_delete=models.CASCADE) return cls.objects.get(id=account_id)
request = models.CharField(max_length=255, null=True, blank=True)
session = models.CharField(max_length=255, null=True, blank=True)
subscription_id = models.CharField(max_length=255, null=True, blank=True)
plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE)
class Hook(models.Model): class Hook(models.Model):
@@ -181,9 +240,10 @@ class Signal(models.Model):
hook = models.ForeignKey(Hook, on_delete=models.CASCADE) hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255) direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
received = models.IntegerField(default=0) received = models.IntegerField(default=0)
type = models.CharField(choices=SIGNAL_TYPE_CHOICES, max_length=255)
def __str__(self): def __str__(self):
return f"{self.name} ({self.signal}) - {self.direction}" return f"{self.name} ({self.hook.name}) - {self.direction}"
class Trade(models.Model): class Trade(models.Model):
@@ -201,6 +261,7 @@ class Trade(models.Model):
trailing_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) take_profit = models.FloatField(null=True, blank=True)
status = models.CharField(max_length=255, 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) direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
# To populate from the trade # To populate from the trade
@@ -213,6 +274,10 @@ class Trade(models.Model):
self._original = self self._original = self
def post(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) return self.account.client.post_trade(self)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
@@ -224,12 +289,16 @@ class Trade(models.Model):
return cls.objects.get(id=trade_id, user=user) return cls.objects.get(id=trade_id, user=user)
@classmethod @classmethod
def get_by_id_or_order(cls, trade_id, user): def get_by_id_or_order(cls, trade_id, account_id, user):
try: try:
return cls.objects.get(id=trade_id, user=user) 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: except cls.DoesNotExist:
try: try:
return cls.objects.get(order_id=trade_id, user=user) return cls.objects.get(order_id=trade_id, account=account, user=user)
except cls.DoesNotExist: except cls.DoesNotExist:
return None return None
@@ -319,23 +388,36 @@ class Strategy(models.Model):
description = models.TextField(null=True, blank=True) description = models.TextField(null=True, blank=True)
account = models.ForeignKey(Account, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE)
trading_times = models.ManyToManyField(TradingTime) trading_times = models.ManyToManyField(TradingTime)
order_type = models.CharField(
choices=TYPE_CHOICES, max_length=255, default="market"
)
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
entry_signals = models.ManyToManyField( entry_signals = models.ManyToManyField(
Signal, related_name="entry_strategies", blank=True Signal, related_name="entry_strategies", blank=True
) )
exit_signals = models.ManyToManyField( exit_signals = models.ManyToManyField(
Signal, related_name="exit_strategies", blank=True Signal, related_name="exit_strategies", blank=True
) )
trend_signals = models.ManyToManyField(
Signal, related_name="trend_strategies", blank=True
)
enabled = models.BooleanField(default=False) enabled = models.BooleanField(default=False)
take_profit_percent = models.FloatField(default=1.5) signal_trading_enabled = models.BooleanField(default=False)
stop_loss_percent = models.FloatField(default=1.0) active_management_enabled = models.BooleanField(default=False)
trailing_stop_loss_percent = models.FloatField(default=1.0, null=True, blank=True) trends = models.JSONField(null=True, blank=True)
price_slippage_percent = models.FloatField(default=2.5)
callback_price_deviation_percent = models.FloatField(default=0.5) asset_group = models.ForeignKey(
trade_size_percent = models.FloatField(default=0.5) "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: class Meta:
verbose_name_plural = "strategies" verbose_name_plural = "strategies"
@@ -344,20 +426,132 @@ class Strategy(models.Model):
return self.name return self.name
# class Perms(models.Model): class NotificationSettings(models.Model):
# class Meta: user = models.OneToOneField(User, on_delete=models.CASCADE)
# permissions = ( ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
# ("bypass_hashing", "Can bypass field hashing"), # ntfy_url = models.CharField(max_length=255, null=True, blank=True)
# ("bypass_blacklist", "Can bypass the blacklist"), #
# ("bypass_encryption", "Can bypass field encryption"), # def __str__(self):
# ("bypass_obfuscation", "Can bypass field obfuscation"), # return f"Notification settings for {self.user}"
# ("bypass_delay", "Can bypass data delay"), #
# ("bypass_randomisation", "Can bypass data randomisation"), #
# ("post_irc", "Can post to IRC"), class RiskModel(models.Model):
# ("post_discord", "Can post to Discord"), user = models.ForeignKey(User, on_delete=models.CASCADE)
# ("query_search", "Can search with query strings"), # name = models.CharField(max_length=255)
# ("use_insights", "Can use the Insights page"), description = models.TextField(null=True, blank=True)
# ("index_int", "Can use the internal index"), # Maximum amount of money to have lost from the initial balance to stop trading
# ("index_meta", "Can use the meta index"), max_loss_percent = models.FloatField(default=0.05)
# ("restricted_sources", "Can access restricted sources"), # 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

View File

@@ -1,8 +1,9 @@
{% load static %} {% load static %}
{% load has_plan %} {% load cache %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en-GB"> <html lang="en-GB">
{% cache 600 head request.path_info %}
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -180,9 +181,25 @@
} }
</style> </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> </head>
{% endcache %}
<body> <body>
{% cache 600 nav request.user.id %}
<nav class="navbar" role="navigation" aria-label="main navigation"> <nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item" href="{% url 'home' %}"> <a class="navbar-item" href="{% url 'home' %}">
@@ -202,57 +219,74 @@
Home Home
</a> </a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
Manage Exchange
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'profit' type='page' %}">
Profit
</a>
<a class="navbar-item" href="{% url 'positions' type='page' %}"> <a class="navbar-item" href="{% url 'positions' type='page' %}">
Positions Positions
</a> </a>
<a class="navbar-item" href="{% url 'trades' type='page' %}"> <a class="navbar-item" href="{% url 'trades' type='page' %}">
Bot Trades 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
</a>
<a class="navbar-item" href="{% url 'signals' type='page' %}">
Signals
</a> </a>
<a class="navbar-item" href="{% url 'accounts' type='page' %}"> <a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts Accounts
</a> </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' %}"> <a class="navbar-item" href="{% url 'strategies' type='page' %}">
Strategies Strategies
</a> </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> </div>
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
Limits Account
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<!-- <a class="navbar-item" href="#"> <a class="navbar-item" href="{% url 'two_factor:profile' %}">
Directions Security
</a> --> </a>
<a class="navbar-item" href="{% url 'tradingtimes' type='page' %}"> <a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
Trading Times Notifications
</a> </a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if settings.STRIPE_ENABLED %} {% if settings.BILLING_ENABLED %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'billing' %}"> <a class="navbar-item" href="{% url 'billing' %}">
Billing Billing
@@ -268,17 +302,19 @@
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
{% if not user.is_authenticated %} {% if not user.is_authenticated %}
<a class="button is-info" href="{% url 'signup' %}"> <a class="button" href="{% url 'signup' %}">
<strong>Sign up</strong> <strong>Sign up</strong>
</a> </a>
<a class="button is-light" href="{% url 'two_factor:login' %}"> <a class="button" href="{% url 'two_factor:login' %}">
Log in Log in
</a> </a>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="button" href="{% url 'two_factor:profile' %}">Security</a> <form method="POST" action="{% url 'logout' %}" style="display:inline;">
<a class="button" href="{% url 'logout' %}">Logout</a> {% csrf_token %}
<button type="submit" class="button">Logout</button>
</form>
{% endif %} {% endif %}
</div> </div>
@@ -286,6 +322,7 @@
</div> </div>
</div> </div>
</nav> </nav>
{% endcache %}
<script> <script>
let deferredPrompt; let deferredPrompt;
const addBtn = document.querySelector('.add-button'); const addBtn = document.querySelector('.add-button');

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<article class="panel is-info"> <article class="panel">
<p class="panel-heading"> <p class="panel-heading">
User information User information
</p> </p>
@@ -8,21 +8,7 @@
<span class="panel-icon"> <span class="panel-icon">
<i class="fas fa-id-card" aria-hidden="true"></i> <i class="fas fa-id-card" aria-hidden="true"></i>
</span> </span>
<span class="tag is-info">{{ user.first_name }} {{ user.last_name }}</span> <span class="tag">{{ user.first_name }} {{ user.last_name }}</span>
</a>
<a class="panel-block">
<span class="panel-icon">
<i class="fas fa-binary" aria-hidden="true"></i>
</span>
{% for plan in user.plans.all %}
<span class="tag 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>
</a> </a>
<a class="panel-block" href="{% url 'portal' %}"> <a class="panel-block" href="{% url 'portal' %}">
<span class="panel-icon"> <span class="panel-icon">

View File

@@ -1,10 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load joinsep %} {% load joinsep %}
{% block outer_content %} {% block content %}
<div class="grid-stack" id="grid-stack-main"> <div class="grid-stack" id="grid-stack-main">
<div class="grid-stack-item" gs-w="5" gs-h="14" 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"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
@@ -16,8 +16,8 @@
</article> </article>
</nav> </nav>
</div> </div>
</div> </div> -->
<div class="grid-stack-item" gs-w="4" gs-h="25" gs-y="0" gs-x="6"> <!-- <div class="grid-stack-item" gs-w="4" gs-h="25" gs-y="0" gs-x="6">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">
@@ -29,23 +29,7 @@
</article> </article>
</nav> </nav>
</div> </div>
</div>
<!-- <div class="grid-stack-item" gs-w="5" gs-h="14" gs-y="14" 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>
Offset
</p>
<article class="panel-block is-active">
{# include 'window-content/offset.html' #}
</article>
</nav>
</div>
</div> --> </div> -->
</div>
<script> <script>
var grid = GridStack.init({ var grid = GridStack.init({
@@ -62,10 +46,15 @@
// a widget is ready to be loaded // a widget is ready to be loaded
document.addEventListener('load-widget', function(event) { 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 // 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); let widgetelement = container.firstElementChild.cloneNode(true);
console.log(widgetelement);
var scripts = htmx.findAll(widgetelement, "script");
var new_id = widgetelement.id; var new_id = widgetelement.id;
// check if there's an existing element like the one we want to swap // check if there's an existing element like the one we want to swap
@@ -83,6 +72,7 @@
} }
// clear the queue element // clear the queue element
container.outerHTML = ""; container.outerHTML = "";
// container.firstElementChild.outerHTML = "";
grid.addWidget(widgetelement); grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid // re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
@@ -107,10 +97,37 @@
for (var i = 0; i < scripts.length; i++) { for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML); 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
</script> 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 %} {% endblock %}

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table {% 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" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
@@ -13,8 +16,10 @@
<th>name</th> <th>name</th>
<th>exchange</th> <th>exchange</th>
<th>currency</th> <th>currency</th>
<th>initial</th>
<th>API key</th> <th>API key</th>
<th>sandbox</th> <th>sandbox</th>
<th>enabled</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in object_list %} {% for item in object_list %}
@@ -24,6 +29,7 @@
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.exchange }}</td> <td>{{ item.exchange }}</td>
<td>{{ item.currency }}</td> <td>{{ item.currency }}</td>
<td>{{ item.initial_balance }}</td>
<td>{{ item.api_key }}</td> <td>{{ item.api_key }}</td>
<td> <td>
{% if item.sandbox %} {% if item.sandbox %}
@@ -36,6 +42,17 @@
</span> </span>
{% endif %} {% endif %}
</td> </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> <td>
<div class="buttons"> <div class="buttons">
<button <button
@@ -44,7 +61,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
@@ -58,16 +75,16 @@
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?" hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="{% url 'account_info' type=type pk=item.id %}"><button <a href="{% url 'account_info' type=type pk=item.id %}"><button
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -82,7 +99,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -95,4 +112,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -0,0 +1,61 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.AssetManagementPolicy' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_active_management request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>description</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description|truncatechars:80 }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'ams_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'ams_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endcache %}

View File

@@ -0,0 +1,59 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.AssetGroup' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_assetgroups_field request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>symbol</th>
<th>status</th>
<th>actions</th>
</thead>
{% for key, item in object_list.items %}
<tr class="
{% if item is True %}has-background-success-light
{% elif item is False %}has-background-danger-light
{% endif %}">
<td>{{ key }}</td>
<td>{{ item }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'assetfilter_flip' group_id=group_id symbol=key %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon" data-tooltip="Flip direction">
<i class="fa-solid fa-arrows-repeat"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'assetfilter_delete' group_id=group_id symbol=key %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endcache %}

View File

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

View File

@@ -0,0 +1,77 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.AssetRule' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_assetrules request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>asset</th>
<th>group</th>
<th>aggregation</th>
<th>value</th>
<th>original status</th>
<th>status</th>
<th>trigger above</th>
<th>trigger below</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr class="
{% if item.status == 2 %}has-background-success-light
{% elif item.status == 3 %}has-background-danger-light
{% elif item.status == 0 %}has-background-grey-light
{% endif %}">
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.asset }}</td>
<td>{{ item.group }}</td>
<td>{{ item.get_aggregation_display }}</td>
<td>{{ item.value }}</td>
<td>{{ item.get_original_status_display }}</td>
<td>{{ item.get_status_display }}</td>
<td>{{ item.trigger_above }}</td>
<td>{{ item.trigger_below }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'assetrule_update' type=type group=item.group.id pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'assetrule_delete' type=type group=item.group.id pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.asset }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endcache %}

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table class="table is-fullwidth is-hoverable" id="callbacks-table"> {% 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> <thead>
<th>id</th> <th>id</th>
<th>hook id</th> <th>hook id</th>
@@ -47,4 +50,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -1 +0,0 @@
<button class="modal-close is-large" aria-label="close"></button>

View File

@@ -1,3 +0,0 @@
<i
class="fa-solid fa-xmark has-text-grey-light float-right"
onclick='grid.removeWidget("widget-{{ unique }}");'></i>

View File

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

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% include 'partials/notify.html' %} {% include 'mixins/partials/notify.html' %}
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table {% 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" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
@@ -20,7 +23,15 @@
<td>{{ item.id }}</td> <td>{{ item.id }}</td>
<td>{{ item.user }}</td> <td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td><code>{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/</code></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>{{ item.received }}</td>
<td> <td>
<div class="buttons"> <div class="buttons">
@@ -30,7 +41,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
@@ -44,16 +55,16 @@
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?" hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="{% url 'callbacks' type='page' object_type='hook' object_id=item.id %}"><button <a href="{% url 'callbacks' type='page' object_type='hook' object_id=item.id %}"><button
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -68,7 +79,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -81,4 +92,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -1,5 +0,0 @@
{% if message is not None %}
<div class="notification is-{{ class }}" hx-ext="remove-me" remove-me="3s">
{{ message }}
</div>
{% endif %}

View File

@@ -0,0 +1,69 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.OrderSettings' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_ordersettings request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>description</th>
<th>TP</th>
<th>SL</th>
<th>TSL</th>
<th>size</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description|truncatechars:80 }}</td>
<td>{{ item.take_profit_percent}}</td>
<td>{{ item.stop_loss_percent }}</td>
<td>{{ item.trailing_stop_loss_percent }}</td>
<td>{{ item.trade_size_percent }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'ordersettings_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'ordersettings_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endcache %}

View File

@@ -1,12 +1,9 @@
{% include 'partials/notify.html' %} {% extends 'mixins/partials/generic-detail.html' %}
<h1 class="title">Live information</h1> {% load cache %}
<table class="table is-fullwidth is-hoverable">
<thead> {% block tbody %}
<th>attribute</th> {% cache 600 object_position_detail request.user.id object type %}
<th>value</th> {% for key, item in object.items %}
</thead>
<tbody>
{% for key, item in items.items %}
<tr> <tr>
{% if key == 'trade_ids' %} {% if key == 'trade_ids' %}
<th>{{ key }}</th> <th>{{ key }}</th>
@@ -15,7 +12,7 @@
{% for trade_id in item %} {% for trade_id in item %}
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type trade_id=trade_id %}" hx-get="{% url 'trade_action' type=type account_id=object.account_id trade_id=trade_id %}"
hx-trigger="click" hx-trigger="click"
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
@@ -35,5 +32,5 @@
{% endif %} {% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> {% endcache %}
</table> {% endblock %}

View File

@@ -1,5 +1,13 @@
{% include 'partials/notify.html' %} {% load cache %}
<table class="table is-fullwidth is-hoverable" id="positions-table"> {% 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> <thead>
<th>account</th> <th>account</th>
<th>asset</th> <th>asset</th>
@@ -11,7 +19,7 @@
<th>trades</th> <th>trades</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in items %} {% for item in object_list %}
<tr class=" <tr class="
{% if item.unrealized_pl > 0 %}has-background-success-light {% if item.unrealized_pl > 0 %}has-background-success-light
{% elif item.unrealized_pl < 0 %}has-background-danger-light {% elif item.unrealized_pl < 0 %}has-background-danger-light
@@ -36,35 +44,36 @@
<td>{{ item.trade_ids|length }}</td> <td>{{ item.trade_ids|length }}</td>
<td> <td>
<div class="buttons"> <div class="buttons">
<button <!-- <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#" hx-get="#"
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
</span> </span>
</span> </span>
</button> </button> -->
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' 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-trigger="click"
hx-target="#positions-table" hx-target="#notification"
hx-swap="outerHTML"
hx-confirm="Are you sure you wish to close {{ item.symbol }}?" hx-confirm="Are you sure you wish to close {{ item.symbol }}?"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}"> <a href="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}">
<button <button
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -79,7 +88,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -92,4 +101,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -1,8 +1,10 @@
{% load static %} {% 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"> <div class="box">
<article class="media"> <article class="media">
<div class="media-left"> <div class="media-left">
@@ -43,6 +45,6 @@
</div> </div>
</article> </article>
</div> </div>
{% endfor %} {% endfor %}
{% endcache %}

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

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

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table {% 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" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
@@ -18,7 +21,10 @@
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in object_list %} {% for item in object_list %}
<tr> <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.id }}</td>
<td>{{ item.user }}</td> <td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
@@ -42,7 +48,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
@@ -56,16 +62,16 @@
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?" hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="{% url 'callbacks' type='page' object_type='signal' object_id=item.id %}"><button <a href="{% url 'callbacks' type='page' object_type='signal' object_id=item.id %}"><button
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -80,7 +86,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -93,4 +99,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table {% 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" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
@@ -9,20 +12,44 @@
hx-get="{{ list_url }}"> hx-get="{{ list_url }}">
<thead> <thead>
<th>id</th> <th>id</th>
<th>user</th>
<th>name</th> <th>name</th>
<th>description</th> <th>description</th>
<th>account</th> <th>account</th>
<th>signal trading</th>
<th>active management</th>
<th>enabled</th> <th>enabled</th>
<th>TP</th>
<th>SL</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in object_list %} {% for item in object_list %}
<tr> <tr>
<td>{{ item.id }}</td> <td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.description }}</td> <td>{{ item.description|truncatechars:80 }}</td>
<td>{{ item.account }}</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> <td>
{% if item.enabled %} {% if item.enabled %}
<span class="icon"> <span class="icon">
@@ -34,8 +61,6 @@
</span> </span>
{% endif %} {% endif %}
</td> </td>
<td>{{ item.take_profit_percent }}</td>
<td>{{ item.stop_loss_percent }}</td>
<td> <td>
<div class="buttons"> <div class="buttons">
<button <button
@@ -44,7 +69,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
@@ -58,19 +83,19 @@
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?" hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="#"><button <a href="{% url 'trenddirections' type=type strategy_id=item.id %}"><button
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon" data-tooltip="View trends">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-arrows-up-down"></i>
</span> </span>
</span> </span>
</button> </button>
@@ -78,14 +103,14 @@
{% else %} {% else %}
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#" hx-get="{% url 'trenddirections' type=type strategy_id=item.id %}"
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon" data-tooltip="View trends">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-arrows-up-down"></i>
</span> </span>
</span> </span>
</button> </button>
@@ -95,4 +120,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table {% 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" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
@@ -9,6 +12,7 @@
hx-get="{{ list_url }}"> hx-get="{{ list_url }}">
<thead> <thead>
<th>id</th> <th>id</th>
<th>user</th>
<th>status</th> <th>status</th>
<th>account id</th> <th>account id</th>
<th>symbol</th> <th>symbol</th>
@@ -22,6 +26,7 @@
{% for item in object_list %} {% for item in object_list %}
<tr> <tr>
<td>{{ item.id }}</td> <td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.status }}</td> <td>{{ item.status }}</td>
<td>{{ item.account.id }}</td> <td>{{ item.account.id }}</td>
<td>{{ item.symbol }}</td> <td>{{ item.symbol }}</td>
@@ -38,7 +43,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
@@ -51,17 +56,17 @@
hx-trigger="click" hx-trigger="click"
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
{% if type == 'page' %} {% if type == 'page' %}
<a href="{% url 'trade_action' type=type trade_id=item.id %}"> <a href="{% url 'trade_action' type=type trade_id=item.id %}">
<button <button
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -72,11 +77,11 @@
{% else %} {% else %}
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type trade_id=item.id %}" hx-get="{% url 'trade_action' type=type account_id=item.account.id trade_id=item.id %}"
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-success"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-eye"></i> <i class="fa-solid fa-eye"></i>
@@ -89,4 +94,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

View File

@@ -1,6 +1,9 @@
{% include 'partials/notify.html' %} {% load cache %}
{% load cachalot cache %}
<table {% 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" class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table" hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table" id="{{ context_object_name }}-table"
@@ -21,7 +24,7 @@
<td>{{ item.id }}</td> <td>{{ item.id }}</td>
<td>{{ item.user }}</td> <td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td>{{ item.description }}</td> <td>{{ item.description|truncatechars:80 }}</td>
<td>{{ item.get_start_day_display }} at {{ item.start_time }}</td> <td>{{ item.get_start_day_display }} at {{ item.start_time }}</td>
<td>{{ item.get_end_day_display }} at {{ item.end_time }}</td> <td>{{ item.get_end_day_display }} at {{ item.end_time }}</td>
<td> <td>
@@ -32,7 +35,7 @@
hx-trigger="click" hx-trigger="click"
hx-target="#{{ type }}s-here" hx-target="#{{ type }}s-here"
hx-swap="innerHTML" hx-swap="innerHTML"
class="button is-info"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-pencil"></i> <i class="fa-solid fa-pencil"></i>
@@ -46,10 +49,10 @@
hx-target="#modals-here" hx-target="#modals-here"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?" hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger"> class="button">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-xmark"></i>
</span> </span>
</span> </span>
</button> </button>
@@ -58,4 +61,5 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcache %}

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

View File

@@ -12,7 +12,7 @@
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div class="field"> <div class="field">
<button class="button is-success"> <button class="button">
Login Login
</button> </button>
</div> </div>

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