Compare commits

...

263 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
682d141b8a Begin implementing exit executor for strategies 2022-12-01 19:33:06 +00:00
bdae8ab093 Make callbacks handle signals 2022-12-01 19:32:50 +00:00
4527a9d04b Reformat migrations 2022-12-01 19:32:29 +00:00
21b9585192 Validate strategy entries and exits 2022-12-01 19:32:08 +00:00
851d021af2 Make signals configurable 2022-11-29 07:20:21 +00:00
f7242f4dd8 Fix off by one issue in OANDA 2022-11-29 07:20:39 +00:00
f240c4b381 Improve navigating trades and positions by cross-linking 2022-11-29 07:20:39 +00:00
4e1b574921 Fix position list validation 2022-11-29 07:20:39 +00:00
6321fb9089 Increase max requests 2022-10-18 07:22:22 +01:00
87c794e9ac Bump UWSGI processes 2022-10-18 07:22:22 +01:00
8ce0066c38 Switch to UWSGI and improve Docker definitions 2022-10-18 07:22:22 +01:00
d3694d1821 Describe all model fields 2022-11-28 20:42:07 +00:00
06865d0aa9 Make the debug toolbar show 2022-11-28 20:14:30 +00:00
3a6c3cee1f Fix login redirect 2022-11-28 20:05:48 +00:00
0fc7c5c712 Implement more advanced 2FA library 2022-11-28 19:45:22 +00:00
7a64759ceb Implement permission checking in views and forms 2022-11-28 18:09:41 +00:00
bb7d6d1b41 Rename test 2022-11-28 17:09:53 +00:00
974deeafaa Add missing space 2022-11-25 22:15:27 +00:00
4973582bdf Implement trading time limits 2022-11-25 19:28:21 +00:00
69a2b269ad Link trading times to strategies 2022-11-25 18:05:02 +00:00
baa8e4fead Implement configuration of trading times 2022-11-25 18:01:34 +00:00
bcb3272064 Re-render invalid forms and fix space handling 2022-11-25 18:01:13 +00:00
b525611aaa Clarify value and quantity in position table headers 2022-11-22 07:20:37 +00:00
cb88cf33c2 Prevent rounding errors in value calculation 2022-11-22 07:20:37 +00:00
3f8fb66656 Fix price bound logic 2022-11-22 08:10:38 +00:00
46bba54cb7 Allow absent price 2022-11-21 07:20:12 +00:00
f6b5652268 Update Drakdoo API format 2022-11-21 07:20:12 +00:00
ca434b8cf0 Remove offset panel 2022-11-15 07:20:17 +00:00
d7e81dedb2 Implement trailing stop loss 2022-11-15 07:20:17 +00:00
5c68191e5b Calculate price slippage more reliably and allow specifying order type and time in force 2022-11-15 07:20:17 +00:00
c8f776e2a8 Set the type back to market 2022-11-15 07:20:17 +00:00
2aac7d1bb5 Add controls boilerplate to main page 2022-11-14 18:29:07 +00:00
d2e0137c8d Fix widgets in pages 2022-11-14 17:14:47 +00:00
e3d57c9aa8 Fix HTMX widgets and tweak display of some titles 2022-11-13 13:22:31 +00:00
781de3c772 Convert everything to Decimal 2022-11-11 07:20:00 +00:00
9fc5d2f4d7 Initialise inverted variable in currency conversion 2022-11-11 07:20:00 +00:00
67404fc161 Don't convert between the same currency 2022-11-11 07:20:00 +00:00
3f855dfb59 Remove some debug statements 2022-11-11 07:20:00 +00:00
afb0504dca Documented the market system properly, squashed some bugs 2022-11-11 07:20:00 +00:00
c3d908341a Refactor and ignore n/a exchange callbacks 2022-11-10 07:20:20 +00:00
8b52063473 Refactor execute_strategy into functions and send a price bound 2022-11-10 19:52:52 +00:00
af9f874209 Improve posting trades to OANDA and make everything more robust 2022-11-10 19:27:46 +00:00
bf863f43b2 Check exchange name and remove redundant code, use IOC orders for OANDA 2022-11-10 07:20:28 +00:00
40f6330a13 Remove asset filter and begin implementing posting trades 2022-11-10 07:20:14 +00:00
47384aed5f Update webmanifest 2022-11-10 09:52:30 +00:00
e8a2f9b0fa Update strategy defaults 2022-11-06 07:20:24 +00:00
6dd2997a74 Fix posting trades 2022-11-06 07:20:32 +00:00
60979652d9 Implement more validation and conversion 2022-11-04 07:20:14 +00:00
d34ac39d68 Pull object names from model definitions in CRUD views 2022-11-04 07:20:42 +00:00
0c52cbd0f8 Use new call helper for all OANDA commands 2022-11-04 07:20:12 +00:00
c773b93675 Remove unnecessary template pack 2022-11-04 07:20:54 +00:00
52216df5a4 Add Python history to gitignore 2022-11-04 07:20:25 +00:00
b36791d56b Simplify schema and error handling 2022-11-04 07:20:55 +00:00
04a87c1da6 Add some comments about the exchange variables 2022-11-04 09:15:09 +00:00
7770a3844c Make failing during validation or conversion configurable 2022-11-04 08:58:01 +00:00
74c46f2647 Watch for changes in HTML files too 2022-11-03 07:20:30 +00:00
f90f388e87 Add ripsecrets to pre-commit hook 2022-11-03 07:20:30 +00:00
65f650f1ac Check if hook exists 2022-11-03 15:59:27 +00:00
d319769fe0 Finish porting data structures for positions 2022-11-02 19:09:50 +00:00
48858bf20b Fix price parsing 2022-11-02 19:04:05 +00:00
1f75da40af Set account ID in exchange library instead of the view 2022-11-02 18:28:31 +00:00
5cb7d08614 Convert API responses with Glom 2022-11-02 18:25:34 +00:00
396d838416 Change positions fields 2022-11-02 18:24:56 +00:00
8ee56b0e37 Begin implementing pydantic validation for OANDA 2022-10-31 08:58:08 +00:00
c15ae379f5 Wrap API calls in helper and validate response 2022-10-30 19:11:07 +00:00
f22fcfdaaa Implement stub functions for OANDA 2022-10-30 11:21:48 +00:00
f6fa9bdbb6 Begin implementing OANDA 2022-10-30 10:57:53 +00:00
49a3737a72 Implement helper for deleting all database objects 2022-10-30 10:57:41 +00:00
166 changed files with 13065 additions and 3110 deletions

3
.gitignore vendored
View File

@@ -9,6 +9,7 @@ __pycache__/
# Distribution / packaging
.Python
.python_history
build/
develop-eggs/
dist/
@@ -157,3 +158,5 @@ cython_debug/
.vscode/
core/static/admin
core/static/debug_toolbar
Makefile
static/

View File

@@ -1,22 +1,22 @@
repos:
- repo: https://github.com/psf/black
rev: 22.6.0
rev: 23.1.0
hooks:
- id: black
exclude: ^core/migrations
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
rev: 5.11.5
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
rev: 6.0.0
hooks:
- id: flake8
args: [--max-line-length=88]
exclude: ^core/migrations
- repo: https://github.com/rtts/djhtml
rev: 'v1.5.2' # replace with the latest tag on GitHub
rev: v2.0.0
hooks:
- id: djhtml
args: [-t 2]
@@ -24,8 +24,7 @@ repos:
exclude : ^core/static/css # slow
- id: djjs
exclude: ^core/static/js # slow
# - repo: https://github.com/thibaudcolas/curlylint
# rev: v0.13.1
# hooks:
# - id: curlylint
# files: \.(html|sls)$
- repo: https://github.com/sirwart/ripsecrets.git
rev: v0.1.5
hooks:
- id: ripsecrets

View File

@@ -1,5 +1,6 @@
# syntax=docker/dockerfile:1
FROM python:3
ARG OPERATION
RUN useradd -d /code xf
RUN mkdir -p /code
@@ -15,9 +16,13 @@ USER xf
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /code
COPY requirements.prod.txt /code/
COPY requirements.txt /code/
RUN python -m venv /venv
RUN . /venv/bin/activate && pip install -r requirements.prod.txt
RUN . /venv/bin/activate && pip install -r requirements.txt
# CMD . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini
CMD . /venv/bin/activate && uvicorn --reload --workers 2 --uds /var/run/socks/app.sock app.asgi:application
CMD if [ "$OPERATION" = "uwsgi" ] ; then . /venv/bin/activate && uwsgi --ini /conf/uwsgi.ini ; else . /venv/bin/activate && exec python manage.py runserver 0.0.0.0:8000; fi
# CMD . /venv/bin/activate && uvicorn --reload --reload-include *.html --workers 2 --uds /var/run/socks/app.sock app.asgi:application
# CMD . /venv/bin/activate && gunicorn -b 0.0.0.0:8000 --reload app.asgi:application -k uvicorn.workers.UvicornWorker

View File

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

26
Makefile-dev Normal file
View File

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

26
Makefile-prod Normal file
View File

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

View File

@@ -13,7 +13,8 @@ ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",")
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",")
# Stripe
STRIPE_ENABLED = getenv("STRIPE_ENABLED", "false").lower() in trues
BILLING_ENABLED = getenv("BILLING_ENABLED", "false").lower() in trues
STRIPE_TEST = getenv("STRIPE_TEST", "true").lower() in trues
STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "")
STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "")
@@ -29,74 +30,36 @@ STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "")
REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues
ASSET_FILTER = [
"LINK/USDT",
"PAXG/USD",
"PAXG/USDT",
"SHIB/USD",
"TRX/USD",
"TRX/USDT",
"UNI/BTC",
"UNI/USD",
"UNI/USDT",
"USDT/USD",
"WBTC/USD",
"YFI/BTC",
"NEAR/USDT",
"SUSHI/USDT",
"DOGE/USDT",
"LINK/BTC",
"LINK/USD",
"GRT/USD",
"AVAX/BTC",
"AVAX/USD",
"AVAX/USDT",
"SOL/BTC",
"SOL/USD",
"SOL/USDT",
"BTC/USDT",
"SUSHI/BTC",
"SUSHI/USD",
"BCH/BTC",
"BCH/USD",
"YFI/USD",
"ETH/USD",
"ETH/USDT",
"YFI/USDT",
"AAVE/USD",
"AAVE/USDT",
"ALGO/USD",
"BAT/USD",
"DAI/USDT",
"ALGO/USDT",
"MATIC/BTC",
"MATIC/USD",
"DOGE/USD",
"MKR/USD",
"BTC/USD",
"DOGE/BTC",
"LTC/BTC",
"LTC/USD",
"LTC/USDT",
"ETH/BTC",
"BCH/USDT",
"DAI/USD",
"NEAR/USD",
]
# Hook URL, do not include leading or trailing slash
HOOK_PATH = "hook"
ASSET_PATH = "asset"
NOTIFY_TOPIC = getenv("NOTIFY_TOPIC", "great-fisk")
ELASTICSEARCH_USERNAME = getenv("ELASTICSEARCH_USERNAME", "elastic")
ELASTICSEARCH_PASSWORD = getenv("ELASTICSEARCH_PASSWORD", "changeme")
ELASTICSEARCH_HOST = getenv("ELASTICSEARCH_HOST", "localhost")
ELASTICSEARCH_TLS = getenv("ELASTICSEARCH_TLS", "false") in trues
LAGO_API_KEY = getenv("LAGO_API_KEY", "")
LAGO_ORG_ID = getenv("LAGO_ORG_ID", "")
LAGO_URL = getenv("LAGO_URL", "")
DEBUG = getenv("DEBUG", "false").lower() in trues
PROFILER = getenv("PROFILER", "false").lower() in trues
REDIS_HOST = getenv("REDIS_HOST", "redis_fisk_dev")
REDIS_PASSWORD = getenv("REDIS_PASSWORD", "changeme")
REDIS_DB = int(getenv("REDIS_DB", "10"))
REDIS_PORT = int(getenv("REDIS_PORT", "6379"))
if DEBUG:
import socket # only if you haven't already imported this
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
"127.0.0.1",
"10.0.2.2",
# "10.0.2.2",
]
SETTINGS_EXPORT = ["STRIPE_ENABLED", "URL", "HOOK_PATH"]
SETTINGS_EXPORT = ["BILLING_ENABLED", "URL", "HOOK_PATH", "ASSET_PATH"]

View File

@@ -44,21 +44,29 @@ INSTALLED_APPS = [
# "django_tables2_bulma_template",
"django_otp",
"django_otp.plugins.otp_totp",
# "django_otp.plugins.otp_email",
# 'django_otp.plugins.otp_hotp',
"django_otp.plugins.otp_static",
"two_factor",
# "two_factor.plugins.phonenumber",
# "two_factor.plugins.email",
# "two_factor.plugins.yubikey",
# "otp_yubikey",
"mixins",
"cachalot",
]
CRISPY_TEMPLATE_PACK = "bulma"
CRISPY_ALLOWED_TEMPLATE_PACKS = (
"uni_form",
"bulma",
)
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
# 'django.middleware.cache.UpdateCacheMiddleware',
"django.middleware.common.CommonMiddleware",
# 'django.middleware.cache.FetchFromCacheMiddleware',
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django_otp.middleware.OTPMiddleware",
@@ -138,9 +146,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "core.User"
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "home"
# LOGIN_URL = "/accounts/login/"
# 2FA
LOGIN_URL = "two_factor:login"
# LOGIN_REDIRECT_URL = 'two_factor:profile'
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
ALLOWED_PAYMENT_METHODS = ["card"]
@@ -153,7 +167,7 @@ REST_FRAMEWORK = {
INTERNAL_IPS = [
"127.0.0.1",
"10.1.10.11",
# "10.1.10.11",
]
DEBUG_TOOLBAR_PANELS = [
@@ -172,10 +186,29 @@ DEBUG_TOOLBAR_PANELS = [
"debug_toolbar.panels.logging.LoggingPanel",
"debug_toolbar.panels.redirects.RedirectsPanel",
"debug_toolbar.panels.profiling.ProfilingPanel",
"cachalot.panels.CachalotPanel",
]
from app.local_settings import * # noqa
# Performance optimisations
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
# "LOCATION": "unix:///var/run/socks/redis.sock",
"LOCATION": "unix:///var/run/redis.sock",
"OPTIONS": {
"db": REDIS_DB,
# "parser_class": "django_redis.cache.RedisCache",
# "PASSWORD": REDIS_PASSWORD,
"pool_class": "redis.BlockingConnectionPool",
},
}
}
# CACHE_MIDDLEWARE_ALIAS = 'default'
# CACHE_MIDDLEWARE_SECONDS = '600'
# CACHE_MIDDLEWARE_KEY_PREFIX = ''
if PROFILER: # noqa - trust me its there
import pyroscope
@@ -187,3 +220,12 @@ if PROFILER: # noqa - trust me its there
# "region": f'{os.getenv("REGION")}',
# }
)
def show_toolbar(request):
return DEBUG # noqa: from local imports
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": show_toolbar,
}

View File

@@ -16,36 +16,51 @@ Including another URLconf
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.views import LoginView
from django.contrib.auth.views import LogoutView
from django.urls import include, path
from django.views.generic import TemplateView
from django_otp.forms import OTPAuthenticationForm
from two_factor.urls import urlpatterns as tf_urls
from core.views import accounts, base, callbacks, hooks, positions, strategies, trades
from core.views.stripe_callbacks import Callback
from core.views import (
accounts,
assets,
base,
callbacks,
hooks,
limits,
notifications,
ordersettings,
policies,
positions,
profit,
risk,
signals,
strategies,
trades,
)
# from core.views.stripe_callbacks import Callback
urlpatterns = [
path("__debug__/", include("debug_toolbar.urls")),
path("", base.Home.as_view(), name="home"),
path("callback", Callback.as_view(), name="callback"),
# path("callback", Callback.as_view(), name="callback"),
path("billing/", base.Billing.as_view(), name="billing"),
path("order/<str:plan_name>/", base.Order.as_view(), name="order"),
path(
"cancel_subscription/<str:plan_name>/",
base.Cancel.as_view(),
name="cancel_subscription",
),
path(
"success/", TemplateView.as_view(template_name="success.html"), name="success"
),
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
# path("order/<str:plan_name>/", base.Order.as_view(), name="order"),
# path(
# "cancel_subscription/<str:plan_name>/",
# base.Cancel.as_view(),
# name="cancel_subscription",
# ),
# path(
# "success/", TemplateView.as_view(template_name="success.html"), name="success"
# ),
# path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
path("portal", base.Portal.as_view(), name="portal"),
path("sapp/", admin.site.urls),
path(
"accounts/login/", LoginView.as_view(authentication_form=OTPAuthenticationForm)
),
path("accounts/", include("django.contrib.auth.urls")),
# 2FA login urls
path("", include(tf_urls)),
path("accounts/signup/", base.Signup.as_view(), name="signup"),
path("accounts/logout/", LogoutView.as_view(), name="logout"),
path("hooks/<str:type>/", hooks.HookList.as_view(), name="hooks"),
path("hooks/<str:type>/create/", hooks.HookCreate.as_view(), name="hook_create"),
path(
@@ -62,7 +77,28 @@ urlpatterns = [
f"{settings.HOOK_PATH}/<str:hook_name>/", hooks.HookAPI.as_view(), name="hook"
),
path(
"callbacks/<str:type>/<str:pk>/",
f"{settings.ASSET_PATH}/<str:webhook_id>/",
assets.AssetGroupAPI.as_view(),
name="asset",
),
path("signals/<str:type>/", signals.SignalList.as_view(), name="signals"),
path(
"signals/<str:type>/create/",
signals.SignalCreate.as_view(),
name="signal_create",
),
path(
"signals/<str:type>/update/<str:pk>/",
signals.SignalUpdate.as_view(),
name="signal_update",
),
path(
"signals/<str:type>/delete/<str:pk>/",
signals.SignalDelete.as_view(),
name="signal_delete",
),
path(
"callbacks/<str:type>/<str:object_type>/<str:object_id>/",
callbacks.Callbacks.as_view(),
name="callbacks",
),
@@ -97,11 +133,22 @@ urlpatterns = [
trades.TradeUpdate.as_view(),
name="trade_update",
),
path(
"trades/<str:type>/view/<str:account_id>/<str:trade_id>/",
trades.TradeAction.as_view(),
name="trade_action",
),
path(
"trades/<str:type>/delete/<str:pk>/",
trades.TradeDelete.as_view(),
name="trade_delete",
),
path(
"trades/action/delete_all/",
trades.TradeDeleteAll.as_view(),
name="trade_delete_all",
),
path("profit/<str:type>/", profit.Profit.as_view(), name="profit"),
path("positions/<str:type>/", positions.Positions.as_view(), name="positions"),
path(
"positions/<str:type>/<str:account_id>/",
@@ -109,7 +156,12 @@ urlpatterns = [
name="positions",
),
path(
"positions/<str:type>/<str:account_id>/<str:asset_id>/",
"positions/close/<str:account_id>/<str:side>/<str:symbol>/",
positions.PositionAction.as_view(),
name="position_action",
),
path(
"positions/<str:type>/<str:account_id>/<str:symbol>/",
positions.PositionAction.as_view(),
name="position_action",
),
@@ -131,4 +183,149 @@ urlpatterns = [
strategies.StrategyDelete.as_view(),
name="strategy_delete",
),
path(
"trading_times/<str:type>/",
limits.TradingTimeList.as_view(),
name="tradingtimes",
),
path(
"trading_times/<str:type>/create/",
limits.TradingTimeCreate.as_view(),
name="tradingtime_create",
),
path(
"trading_times/<str:type>/update/<str:pk>/",
limits.TradingTimeUpdate.as_view(),
name="tradingtime_update",
),
path(
"trading_times/<str:type>/delete/<str:pk>/",
limits.TradingTimeDelete.as_view(),
name="tradingtime_delete",
),
path(
"trend_directions/<str:strategy_id>/flip/<str:symbol>/",
limits.TrendDirectionFlip.as_view(),
name="trenddirection_flip",
),
path(
"trend_directions/<str:strategy_id>/delete/<str:symbol>/",
limits.TrendDirectionDelete.as_view(),
name="trenddirection_delete",
),
path(
"trend_directions/<str:type>/view/<str:strategy_id>/",
limits.TrendDirectionList.as_view(),
name="trenddirections",
),
path(
"notifications/<str:type>/update/",
notifications.NotificationsUpdate.as_view(),
name="notifications_update",
),
# Risks
path(
"risk/<str:type>/",
risk.RiskList.as_view(),
name="risks",
),
path(
"risk/<str:type>/create/",
risk.RiskCreate.as_view(),
name="risk_create",
),
path(
"risk/<str:type>/update/<str:pk>/",
risk.RiskUpdate.as_view(),
name="risk_update",
),
path(
"risk/<str:type>/delete/<str:pk>/",
risk.RiskDelete.as_view(),
name="risk_delete",
),
# Asset Groups
path(
"group/<str:type>/",
assets.AssetGroupList.as_view(),
name="assetgroups",
),
path(
"group/<str:type>/create/",
assets.AssetGroupCreate.as_view(),
name="assetgroup_create",
),
path(
"group/<str:type>/update/<str:pk>/",
assets.AssetGroupUpdate.as_view(),
name="assetgroup_update",
),
path(
"group/<str:type>/delete/<str:pk>/",
assets.AssetGroupDelete.as_view(),
name="assetgroup_delete",
),
# Asset Rules
path(
"assetrule/<str:type>/<str:group>/",
assets.AssetRuleList.as_view(),
name="assetrules",
),
path(
"assetrule/<str:type>/create/<str:group>/",
assets.AssetRuleCreate.as_view(),
name="assetrule_create",
),
path(
"assetrule/<str:type>/update/<str:group>/<str:pk>/",
assets.AssetRuleUpdate.as_view(),
name="assetrule_update",
),
path(
"assetrule/<str:type>/delete/<str:group>/<str:pk>/",
assets.AssetRuleDelete.as_view(),
name="assetrule_delete",
),
# Order Settings
path(
"ordersettings/<str:type>/",
ordersettings.OrderSettingsList.as_view(),
name="ordersettings",
),
path(
"ordersettings/<str:type>/create/",
ordersettings.OrderSettingsCreate.as_view(),
name="ordersettings_create",
),
path(
"ordersettings/<str:type>/update/<str:pk>/",
ordersettings.OrderSettingsUpdate.as_view(),
name="ordersettings_update",
),
path(
"ordersettings/<str:type>/delete/<str:pk>/",
ordersettings.OrderSettingsDelete.as_view(),
name="ordersettings_delete",
),
# Active Management Policies
path(
"ams/<str:type>/",
policies.ActiveManagementPolicyList.as_view(),
name="ams",
),
path(
"ams/<str:type>/create/",
policies.ActiveManagementPolicyCreate.as_view(),
name="ams_create",
),
path(
"ams/<str:type>/update/<str:pk>/",
policies.ActiveManagementPolicyUpdate.as_view(),
name="ams_update",
),
path(
"ams/<str:type>/delete/<str:pk>/",
policies.ActiveManagementPolicyDelete.as_view(),
name="ams_delete",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -1,11 +1,22 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django_otp.admin import OTPAdminSite
from .forms import CustomUserCreationForm
from .models import Plan, Session, User
from .models import ( # AssetRestriction,; Plan,; Session,
Account,
AssetGroup,
Callback,
Hook,
NotificationSettings,
RiskModel,
Signal,
Strategy,
Trade,
TradingTime,
User,
)
admin.site.__class__ = OTPAdminSite
# admin.site.__class__ = OTPAdminSite
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
# for model_cls, model_admin in admin.site._registry.items():
@@ -14,27 +25,91 @@ admin.site.__class__ = OTPAdminSite
# Register your models here.
class CustomUserAdmin(UserAdmin):
list_filter = ["plans"]
# list_filter = ["plans"]
model = User
add_form = CustomUserCreationForm
fieldsets = (
*UserAdmin.fieldsets,
(
"Stripe information",
{"fields": ("stripe_id",)},
),
(
"Payment information",
{
"fields": (
"plans",
"last_payment",
)
},
"Billing information",
{"fields": ("billing_provider_id", "customer_id", "stripe_id")},
),
# (
# "Payment information",
# {
# "fields": (
# # "plans",
# "last_payment",
# )
# },
# ),
)
class AccountAdmin(admin.ModelAdmin):
list_display = ("user", "name", "exchange", "sandbox", "currency")
class HookAdmin(admin.ModelAdmin):
list_display = ("user", "name", "hook", "received")
class SignalAdmin(admin.ModelAdmin):
list_display = ("user", "name", "signal", "hook", "direction", "received", "type")
class TradeAdmin(admin.ModelAdmin):
list_display = ("user", "account", "symbol", "amount", "direction", "status")
class CallbackAdmin(admin.ModelAdmin):
list_display = ("hook", "signal", "title", "symbol", "price")
class TradingTimeAdmin(admin.ModelAdmin):
list_display = ("user", "name", "start_day", "start_time", "end_day", "end_time")
class StrategyAdmin(admin.ModelAdmin):
list_display = ("user", "name", "description", "account", "enabled")
class NotificationSettingsAdmin(admin.ModelAdmin):
list_display = ("user", "ntfy_topic", "ntfy_url")
class RiskModelAdmin(admin.ModelAdmin):
list_display = (
"user",
"name",
"description",
"max_loss_percent",
"max_risk_percent",
"max_open_trades",
"max_open_trades_per_symbol",
)
class AssetGroupAdmin(admin.ModelAdmin):
list_display = ("user", "name", "description", "webhook_id")
# class AssetRestrictionAdmin(admin.ModelAdmin):
# list_display = ("user", "name", "description", "webhook_id", "group")
admin.site.register(User, CustomUserAdmin)
admin.site.register(Plan)
admin.site.register(Session)
# admin.site.register(Plan)
# admin.site.register(Session)
admin.site.register(Account, AccountAdmin)
admin.site.register(Hook, HookAdmin)
admin.site.register(Signal, SignalAdmin)
admin.site.register(Trade, TradeAdmin)
admin.site.register(Callback, CallbackAdmin)
admin.site.register(TradingTime, TradingTimeAdmin)
admin.site.register(Strategy, StrategyAdmin)
admin.site.register(NotificationSettings, NotificationSettingsAdmin)
admin.site.register(RiskModel, RiskModelAdmin)
admin.site.register(AssetGroup, AssetGroupAdmin)
# admin.site.register(AssetRestriction, AssetRestrictionAdmin)

241
core/exchanges/__init__.py Normal file
View File

@@ -0,0 +1,241 @@
from abc import ABC, abstractmethod
from alpaca.common.exceptions import APIError
from glom import glom
from oandapyV20.exceptions import V20Error
from pydantic.error_wrappers import ValidationError
from core.lib import schemas
from core.util import logs
# Return error if the schema for the message type is not found
STRICT_VALIDATION = False
# Raise exception if the conversion schema is not found
STRICT_CONVERSION = False
# TODO: Set them to True when all message types are implemented
log = logs.get_logger("exchanges")
class NoSchema(Exception):
"""
Raised when:
- The schema for the message type is not found
- The conversion schema is not found
- There is no schema library for the exchange
"""
pass
class NoSuchMethod(Exception):
"""
Exchange library has no such method.
"""
pass
class GenericAPIError(Exception):
"""
Generic API error.
"""
pass
class ExchangeError(Exception):
"""
Exchange error.
"""
pass
def is_camel_case(s):
return s != s.lower() and s != s.upper() and "_" not in s
def snake_to_camel(word):
if is_camel_case(word):
return word
return "".join(x.capitalize() or "_" for x in word.split("_"))
class BaseExchange(ABC):
def __init__(self, account):
name = self.__class__.__name__
self.name = name.replace("Exchange", "").lower()
self.account = account
self.client = None
self.connect()
@abstractmethod
def connect(self):
pass
@property
def schema(self):
"""
Get the schema library for the exchange.
"""
# Does the schemas library have a library for this exchange name?
if hasattr(schemas, f"{self.name}_s"):
schema_instance = getattr(schemas, f"{self.name}_s")
else:
log.error(f"No schema library for {self.name}")
raise Exception(f"No schema library for exchange {self.name}")
return schema_instance
def get_schema(self, method, convert=False):
if isinstance(method, str):
to_camel = snake_to_camel(method)
else:
to_camel = snake_to_camel(method.__class__.__name__)
if convert:
to_camel = f"{to_camel}Schema"
# if hasattr(self.schema, method):
# schema = getattr(self.schema, method)
if hasattr(self.schema, to_camel):
schema = getattr(self.schema, to_camel)
else:
raise NoSchema(f"Could not get schema: {to_camel}")
return schema
def call_method(self, method, *args, **kwargs):
"""
Get a method from the exchange library.
"""
if hasattr(self.client, method):
response = getattr(self.client, method)(*args, **kwargs)
if isinstance(response, list):
response = {"itemlist": response}
return response
else:
raise NoSuchMethod
def convert_spec(self, response, method):
"""
Convert an API response to the requested spec.
:raises NoSchema: If the conversion schema is not found
"""
schema = self.get_schema(method, convert=True)
# Use glom to convert the response to the schema
converted = glom(response, schema)
return converted
def validate_response(self, response, method):
schema = self.get_schema(method)
# Return a dict of the validated response
try:
response_valid = schema(**response).dict()
except ValidationError as e:
log.error(f"Error validating {method} response: {response}")
log.error(f"Errors: {e}")
raise GenericAPIError("Error validating response")
return response_valid
def call(self, method, *args, **kwargs):
"""
Call the exchange API and validate the response
:raises NoSchema: If the method is not in the schema mapping
:raises ValidationError: If the response cannot be validated
"""
try:
response = self.call_method(method, *args, **kwargs)
except (APIError, V20Error) as e:
log.error(f"Error calling method {method}: {e}")
raise GenericAPIError(e)
try:
response_valid = self.validate_response(response, method)
except NoSchema as e:
log.error(f"{e} - {response}")
response_valid = response
# Convert the response to a format that we can use
try:
response_converted = self.convert_spec(response_valid, method)
except NoSchema as e:
log.error(f"{e} - {response}")
response_converted = response_valid
# return (True, response_converted)
return response_converted
# except Exception as e:
# log.error(f"Error calling method: {e}")
# raise GenericAPIError(e)
@abstractmethod
def get_account(self):
pass
def extract_instrument(self, instruments, instrument):
for x in instruments["itemlist"]:
if x["name"] == instrument:
return x
return None
@abstractmethod
def get_currencies(self, symbols):
pass
@abstractmethod
def get_instruments(self):
pass
@abstractmethod
def get_supported_assets(self):
pass
@abstractmethod
def get_balance(self):
pass
@abstractmethod
def get_market_value(self, symbol):
pass
@abstractmethod
def post_trade(self, trade):
pass
@abstractmethod
def close_trade(self, trade_id, units=None):
pass
@abstractmethod
def get_trade(self, trade_id):
pass
@abstractmethod
def update_trade(self, trade):
pass
@abstractmethod
def cancel_trade(self, trade_id):
pass
@abstractmethod
def get_position_info(self, symbol):
pass
@abstractmethod
def get_all_positions(self):
pass
@abstractmethod
def get_all_open_trades(self):
pass
@abstractmethod
def close_position(self, side, symbol):
pass
@abstractmethod
def close_all_positions(self):
pass

157
core/exchanges/alpaca.py Normal file
View File

@@ -0,0 +1,157 @@
from alpaca.common.exceptions import APIError
from alpaca.trading.client import TradingClient
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import (
GetAssetsRequest,
LimitOrderRequest,
MarketOrderRequest,
)
from core.exchanges import BaseExchange, ExchangeError, GenericAPIError, common
class AlpacaExchange(BaseExchange):
def connect(self):
self.client = TradingClient(
self.account.api_key,
self.account.api_secret,
paper=self.account.sandbox,
raw_data=True,
)
def get_account(self):
return self.call("get_account")
def get_instruments(self):
request = GetAssetsRequest(status="active", asset_class="crypto")
assets = self.call("get_all_assets", filter=request)
return assets
def get_currencies(self, currencies):
pass # TODO
def get_supported_assets(self):
assets = self.get_instruments()
assets = assets["itemlist"]
asset_list = [x["symbol"] for x in assets if "symbol" in x]
return asset_list
def get_balance(self):
account_info = self.call("get_account")
equity = account_info["equity"]
try:
balance = float(equity)
except ValueError:
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
def get_market_value(self, symbol): # TODO: pydantic
try:
position = self.client.get_position(symbol)
except APIError as e:
self.log.error(f"Could not get market value for {symbol}: {e}")
raise GenericAPIError(e)
return float(position["market_value"])
def post_trade(self, trade): # TODO: pydantic
# the trade is not placed yet
if trade.direction == "buy":
direction = OrderSide.BUY
elif trade.direction == "sell":
direction = OrderSide.SELL
else:
raise ExchangeError("Unknown direction")
cast = {
"symbol": trade.symbol,
"side": direction,
"time_in_force": TimeInForce.IOC, # TODO
}
if trade.amount is not None:
cast["qty"] = trade.amount
if trade.amount_usd is not None:
cast["notional"] = trade.amount_usd
if not trade.amount and not trade.amount_usd:
raise ExchangeError("No amount specified")
if trade.take_profit:
cast["take_profit"] = {"limit_price": trade.take_profit}
if trade.stop_loss:
stop_limit_price = trade.stop_loss - (trade.stop_loss * 0.005)
cast["stop_loss"] = {
"stop_price": trade.stop_loss,
"limit_price": stop_limit_price,
}
if trade.type == "market":
market_order_data = MarketOrderRequest(**cast)
try:
order = self.client.submit_order(order_data=market_order_data)
except APIError as e:
self.log.error(f"Error placing market order: {e}")
trade.status = "error"
trade.save()
raise GenericAPIError(e)
elif trade.type == "limit":
if not trade.price:
raise ExchangeError("No price specified for limit order")
cast["limit_price"] = trade.price
limit_order_data = LimitOrderRequest(**cast)
try:
order = self.client.submit_order(order_data=limit_order_data)
except APIError as e:
self.log.error(f"Error placing limit order: {e}")
trade.status = "error"
trade.save()
raise GenericAPIError(e)
else:
raise ExchangeError("Unknown trade type")
trade.response = order
trade.status = "posted"
trade.order_id = order["id"]
trade.client_order_id = order["client_order_id"]
trade.save()
return order
def close_trade(self, trade_id, units=None): # TODO
"""
Close a trade
"""
def get_trade(self, trade_id):
pass # TODO
def update_trade(self, trade):
pass
def cancel_trade(self, trade_id):
pass
def get_position_info(self, symbol):
position = self.call("get_open_position", symbol)
return position # TODO: check
def get_all_positions(self):
items = []
response = self.call("get_all_positions")
for item in response["itemlist"]:
item["account"] = self.account.name
item["account_id"] = self.account.id
item["unrealized_pl"] = float(item["unrealized_pl"])
items.append(item)
return items
def 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

207
core/exchanges/oanda.py Normal file
View File

@@ -0,0 +1,207 @@
from oandapyV20 import API
from oandapyV20.endpoints import accounts, orders, positions, pricing, trades
from core.exchanges import BaseExchange, common
from core.util import logs
log = logs.get_logger("oanda")
class OANDAExchange(BaseExchange):
def call_method(self, request):
self.client.request(request)
response = request.response
if isinstance(response, list):
response = {"itemlist": response}
return response
def connect(self):
self.client = API(access_token=self.account.api_secret)
self.account_id = self.account.api_key
def get_account(self):
r = accounts.AccountDetails(self.account_id)
return self.call(r)
def get_instruments(self):
r = accounts.AccountInstruments(accountID=self.account_id)
response = self.call(r)
return response
def get_currencies(self, currencies):
params = {"instruments": ",".join(currencies)}
r = pricing.PricingInfo(accountID=self.account_id, params=params)
response = self.call(r)
return response
def get_supported_assets(self, response=None):
if not response:
response = self.get_instruments()
return [x["name"] for x in response["itemlist"]]
def get_balance(self, return_usd=False):
r = accounts.AccountSummary(self.account_id)
response = self.call(r)
balance = float(response["balance"])
currency = response["currency"]
balance_usd = common.to_currency("sell", self.account, balance, currency, "USD")
common.get_balance_hook(
self.account.user.id,
self.account.user.username,
self.account.id,
self.account.name,
balance_usd,
)
if return_usd:
return balance_usd
return balance
def get_market_value(self, symbol):
raise NotImplementedError
def post_trade(self, trade):
if trade.direction == "sell":
amount = -trade.amount
else:
amount = trade.amount
data = {
"order": {
# "price": "1.5000", - added later
"timeInForce": trade.time_in_force.upper(),
"instrument": trade.symbol,
"units": str(amount),
"type": trade.type.upper(),
"positionFill": "DEFAULT",
}
}
if trade.stop_loss is not None:
data["order"]["stopLossOnFill"] = {
"timeInForce": "GTC",
"price": str(trade.stop_loss),
}
if trade.take_profit is not None:
data["order"]["takeProfitOnFill"] = {"price": str(trade.take_profit)}
if trade.price is not None:
if trade.type == "limit":
data["order"]["price"] = str(trade.price)
elif trade.type == "market":
data["order"]["priceBound"] = str(trade.price)
if trade.trailing_stop_loss is not None:
data["order"]["trailingStopLossOnFill"] = {
"distance": str(trade.trailing_stop_loss),
"timeInForce": "GTC",
}
r = orders.OrderCreate(self.account_id, data=data)
response = self.call(r)
trade.response = response
trade.status = "posted"
trade.order_id = str(int(response["id"]) + 1)
trade.client_order_id = response["requestID"]
trade.save()
return response
def get_trade_precision(self, symbol):
instruments = self.account.instruments
if not instruments:
log.error("No instruments found")
return None
# Extract the information for the symbol
instrument = self.extract_instrument(instruments, symbol)
if not instrument:
log.error(f"Symbol not found: {symbol}")
return None
# Get the required precision
try:
trade_precision = instrument["tradeUnitsPrecision"]
return trade_precision
except KeyError:
log.error(f"Precision not found for {symbol} from {instrument}")
return None
def close_trade(self, trade_id, units=None, symbol=None):
"""
Close a trade.
"""
if not units:
r = trades.TradeClose(accountID=self.account_id, tradeID=trade_id)
return self.call(r)
else:
trade_precision = self.get_trade_precision(symbol)
if trade_precision is None:
log.error(f"Unable to get trade precision for {symbol}")
return None
units = round(units, trade_precision)
data = {
"units": str(units),
}
r = trades.TradeClose(
accountID=self.account_id, tradeID=trade_id, data=data
)
return self.call(r)
def get_trade(self, trade_id):
# OANDA is off by one...
r = trades.TradeDetails(accountID=self.account_id, tradeID=trade_id)
return self.call(r)
def update_trade(self, trade_id, take_profit_price, stop_loss_price):
data = {}
if take_profit_price:
data["takeProfit"] = {"price": str(take_profit_price)}
if stop_loss_price:
data["stopLoss"] = {"price": str(stop_loss_price)}
r = trades.TradeCRCDO(accountID=self.account_id, tradeID=trade_id, data=data)
return self.call(r)
def cancel_trade(self, trade_id):
raise NotImplementedError
def get_position_info(self, symbol):
r = positions.PositionDetails(self.account_id, symbol)
response = self.call(r)
response["account"] = self.account.name
response["account_id"] = self.account.id
return response
def get_all_positions(self):
items = []
r = positions.OpenPositions(accountID=self.account_id)
response = self.call(r)
for item in response["itemlist"]:
item["account"] = self.account.name
item["account_id"] = self.account.id
item["unrealized_pl"] = float(item["unrealized_pl"])
items.append(item)
return items
def get_all_open_trades(self):
r = trades.OpenTrades(accountID=self.account_id)
return self.call(r)["itemlist"]
def close_position(self, side, symbol):
data = {
f"{side}Units": "ALL",
}
r = positions.PositionClose(
accountID=self.account_id, instrument=symbol, data=data
)
response = self.call(r)
return response
def close_all_positions(self):
all_positions = self.get_all_positions()
responses = []
for position in all_positions:
side = position["side"]
symbol = position["symbol"]
data = {
f"{side}Units": "ALL",
}
r = positions.PositionClose(
accountID=self.account_id, instrument=symbol, data=data
)
response = self.call(r)
responses.append(response)
return responses

View File

@@ -1,10 +1,26 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import FieldDoesNotExist
from django.forms import ModelForm
from mixins.restrictions import RestrictedFormMixin
from .models import Account, Hook, Strategy, Trade, User
from .models import ( # AssetRestriction,
Account,
ActiveManagementPolicy,
AssetGroup,
AssetRule,
Hook,
NotificationSettings,
OrderSettings,
RiskModel,
Signal,
Strategy,
Trade,
TradingTime,
User,
)
# Create your forms here.
# flake8: noqa: E501
class NewUserForm(UserCreationForm):
@@ -35,17 +51,39 @@ class CustomUserCreationForm(UserCreationForm):
fields = "__all__"
class HookForm(ModelForm):
class HookForm(RestrictedFormMixin, ModelForm):
class Meta:
model = Hook
fields = (
"name",
"hook",
)
help_texts = {
"name": "Name of the hook. Informational only.",
"hook": "The URL slug to use for the hook. Make it unique.",
}
class SignalForm(RestrictedFormMixin, ModelForm):
class Meta:
model = Signal
fields = (
"name",
"signal",
"hook",
"type",
"direction",
)
help_texts = {
"name": "Name of the signal. Informational only.",
"signal": "The name of the signal in Drakdoo. Copy it from there.",
"hook": "The hook this signal belongs to.",
"type": "Whether the signal is used for entering or exiting trades, or determining the trend.",
"direction": "The direction of the signal. This is used to determine if the signal is a buy or sell.",
}
class AccountForm(ModelForm):
class AccountForm(RestrictedFormMixin, ModelForm):
class Meta:
model = Account
fields = (
@@ -53,40 +91,352 @@ class AccountForm(ModelForm):
"exchange",
"api_key",
"api_secret",
"initial_balance",
"sandbox",
"enabled",
)
help_texts = {
"name": "Name of the account. Informational only.",
"exchange": "The exchange to use for this account.",
"api_key": "The API key or username for the account.",
"api_secret": "The API secret or password/token for the account.",
"sandbox": "Whether to use the sandbox/demo or not.",
"enabled": "Whether the account is enabled.",
"initial_balance": "The initial balance of the account.",
}
class StrategyForm(ModelForm):
class StrategyForm(RestrictedFormMixin, ModelForm):
fieldargs = {
"entry_signals": {"type": "entry"},
"exit_signals": {"type": "exit"},
"trend_signals": {"type": "trend"},
}
# Filter for enabled accounts
def __init__(self, *args, **kwargs):
super(StrategyForm, self).__init__(*args, **kwargs)
self.fields["account"].queryset = Account.objects.filter(enabled=True)
class Meta:
model = Strategy
fields = (
"name",
"description",
"account",
"hooks",
"asset_group",
"risk_model",
"trading_times",
"order_settings",
"entry_signals",
"exit_signals",
"trend_signals",
"signal_trading_enabled",
"active_management_enabled",
"active_management_policy",
"enabled",
"take_profit_percent",
"stop_loss_percent",
"price_slippage_percent",
"trade_size_percent",
)
hooks = forms.ModelMultipleChoiceField(
queryset=Hook.objects.all(), widget=forms.CheckboxSelectMultiple
help_texts = {
"name": "Name of the strategy. Informational only.",
"description": "Description of the strategy. Informational only.",
"account": "The account to use for this strategy.",
"asset_group": "Asset groups determine which pairs can be traded.",
"risk_model": "The risk model to use for this strategy. Highly recommended.",
"trading_times": "When the strategy will place new trades.",
"order_settings": "Order settings to use for this strategy.",
"entry_signals": "Callbacks received to these signals will trigger a trade.",
"exit_signals": "Callbacks received to these signals will close all trades for the symbol on the account.",
"trend_signals": "Callbacks received to these signals will limit the trading direction of the given symbol to the callback direction until further notice.",
"signal_trading_enabled": "Whether the strategy will place trades based on signals.",
"active_management_enabled": "Whether the strategy will amend/remove trades on the account that violate the rules.",
"active_management_policy": "The policy to use for active management.",
"enabled": "Whether the strategy is enabled.",
}
entry_signals = forms.ModelMultipleChoiceField(
queryset=Signal.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["entry_signals"],
required=False,
)
exit_signals = forms.ModelMultipleChoiceField(
queryset=Signal.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["exit_signals"],
required=False,
)
trend_signals = forms.ModelMultipleChoiceField(
queryset=Signal.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["trend_signals"],
required=False,
)
trading_times = forms.ModelMultipleChoiceField(
queryset=TradingTime.objects.all(), widget=forms.CheckboxSelectMultiple
)
def clean(self):
cleaned_data = super(StrategyForm, self).clean()
entry_signals = cleaned_data.get("entry_signals")
exit_signals = cleaned_data.get("exit_signals")
for entry in entry_signals.all():
if entry in exit_signals.all():
self._errors["entry_signals"] = self.error_class(
[
"You cannot bet against yourself. Do not use the same signal for entry and exit."
]
)
for exit in exit_signals.all():
if exit in entry_signals.all():
self._errors["exit_signals"] = self.error_class(
[
"You cannot bet against yourself. Do not use the same signal for entry and exit."
]
)
# Get all the directions for entry and exit signals
entries_set = set([x.direction for x in entry_signals.all()])
exits_set = set([x.direction for x in exit_signals.all()])
class TradeForm(ModelForm):
# Make sure both fields are filled before we check this
if entries_set and exits_set:
# Combine them into one set
check_set = set()
check_set.update(entries_set, exits_set)
# If the length is 1, they are all the same direction
if len(check_set) == 1:
self._errors["entry_signals"] = self.error_class(
[
"You cannot have entry and exit signals that are the same direction. At least one must be opposing."
]
)
self._errors["exit_signals"] = self.error_class(
[
"You cannot have entry and exit signals that are the same direction. At least one must be opposing."
]
)
if cleaned_data.get("active_management_enabled"):
# Ensure that no other strategy with this account has active management enabled
if (
Strategy.objects.filter(
account=cleaned_data.get("account"),
active_management_enabled=True,
enabled=True,
)
.exclude(id=self.instance.id)
.exists()
):
self.add_error(
"active_management_enabled",
"You cannot have more than one strategy with active management enabled for the same account.",
)
return
if not cleaned_data.get("active_management_policy"):
self.add_error(
"active_management_policy",
"You must select an active management policy if active management is enabled.",
)
return
return cleaned_data
class TradeForm(RestrictedFormMixin, ModelForm):
class Meta:
model = Trade
fields = (
"account",
"symbol",
"type",
"time_in_force",
"amount",
"price",
"stop_loss",
"trailing_stop_loss",
"take_profit",
"direction",
)
help_texts = {
"account": "The account to use for this trade.",
"symbol": "The symbol to trade.",
"type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.",
"time_in_force": "The time in force controls how the order is executed.",
"amount": "The amount to trade in the quote currency (the second part of the symbol if applicable, otherwise your account base currency).",
"price": "The price to trade at. Sets this price as the trigger price for limit orders. Sets a price bound (if supported) for market orders.",
"stop_loss": "The stop loss price. This will be set at the specified price.",
"trailing_stop_loss": "The trailing stop loss price. This will be set at the specified price and will follow the price as it moves in your favor.",
"take_profit": "The take profit price. This will be set at the specified price.",
"direction": "The direction of the trade. This is used to determine if the trade is a buy or sell.",
}
class TradingTimeForm(RestrictedFormMixin, ModelForm):
class Meta:
model = TradingTime
fields = (
"name",
"description",
"start_day",
"start_time",
"end_day",
"end_time",
)
help_texts = {
"name": "Name of the trading time. Informational only.",
"description": "Description of the trading time. Informational only.",
"start_day": "The day of the week to start trading.",
"start_time": "The time of day to start trading.",
"end_day": "The day of the week to stop trading.",
"end_time": "The time of day to stop trading.",
}
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = NotificationSettings
fields = (
"ntfy_topic",
"ntfy_url",
)
help_texts = {
"ntfy_topic": "The topic to send notifications to.",
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
}
class RiskModelForm(RestrictedFormMixin, ModelForm):
class Meta:
model = RiskModel
fields = (
"name",
"description",
"max_loss_percent",
"max_risk_percent",
"max_open_trades",
"max_open_trades_per_symbol",
"price_slippage_percent",
"callback_price_deviation_percent",
)
help_texts = {
"name": "Name of the risk model. Informational only.",
"description": "Description of the risk model. Informational only.",
"max_loss_percent": "The maximum percent of the account balance that can be lost before we cease trading.",
"max_risk_percent": "The maximum percent of the account balance that can be risked on all open trades.",
"max_open_trades": "The maximum number of open trades.",
"max_open_trades_per_symbol": "The maximum number of open trades per symbol.",
"price_slippage_percent": "The price slippage is the maximum percent the price can move against you before the trade is cancelled. Limit orders will be set at this percentage against your favour. Market orders will have a price bound set if this is supported.",
"callback_price_deviation_percent": "The callback price deviation is the maximum percent the price can change between receiving the callback and acting on it. This protects against rogue or delayed callbacks. Keep it low.",
}
class AssetGroupForm(RestrictedFormMixin, ModelForm):
class Meta:
model = AssetGroup
fields = (
"name",
"description",
"when_no_data",
"when_no_match",
"when_no_aggregation",
"when_not_in_bounds",
"when_bullish",
"when_bearish",
)
help_texts = {
"name": "Name of the asset group. Informational only.",
"description": "Description of the asset group. Informational only.",
"when_no_data": "The action to take when no webhooks have been received for an asset.",
"when_no_match": "The action to take when there were no matches last callback for an asset.",
"when_no_aggregation": "The action to take when there is no defined aggregations for the asset.",
"when_not_in_bounds": "The action to take when the aggregation is not breaching either bound.",
"when_bullish": "The action to take when the asset is bullish.",
"when_bearish": "The action to take when the asset is bearish.",
}
class AssetRuleForm(RestrictedFormMixin, ModelForm):
def __init__(self, *args, **kwargs):
super(AssetRuleForm, self).__init__(*args, **kwargs)
self.fields["value"].disabled = True
self.fields["original_status"].disabled = True
self.fields["aggregation"].disabled = True
class Meta:
model = AssetRule
fields = (
"asset",
"aggregation",
"value",
"original_status",
"status",
"trigger_below",
"trigger_above",
)
help_texts = {
"asset": "The asset to apply the rule to.",
"aggregation": "Aggregation of the callback",
"value": "Value of the aggregation",
"original_status": "The original status of the asset.",
"status": "The status of the asset, following rules configured on the Asset Group.",
"trigger_below": "Trigger Bearish when value is below this.",
"trigger_above": "Trigger Bullish when value is above this.",
}
class OrderSettingsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = OrderSettings
fields = (
"name",
"description",
"order_type",
"time_in_force",
"take_profit_percent",
"stop_loss_percent",
"trailing_stop_loss_percent",
"trade_size_percent",
)
help_texts = {
"name": "Name of the order settings. Informational only.",
"description": "Description of the order settings. Informational only.",
"order_type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.",
"time_in_force": "The time in force controls how the order is executed.",
"take_profit_percent": "The take profit will be set at this percentage above/below the entry price.",
"stop_loss_percent": "The stop loss will be set at this percentage above/below the entry price.",
"trailing_stop_loss_percent": "The trailing stop loss will be set at this percentage above/below the entry price. A trailing stop loss will follow the price as it moves in your favor.",
"trade_size_percent": "Percentage of the account balance to use for each trade.",
}
class ActiveManagementPolicyForm(RestrictedFormMixin, ModelForm):
class Meta:
model = ActiveManagementPolicy
fields = (
"name",
"description",
"when_trading_time_violated",
"when_trends_violated",
"when_position_size_violated",
"when_protection_violated",
"when_asset_groups_violated",
"when_max_open_trades_violated",
"when_max_open_trades_per_symbol_violated",
"when_max_loss_violated",
"when_max_risk_violated",
"when_crossfilter_violated",
)
help_texts = {
"name": "Name of the active management policy. Informational only.",
"description": "Description of the active management policy. Informational only.",
"when_trading_time_violated": "The action to take when the trading time is violated.",
"when_trends_violated": "The action to take a trade against the trend is discovered.",
"when_position_size_violated": "The action to take when a trade exceeding the position size is discovered.",
"when_protection_violated": "The action to take when a trade violating/lacking defined TP/SL/TSL is discovered.",
"when_asset_groups_violated": "The action to take when a trade violating the asset group rules is discovered.",
"when_max_open_trades_violated": "The action to take when a trade puts the account above the maximum open trades.",
"when_max_open_trades_per_symbol_violated": "The action to take when a trade puts the account above the maximum open trades per symbol.",
"when_max_loss_violated": "The action to take when the account exceeds its maximum loss. NOTE: The close action will close all trades.",
"when_max_risk_violated": "The action to take when a trade exposes the account to more than the maximum risk.",
"when_crossfilter_violated": "The action to take when a trade is deemed to conflict with another -- e.g. a buy and sell on the same asset.",
}

111
core/lib/billing.py Normal file
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,115 +0,0 @@
from alpaca.common.exceptions import APIError
from core.models import Strategy, Trade
from core.util import logs
log = logs.get_logger(__name__)
def get_balance(account):
account_info = account.client.get_account()
cash = account_info["equity"]
try:
return float(cash)
except ValueError:
return False
def get_market_value(account, symbol):
try:
position = account.client.get_position(symbol)
return float(position["market_value"])
except APIError:
return False
def execute_strategy(callback, strategy):
cash_balance = get_balance(strategy.account)
log.debug(f"Cash balance: {cash_balance}")
if not cash_balance:
return None
user = strategy.user
account = strategy.account
hook = callback.hook
base = callback.base
quote = callback.quote
direction = hook.direction
if quote not in ["usd", "usdt", "usdc", "busd"]:
log.error(f"Quote not compatible with Dollar: {quote}")
return False
quote = "usd" # TODO: MASSIVE HACK
symbol = f"{base.upper()}/{quote.upper()}"
if symbol not in account.supported_assets:
log.error(f"Symbol not supported by account: {symbol}")
return False
print(f"Identified pair from callback {symbol}")
# market_from_alpaca = get_market_value(account, symbol)
# change_percent = abs(((float(market_from_alpaca)-price)/price)*100)
# if change_percent > strategy.price_slippage_percent:
# log.error(f"Price slippage too high: {change_percent}")
# return False
# type = "limit"
type = "market"
trade_size_as_ratio = strategy.trade_size_percent / 100
log.debug(f"Trade size as ratio: {trade_size_as_ratio}")
amount_usd = trade_size_as_ratio * cash_balance
log.debug(f"Trade size: {amount_usd}")
price = callback.price
if not price:
return
log.debug(f"Extracted price of quote: {price}")
# We can do this because the quote IS in $ or equivalent
trade_size_in_quote = amount_usd / price
log.debug(f"Trade size in quote: {trade_size_in_quote}")
# calculate sl/tp
stop_loss_as_ratio = strategy.stop_loss_percent / 100
take_profit_as_ratio = strategy.take_profit_percent / 100
log.debug(f"Stop loss as ratio: {stop_loss_as_ratio}")
log.debug(f"Take profit as ratio: {take_profit_as_ratio}")
stop_loss_subtract = price * stop_loss_as_ratio
take_profit_add = price * take_profit_as_ratio
log.debug(f"Stop loss subtract: {stop_loss_subtract}")
log.debug(f"Take profit add: {take_profit_add}")
stop_loss = price - stop_loss_subtract
take_profit = price + take_profit_add
log.debug(f"Stop loss: {stop_loss}")
log.debug(f"Take profit: {take_profit}")
new_trade = Trade.objects.create(
user=user,
account=account,
hook=hook,
symbol=symbol,
type=type,
# amount_usd=amount_usd,
amount=trade_size_in_quote,
# price=price,
stop_loss=stop_loss,
take_profit=take_profit,
direction=direction,
)
new_trade.save()
posted, info = new_trade.post()
log.debug(f"Posted trade: {posted} - {info}")
def process_callback(callback):
log.info(f"Received callback for {callback.hook}")
strategies = Strategy.objects.filter(hooks=callback.hook, enabled=True)
log.debug(f"Matched strategies: {strategies}")
for strategy in strategies:
log.debug(f"Executing strategy {strategy}")
if callback.hook.user != strategy.user:
log.error("Ownership differs between callback and strategy.")
return
execute_strategy(callback, strategy)

38
core/lib/notify.py Normal file
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):
"""
Get all the plans from the database and create an object Stripe wants.
"""
line_items = []
for plan in await sync_to_async(list)(Plan.objects.all()):
if product_id_filter:
if plan.product_id != product_id_filter:
continue
line_items.append(
{
"price": plan.product_id,
"quantity": 1,
}
)
return line_items
# async def assemble_plan_map(product_id_filter=None):
# """
# Get all the plans from the database and create an object Stripe wants.
# """
# line_items = []
# for plan in await sync_to_async(list)(Plan.objects.all()):
# if product_id_filter:
# if plan.product_id != product_id_filter:
# continue
# line_items.append(
# {
# "price": plan.product_id,
# "quantity": 1,
# }
# )
# return line_items

View File

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

View File

@@ -0,0 +1,191 @@
from pydantic import BaseModel, Field
class Asset(BaseModel):
id: str
class_: str = Field(..., alias="class")
exchange: str
symbol: str
name: str
status: str
tradable: bool
marginable: bool
maintenance_margin_requirement: int
shortable: bool
easy_to_borrow: bool
fractionable: bool
min_order_size: str
min_trade_increment: str
price_increment: str
# get_all_assets
class GetAllAssets(BaseModel):
itemlist: list[Asset]
GetAllAssetsSchema = {
"itemlist": (
"itemlist",
[
{
"id": "id",
"class": "class_",
"exchange": "exchange",
"symbol": "symbol",
"name": "name",
"status": "status",
"tradable": "tradable",
"marginable": "marginable",
"maintenance_margin_requirement": "maintenanceMarginRequirement",
"shortable": "shortable",
"easy_to_borrow": "easyToBorrow",
"fractionable": "fractionable",
"min_order_size": "minOrderSize",
"min_trade_increment": "minTradeIncrement",
"price_increment": "priceIncrement",
}
],
)
}
# get_open_position
class GetOpenPosition(BaseModel):
asset_id: str
symbol: str
exchange: str
asset_class: str = Field(..., alias="asset_class")
asset_marginable: bool
qty: str
avg_entry_price: str
side: str
market_value: str
cost_basis: str
unrealized_pl: str
unrealized_plpc: str
unrealized_intraday_pl: str
unrealized_intraday_plpc: str
current_price: str
lastday_price: str
change_today: str
qty_available: str
class Position(BaseModel):
asset_id: str
symbol: str
exchange: str
asset_class: str
asset_marginable: bool
qty: str
avg_entry_price: str
side: str
market_value: str
cost_basis: str
unrealized_pl: str
unrealized_plpc: str
unrealized_intraday_pl: str
unrealized_intraday_plpc: str
current_price: str
lastday_price: str
change_today: str
qty_available: str
# get_all_positions
class GetAllPositions(BaseModel):
itemlist: list[Position]
GetAllPositionsSchema = {
"itemlist": (
"itemlist",
[
{
"symbol": "symbol",
"unrealized_pl": "unrealized_pl",
"price": "current_price",
"units": "qty",
"side": "side",
"value": "market_value",
}
],
)
}
# get_account
class GetAccount(BaseModel):
id: str
account_number: str
status: str
crypto_status: str
currency: str
buying_power: str
regt_buying_power: str
daytrading_buying_power: str
effective_buying_power: str
non_marginable_buying_power: str
bod_dtbp: str
cash: str
accrued_fees: str
pending_transfer_in: str
portfolio_value: str
pattern_day_trader: bool
trading_blocked: bool
transfers_blocked: bool
account_blocked: bool
created_at: str
trade_suspended_by_user: bool
multiplier: str
shorting_enabled: bool
equity: str
last_equity: str
long_market_value: str
short_market_value: str
position_market_value: str
initial_margin: str
maintenance_margin: str
last_maintenance_margin: str
sma: str
daytrade_count: int
balance_asof: str
GetAccountSchema = {
"id": "id",
"account_number": "account_number",
"status": "status",
"crypto_status": "crypto_status",
"currency": "currency",
"buying_power": "buying_power",
"regt_buying_power": "regt_buying_power",
"daytrading_buying_power": "daytrading_buying_power",
"effective_buying_power": "effective_buying_power",
"non_marginable_buying_power": "non_marginable_buying_power",
"bod_dtbp": "bod_dtbp",
"cash": "cash",
"accrued_fees": "accrued_fees",
"pending_transfer_in": "pending_transfer_in",
"portfolio_value": "portfolio_value",
"pattern_day_trader": "pattern_day_trader",
"trading_blocked": "trading_blocked",
"transfers_blocked": "transfers_blocked",
"account_blocked": "account_blocked",
"created_at": "created_at",
"trade_suspended_by_user": "trade_suspended_by_user",
"multiplier": "multiplier",
"shorting_enabled": "shorting_enabled",
"equity": "equity",
"last_equity": "last_equity",
"long_market_value": "long_market_value",
"short_market_value": "short_market_value",
"position_market_value": "position_market_value",
"initial_margin": "initial_margin",
"maintenance_margin": "maintenance_margin",
"last_maintenance_margin": "last_maintenance_margin",
"sma": "sma",
"daytrade_count": "daytrade_count",
"balance_asof": "balance_asof",
}

View File

@@ -0,0 +1,22 @@
from pydantic import BaseModel
class DrakdooMarket(BaseModel):
exchange: str
item: str
currency: str
contract: str
class DrakdooTimestamp(BaseModel):
sent: int
trade: int
class DrakdooCallback(BaseModel):
title: str
message: str
period: str
price: str | None
market: DrakdooMarket
timestamp: DrakdooTimestamp

View File

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

754
core/lib/schemas/oanda_s.py Normal file
View File

@@ -0,0 +1,754 @@
from decimal import Decimal as D
from typing import Optional
from pydantic import BaseModel
class PositionLong(BaseModel):
units: str
averagePrice: Optional[str] = None
pl: str
resettablePL: str
financing: str
dividendAdjustment: str
guaranteedExecutionFees: str
tradeIDs: Optional[list[str]] = []
unrealizedPL: str
class PositionShort(BaseModel):
units: str
averagePrice: Optional[str] = None
pl: str
resettablePL: str
financing: str
dividendAdjustment: str
guaranteedExecutionFees: str
tradeIDs: Optional[list[str]] = []
unrealizedPL: str
class Position(BaseModel):
instrument: str
long: PositionLong
short: PositionShort
pl: str
resettablePL: str
financing: str
commission: str
dividendAdjustment: str
guaranteedExecutionFees: str
unrealizedPL: str
marginUsed: str
class OpenPositions(BaseModel):
positions: list[Position]
lastTransactionID: str
def parse_time(x):
"""
Parse the time from the Oanda API.
"""
if "openTime" in x:
ts_split = x["openTime"].split(".")
else:
ts_split = x["trade"]["openTime"].split(".")
microseconds = ts_split[1].replace("Z", "")
microseconds_6 = microseconds[:6]
new_ts = ts_split[0] + "." + microseconds_6 + "Z"
return new_ts
def prevent_hedging(x):
"""
Our implementation breaks if a position has both.
We implemented it this way in order to more easily support other exchanges.
The actual direction is put into the root object with Grom.
"""
if float(x["long"]["units"]) > 0 and float(x["short"]["units"]) < 0:
raise ValueError("Hedging not allowed")
def parse_prices(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0:
return x["long"]["averagePrice"]
elif float(x["short"]["units"]) < 0:
return x["short"]["averagePrice"]
else:
return 0
def parse_units(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0:
return x["long"]["units"]
elif float(x["short"]["units"]) < 0:
return x["short"]["units"]
else:
return 0
def parse_value(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0:
return D(x["long"]["units"]) * D(x["long"]["averagePrice"])
elif float(x["short"]["units"]) < 0:
return D(x["short"]["units"]) * D(x["short"]["averagePrice"])
else:
return 0
def parse_current_units_side(x):
if float(x["currentUnits"]) > 0:
return "long"
elif float(x["currentUnits"]) < 0:
return "short"
def parse_side(x):
prevent_hedging(x)
if float(x["long"]["units"]) > 0:
return "long"
elif float(x["short"]["units"]) < 0:
return "short"
else:
return "unknown"
def parse_trade_ids(x, sum=0):
prevent_hedging(x)
if float(x["long"]["units"]) > 0:
return [str(int(y) + sum) for y in x["long"]["tradeIDs"]]
elif float(x["short"]["units"]) < 0:
return [str(int(y) + sum) for y in x["short"]["tradeIDs"]]
else:
return "unknown"
OpenPositionsSchema = {
"itemlist": (
"positions",
[
{
"symbol": "instrument",
"unrealized_pl": "unrealizedPL",
"trade_ids": parse_trade_ids, # actual value is lower by 1
"price": parse_prices,
"units": parse_units,
"side": parse_side,
"value": parse_value,
}
],
)
}
class AccountDetailsNested(BaseModel):
guaranteedStopLossOrderMode: str
hedgingEnabled: bool
id: str
createdTime: str
currency: str
createdByUserID: int
alias: str
marginRate: str
lastTransactionID: str
balance: str
openTradeCount: int
openPositionCount: int
pendingOrderCount: int
pl: str
resettablePL: str
resettablePLTime: str
financing: str
commission: str
dividendAdjustment: str
guaranteedExecutionFees: str
orders: list # Order
positions: list # Position
trades: list # Trade
unrealizedPL: str
NAV: str
marginUsed: str
marginAvailable: str
positionValue: str
marginCloseoutUnrealizedPL: str
marginCloseoutNAV: str
marginCloseoutMarginUsed: str
marginCloseoutPositionValue: str
marginCloseoutPercent: str
withdrawalLimit: str
marginCallMarginUsed: str
marginCallPercent: str
class AccountDetails(BaseModel):
account: AccountDetailsNested
lastTransactionID: str
AccountDetailsSchema = {
"guaranteedSLOM": "account.guaranteedStopLossOrderMode",
"hedgingEnabled": "account.hedgingEnabled",
"id": "account.id",
"created_at": "account.createdTime",
"currency": "account.currency",
"createdByUserID": "account.createdByUserID",
"alias": "account.alias",
"marginRate": "account.marginRate",
"lastTransactionID": "account.lastTransactionID",
"balance": "account.balance",
"openTradeCount": "account.openTradeCount",
"openPositionCount": "account.openPositionCount",
"pendingOrderCount": "account.pendingOrderCount",
"pl": "account.pl",
"resettablePL": "account.resettablePL",
"resettablePLTime": "account.resettablePLTime",
"financing": "account.financing",
"commission": "account.commission",
"dividendAdjustment": "account.dividendAdjustment",
"guaranteedExecutionFees": "account.guaranteedExecutionFees",
# "orders": "account.orders",
# "positions": "account.positions",
# "trades": "account.trades",
"unrealizedPL": "account.unrealizedPL",
"NAV": "account.NAV",
"marginUsed": "account.marginUsed",
"marginAvailable": "account.marginAvailable",
"positionValue": "account.positionValue",
"marginCloseoutUnrealizedPL": "account.marginCloseoutUnrealizedPL",
"marginCloseoutNAV": "account.marginCloseoutNAV",
"marginCloseoutMarginUsed": "account.marginCloseoutMarginUsed",
"marginCloseoutPositionValue": "account.marginCloseoutPositionValue",
"marginCloseoutPercent": "account.marginCloseoutPercent",
"withdrawalLimit": "account.withdrawalLimit",
"marginCallMarginUsed": "account.marginCallMarginUsed",
"marginCallPercent": "account.marginCallPercent",
}
class AccountSummaryNested(BaseModel):
marginCloseoutNAV: str
marginUsed: str
currency: str
resettablePL: str
NAV: str
marginCloseoutMarginUsed: str
marginCloseoutPositionValue: str
openTradeCount: int
id: str
hedgingEnabled: bool
marginCloseoutPercent: str
marginCallMarginUsed: str
openPositionCount: int
positionValue: str
pl: str
lastTransactionID: str
marginAvailable: str
marginRate: str
marginCallPercent: str
pendingOrderCount: int
withdrawalLimit: str
unrealizedPL: str
alias: str
createdByUserID: int
marginCloseoutUnrealizedPL: str
createdTime: str
balance: str
class AccountSummary(BaseModel):
account: AccountSummaryNested
lastTransactionID: str
AccountSummarySchema = {
"marginCloseoutNAV": "account.marginCloseoutNAV",
"marginUsed": "account.marginUsed",
"currency": "account.currency",
"resettablePL": "account.resettablePL",
"NAV": "account.NAV",
"marginCloseoutMarginUsed": "account.marginCloseoutMarginUsed",
"marginCloseoutPositionValue": "account.marginCloseoutPositionValue",
"openTradeCount": "account.openTradeCount",
"id": "account.id",
"hedgingEnabled": "account.hedgingEnabled",
"marginCloseoutPercent": "account.marginCloseoutPercent",
"marginCallMarginUsed": "account.marginCallMarginUsed",
"openPositionCount": "account.openPositionCount",
"positionValue": "account.positionValue",
"pl": "account.pl",
"lastTransactionID": "account.lastTransactionID",
"marginAvailable": "account.marginAvailable",
"marginRate": "account.marginRate",
"marginCallPercent": "account.marginCallPercent",
"pendingOrderCount": "account.pendingOrderCount",
"withdrawalLimit": "account.withdrawalLimit",
"unrealizedPL": "account.unrealizedPL",
"alias": "account.alias",
"createdByUserID": "account.createdByUserID",
"marginCloseoutUnrealizedPL": "account.marginCloseoutUnrealizedPL",
"createdTime": "account.createdTime",
"balance": "account.balance",
}
class PositionDetailsNested(BaseModel):
instrument: str
long: PositionLong
short: PositionShort
pl: str
resettablePL: str
financing: str
commission: str
dividendAdjustment: str
guaranteedExecutionFees: str
unrealizedPL: str
marginUsed: Optional[str] = None
class PositionDetails(BaseModel):
position: PositionDetailsNested
lastTransactionID: str
PositionDetailsSchema = {
"symbol": "position.instrument",
"long": "position.long",
"short": "position.short",
"pl": "position.pl",
"resettablePL": "position.resettablePL",
"financing": "position.financing",
"commission": "position.commission",
"dividendAdjustment": "position.dividendAdjustment",
"guaranteedExecutionFees": "position.guaranteedExecutionFees",
"unrealizedPL": "position.unrealizedPL",
"marginUsed": "position.marginUsed",
"price": lambda x: parse_prices(x["position"]),
"units": lambda x: parse_units(x["position"]),
"side": lambda x: parse_side(x["position"]),
"value": lambda x: parse_value(x["position"]),
"trade_ids": lambda x: parse_trade_ids(
x["position"], sum=0
), # this value is correct
}
class InstrumentTag(BaseModel):
type: str
name: str
class InstrumentFinancingDaysOfWeek(BaseModel):
dayOfWeek: str
daysCharged: int
class InstrumentFinancing(BaseModel):
longRate: str
shortRate: str
financingDaysOfWeek: list[InstrumentFinancingDaysOfWeek]
class InstrumentGuaranteedRestriction(BaseModel):
volume: str
priceRange: str
class Instrument(BaseModel):
name: str
type: str
displayName: str
pipLocation: int
displayPrecision: int
tradeUnitsPrecision: int
minimumTradeSize: str
maximumTrailingStopDistance: str
minimumTrailingStopDistance: str
maximumPositionSize: str
maximumOrderUnits: str
marginRate: str
guaranteedStopLossOrderMode: str
tags: list[InstrumentTag]
financing: InstrumentFinancing
guaranteedStopLossOrderLevelRestriction: Optional[
InstrumentGuaranteedRestriction
] = None
class AccountInstruments(BaseModel):
instruments: list[Instrument]
AccountInstrumentsSchema = {
"itemlist": (
"instruments",
[
{
"name": "name",
"type": "type",
"displayName": "displayName",
"pipLocation": "pipLocation",
"displayPrecision": "displayPrecision",
"tradeUnitsPrecision": "tradeUnitsPrecision",
"minimumTradeSize": "minimumTradeSize",
"maximumTrailingStopDistance": "maximumTrailingStopDistance",
"minimumTrailingStopDistance": "minimumTrailingStopDistance",
"maximumPositionSize": "maximumPositionSize",
"maximumOrderUnits": "maximumOrderUnits",
"marginRate": "marginRate",
"guaranteedSLOM": "guaranteedStopLossOrderMode",
"tags": "tags",
"financing": "financing",
"guaranteedSLOLR": "guaranteedStopLossOrderLevelRestriction",
}
],
)
}
class PriceBid(BaseModel):
price: str
liquidity: int
class PriceAsk(BaseModel):
price: str
liquidity: int
class PriceQuoteHomeConversionFactors(BaseModel):
positiveUnits: str
negativeUnits: str
class Price(BaseModel):
type: str
time: str
bids: list[PriceBid]
asks: list[PriceAsk]
closeoutBid: str
closeoutAsk: str
status: str
tradeable: bool
quoteHomeConversionFactors: PriceQuoteHomeConversionFactors
instrument: str
class PricingInfo(BaseModel):
time: str
prices: list[Price]
PricingInfoSchema = {
"time": "time",
"prices": (
"prices",
[
{
"type": "type",
"time": "time",
"bids": "bids",
"asks": "asks",
"closeoutBid": "closeoutBid",
"closeoutAsk": "closeoutAsk",
"status": "status",
"tradeable": "tradeable",
"quoteHomeConversionFactors": "quoteHomeConversionFactors",
"symbol": "instrument",
}
],
),
}
class Trade(BaseModel):
tradeID: str
clientTradeID: str
units: str
realizedPL: str
financing: str
baseFinancing: str
price: str
guaranteedExecutionFee: str
quoteGuaranteedExecutionFee: str
halfSpreadCost: str
# takeProfitOrder: TakeProfitOrder | None
takeProfitOrder: Optional[dict] = None
stopLossOrder: Optional[dict] = None
trailingStopLossOrder: Optional[dict] = None
class SideCarOrder(BaseModel):
id: str
createTime: str
state: str
price: Optional[str] = None
timeInForce: str
gtdTime: Optional[str] = None
clientExtensions: Optional[dict] = None
tradeID: str
clientTradeID: Optional[str] = None
type: str
time: Optional[str] = None
priceBound: Optional[str] = None
positionFill: Optional[str] = None
reason: Optional[str] = None
orderFillTransactionID: Optional[str] = None
tradeOpenedID: Optional[str] = None
tradeReducedID: Optional[str] = None
tradeClosedIDs: Optional[list[str]] = []
cancellingTransactionID: Optional[str] = None
replacesOrderID: Optional[str] = None
replacedByOrderID: Optional[str] = None
class OpenTradesTrade(BaseModel):
id: str
instrument: str
price: str
openTime: str
initialUnits: str
initialMarginRequired: str
state: str
currentUnits: str
realizedPL: str
financing: str
dividendAdjustment: str
unrealizedPL: str
marginUsed: str
takeProfitOrder: Optional[SideCarOrder] = None
stopLossOrder: Optional[SideCarOrder] = None
trailingStopLossOrder: Optional[SideCarOrder] = None
trailingStopValue: Optional[dict] = None
class OpenTrades(BaseModel):
trades: list[OpenTradesTrade]
lastTransactionID: str
OpenTradesSchema = {
"itemlist": (
"trades",
[
{
"id": "id",
"symbol": "instrument",
"price": "price",
"openTime": parse_time,
"initialUnits": "initialUnits",
"initialMarginRequired": "initialMarginRequired",
"state": "state",
"currentUnits": "currentUnits",
"realizedPL": "realizedPL",
"financing": "financing",
"dividendAdjustment": "dividendAdjustment",
"unrealizedPL": "unrealizedPL",
"marginUsed": "marginUsed",
"takeProfitOrder": "takeProfitOrder",
"stopLossOrder": "stopLossOrder",
"trailingStopLossOrder": "trailingStopLossOrder",
"trailingStopValue": "trailingStopValue",
"side": parse_current_units_side,
}
],
),
"lastTransactionID": "lastTransactionID",
}
class HomeConversionFactors(BaseModel):
gainQuoteHome: str
lossQuoteHome: str
gainBaseHome: str
lossBaseHome: str
class LongPositionCloseout(BaseModel):
instrument: str
units: str
class OrderTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
instrument: Optional[str] = None
units: Optional[str] = None
timeInForce: Optional[str] = None
positionFill: Optional[str] = None
reason: str
longPositionCloseout: LongPositionCloseout | None
longOrderFillTransaction: Optional[dict] = None
class OrderCreate(BaseModel):
orderCreateTransaction: OrderTransaction
OrderCreateSchema = {
"id": "orderCreateTransaction.id",
"accountID": "orderCreateTransaction.accountID",
"userID": "orderCreateTransaction.userID",
"batchID": "orderCreateTransaction.batchID",
"requestID": "orderCreateTransaction.requestID",
"time": "orderCreateTransaction.time",
"type": "orderCreateTransaction.type",
"symbol": "orderCreateTransaction.instrument",
"units": "orderCreateTransaction.units",
"timeInForce": "orderCreateTransaction.timeInForce",
"positionFill": "orderCreateTransaction.positionFill",
"reason": "orderCreateTransaction.reason",
}
class LongOrderFillTransaction(BaseModel):
id: str
accountID: str
userID: int
batchID: str
requestID: str
time: str
type: str
orderID: str
instrument: str
units: str
requestedUnits: str
price: str
pl: str
quotePL: str
financing: str
baseFinancing: str
commission: str
accountBalance: str
gainQuoteHomeConversionFactor: str
lossQuoteHomeConversionFactor: str
guaranteedExecutionFee: str
quoteGuaranteedExecutionFee: str
halfSpreadCost: str
fullVWAP: str
reason: str
tradesClosed: list[Trade]
fullPrice: Price
homeConversionFactors: HomeConversionFactors
longPositionCloseout: LongPositionCloseout
class PositionClose(BaseModel):
longOrderCreateTransaction: OrderTransaction | None
longOrderFillTransaction: OrderTransaction | None
longOrderCancelTransaction: OrderTransaction | None
shortOrderCreateTransaction: OrderTransaction | None
shortOrderFillTransaction: OrderTransaction | None
shortOrderCancelTransaction: OrderTransaction | None
relatedTransactionIDs: list[str]
lastTransactionID: str
PositionCloseSchema = {
"longOrderCreateTransaction": "longOrderCreateTransaction",
"longOrderFillTransaction": "longOrderFillTransaction",
"longOrderCancelTransaction": "longOrderCancelTransaction",
"shortOrderCreateTransaction": "shortOrderCreateTransaction",
"shortOrderFillTransaction": "shortOrderFillTransaction",
"shortOrderCancelTransaction": "shortOrderCancelTransaction",
"relatedTransactionIDs": "relatedTransactionIDs",
"lastTransactionID": "lastTransactionID",
}
class ClientExtensions(BaseModel):
id: str
tag: str
class TradeDetailsTrade(BaseModel):
id: str
instrument: str
price: str
openTime: str
initialUnits: str
initialMarginRequired: str
state: str
currentUnits: str
realizedPL: str
closingTransactionIDs: Optional[list[str]] = []
financing: str
dividendAdjustment: str
closeTime: Optional[str] = None
averageClosePrice: Optional[str] = None
clientExtensions: Optional[ClientExtensions] = None
class TradeDetails(BaseModel):
trade: TradeDetailsTrade
lastTransactionID: str
TradeDetailsSchema = {
"id": "trade.id",
"symbol": "trade.instrument",
"price": "trade.price",
"openTime": parse_time,
"initialUnits": "trade.initialUnits",
"initialMarginRequired": "trade.initialMarginRequired",
"state": "trade.state",
"currentUnits": "trade.currentUnits",
"realizedPL": "trade.realizedPL",
"closingTransactionIDs": "trade.closingTransactionIDs",
"financing": "trade.financing",
"dividendAdjustment": "trade.dividendAdjustment",
"closeTime": "trade.closeTime",
"averageClosePrice": "trade.averageClosePrice",
"clientExtensions": "trade.clientExtensions",
"lastTransactionID": "lastTransactionID",
}
class TradeClose(BaseModel):
orderCreateTransaction: OrderTransaction
TradeCloseSchema = {
"id": "orderCreateTransaction.id",
"accountID": "orderCreateTransaction.accountID",
"userID": "orderCreateTransaction.userID",
"batchID": "orderCreateTransaction.batchID",
"requestID": "orderCreateTransaction.requestID",
"time": "orderCreateTransaction.time",
"type": "orderCreateTransaction.type",
"symbol": "orderCreateTransaction.instrument",
"units": "orderCreateTransaction.units",
"timeInForce": "orderCreateTransaction.timeInForce",
"positionFill": "orderCreateTransaction.positionFill",
"reason": "orderCreateTransaction.reason",
"longPositionCloseout": "orderCreateTransaction.longPositionCloseout",
"longOrderFillTransaction": "orderCreateTransaction.longOrderFillTransaction",
}
class TradeCRCDO(BaseModel):
takeProfitOrderCancelTransaction: Optional[OrderTransaction]
takeProfitOrderTransaction: Optional[OrderTransaction]
stopLossOrderCancelTransaction: Optional[OrderTransaction]
stopLossOrderTransaction: Optional[OrderTransaction]
relatedTransactionIDs: list[str]
lastTransactionID: str
TradeCRCDOSchema = {
"takeProfitOrderCancelTransaction": "takeProfitOrderCancelTransaction",
"takeProfitOrderTransaction": "takeProfitOrderTransaction",
"stopLossOrderCancelTransaction": "stopLossOrderCancelTransaction",
"stopLossOrderTransaction": "stopLossOrderTransaction",
"relatedTransactionIDs": "relatedTransactionIDs",
"lastTransactionID": "lastTransactionID",
}

View File

@@ -1,125 +0,0 @@
from serde import Model, fields
# {
# "id": "92f0b26b-4c98-4553-9c74-cdafc7e037db",
# "clientOrderId": "ccxt_26adcbf445674f01af38a66a15e6f5b5",
# "timestamp": 1666096856515,
# "datetime": "2022-10-18T12:40:56.515477181Z",
# "lastTradeTimeStamp": null,
# "status": "open",
# "symbol": "BTC/USD",
# "type": "market",
# "timeInForce": "gtc",
# "postOnly": null,
# "side": "buy",
# "price": null,
# "stopPrice": null,
# "cost": null,
# "average": null,
# "amount": 1.1,
# "filled": 0.0,
# "remaining": 1.1,
# "trades": [],
# "fee": null,
# "info": {
# "id": "92f0b26b-4c98-4553-9c74-cdafc7e037db",
# "client_order_id": "ccxt_26adcbf445674f01af38a66a15e6f5b5",
# "created_at": "2022-10-18T12:40:56.516095561Z",
# "updated_at": "2022-10-18T12:40:56.516173841Z",
# "submitted_at": "2022-10-18T12:40:56.515477181Z",
# "filled_at": null,
# "expired_at": null,
# "canceled_at": null,
# "failed_at": null,
# "replaced_at": null,
# "replaced_by": null,
# "replaces": null,
# "asset_id": "276e2673-764b-4ab6-a611-caf665ca6340",
# "symbol": "BTC/USD",
# "asset_class": "crypto",
# "notional": null,
# "qty": "1.1",
# "filled_qty": "0",
# "filled_avg_price": null,
# "order_class": "",
# "order_type": "market",
# "type": "market",
# "side": "buy",
# "time_in_force": "gtc",
# "limit_price": null,
# "stop_price": null,
# "status": "pending_new",
# "extended_hours": false,
# "legs": null,
# "trail_percent": null,
# "trail_price": null,
# "hwm": null,
# "subtag": null,
# "source": null
# },
# "fees": [],
# "lastTradeTimestamp": null
# }
class CCXTInfo(Model):
id = fields.Uuid()
client_order_id = fields.Str()
created_at = fields.Str()
updated_at = fields.Str()
submitted_at = fields.Str()
filled_at = fields.Optional(fields.Str())
expired_at = fields.Optional(fields.Str())
canceled_at = fields.Optional(fields.Str())
failed_at = fields.Optional(fields.Str())
replaced_at = fields.Optional(fields.Str())
replaced_by = fields.Optional(fields.Str())
replaces = fields.Optional(fields.Str())
asset_id = fields.Uuid()
symbol = fields.Str()
asset_class = fields.Str()
notional = fields.Optional(fields.Str())
qty = fields.Str()
filled_qty = fields.Str()
filled_avg_price = fields.Optional(fields.Str())
order_class = fields.Str()
order_type = fields.Str()
type = fields.Str()
side = fields.Str()
time_in_force = fields.Str()
limit_price = fields.Optional(fields.Str())
stop_price = fields.Optional(fields.Str())
status = fields.Str()
extended_hours = fields.Bool()
legs = fields.Optional(fields.List(fields.Nested("CCXTInfo")))
trail_percent = fields.Optional(fields.Str())
trail_price = fields.Optional(fields.Str())
hwm = fields.Optional(fields.Str())
subtag = fields.Optional(fields.Str())
source = fields.Optional(fields.Str())
class CCXTRoot(Model):
id = fields.Uuid()
clientOrderId = fields.Str()
timestamp = fields.Int()
datetime = fields.Str()
lastTradeTimeStamp = fields.Optional(fields.Str())
status = fields.Str()
symbol = fields.Str()
type = fields.Str()
timeInForce = fields.Str()
postOnly = fields.Optional(fields.Str())
side = fields.Str()
price = fields.Optional(fields.Float())
stopPrice = fields.Optional(fields.Float())
cost = fields.Optional(fields.Float())
average = fields.Optional(fields.Float())
amount = fields.Float()
filled = fields.Float()
remaining = fields.Float()
trades = fields.Optional(fields.List(fields.Dict()))
fee = fields.Optional(fields.Float())
info = fields.Nested(CCXTInfo)
fees = fields.Optional(fields.List(fields.Dict()))
lastTradeTimestamp = fields.Optional(fields.Str())

View File

@@ -1,82 +0,0 @@
# Trade handling
from alpaca.common.exceptions import APIError
from alpaca.trading.enums import OrderSide, TimeInForce
from alpaca.trading.requests import LimitOrderRequest, MarketOrderRequest
from core.util import logs
log = logs.get_logger(__name__)
def sync_trades_with_db(user):
pass
def post_trade(trade):
# the trade is not placed yet
trading_client = trade.account.get_client()
if trade.direction == "buy":
direction = OrderSide.BUY
elif trade.direction == "sell":
direction = OrderSide.SELL
else:
raise Exception("Unknown direction")
cast = {"symbol": trade.symbol, "side": direction, "time_in_force": TimeInForce.IOC}
if trade.amount is not None:
cast["qty"] = trade.amount
if trade.amount_usd is not None:
cast["notional"] = trade.amount_usd
if not trade.amount and not trade.amount_usd:
return (False, "No amount specified")
if trade.take_profit:
cast["take_profit"] = {"limit_price": trade.take_profit}
if trade.stop_loss:
stop_limit_price = trade.stop_loss - (trade.stop_loss * 0.005)
cast["stop_loss"] = {
"stop_price": trade.stop_loss,
"limit_price": stop_limit_price,
}
if trade.type == "market":
market_order_data = MarketOrderRequest(**cast)
try:
order = trading_client.submit_order(order_data=market_order_data)
except APIError as e:
log.error(f"Error placing market order: {e}")
return (False, e)
elif trade.type == "limit":
if not trade.price:
return (False, "Limit order with no price")
cast["limit_price"] = trade.price
limit_order_data = LimitOrderRequest(**cast)
try:
order = trading_client.submit_order(order_data=limit_order_data)
except APIError as e:
log.error(f"Error placing limit order: {e}")
return (False, e)
else:
raise Exception("Unknown trade type")
trade.response = order
trade.status = "posted"
trade.order_id = order["id"]
trade.client_order_id = order["client_order_id"]
trade.save()
return (True, order)
def update_trade(self):
pass
def close_trade(trade):
pass
def get_position_info(account, asset_id):
trading_client = account.get_client()
try:
position = trading_client.get_open_position(asset_id)
except APIError as e:
return (False, e)
return (True, position)

View File

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

View File

@@ -30,8 +30,7 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('stripe_id', models.CharField(blank=True, max_length=255, null=True)),
('last_payment', models.DateTimeField(blank=True, null=True)),
('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
('email', models.EmailField(max_length=254, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
],
@@ -44,32 +43,6 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Plan',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('description', models.CharField(blank=True, max_length=1024, null=True)),
('cost', models.IntegerField()),
('product_id', models.CharField(blank=True, max_length=255, null=True, unique=True)),
('image', models.CharField(blank=True, max_length=1024, null=True)),
],
),
migrations.CreateModel(
name='Session',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('request', models.CharField(blank=True, max_length=255, null=True)),
('subscription_id', models.CharField(blank=True, max_length=255, null=True)),
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.plan')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='user',
name='plans',
field=models.ManyToManyField(blank=True, to='core.plan'),
),
migrations.AddField(
model_name='user',
name='user_permissions',

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.0.6 on 2022-10-12 09:08
# Generated by Django 4.1.7 on 2023-02-24 16:09
from django.db import migrations, models
@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
('core', '0003_user_customer_id'),
]
operations = [
migrations.AddField(
model_name='session',
name='session',
model_name='user',
name='stripe_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.1.2 on 2022-10-17 18:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0008_trade'),
]
operations = [
migrations.AddField(
model_name='trade',
name='exchange_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,83 +1,169 @@
import uuid
from datetime import timedelta
import stripe
from alpaca.common.exceptions import APIError
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import GetAssetsRequest
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.db import models
from core.lib import trades
from core.lib.customers import get_or_create, update_customer_fields
from core.exchanges.alpaca import AlpacaExchange
from core.exchanges.fake import FakeExchange
from core.exchanges.mexc import MEXCExchange
from core.exchanges.oanda import OANDAExchange
# from core.lib.customers import get_or_create, update_customer_fields
from core.lib import billing
from core.util import logs
log = logs.get_logger(__name__)
EXCHANGE_MAP = {
"alpaca": AlpacaExchange,
"oanda": OANDAExchange,
"mexc": MEXCExchange,
"fake": FakeExchange,
}
TYPE_CHOICES = (
("market", "Market"),
("limit", "Limit"),
)
DIRECTION_CHOICES = (
("buy", "Buy"),
("sell", "Sell"),
)
TIF_CHOICES = (
("gtc", "GTC (Good Til Cancelled)"),
("gfd", "GFD (Good For Day)"),
("fok", "FOK (Fill Or Kill)"),
("ioc", "IOC (Immediate Or Cancel)"),
)
DAY_CHOICES = (
(1, "Monday"),
(2, "Tuesday"),
(3, "Wednesday"),
(4, "Thursday"),
(5, "Friday"),
(6, "Saturday"),
(7, "Sunday"),
)
SIGNAL_TYPE_CHOICES = (
("entry", "Entry"),
("exit", "Exit"),
("trend", "Trend"),
)
AGGREGATION_CHOICES = (
("none", "None"),
("avg_sentiment", "Average sentiment"),
)
STATUS_CHOICES = (
(0, "No data"),
(1, "No match"),
(2, "Bullish"),
(3, "Bearish"),
(4, "No aggregation"),
(5, "Not in bounds"),
(6, "Always allow"),
(7, "Always deny"),
)
class Plan(models.Model):
name = models.CharField(max_length=255, unique=True)
description = models.CharField(max_length=1024, null=True, blank=True)
cost = models.IntegerField()
product_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
image = models.CharField(max_length=1024, null=True, blank=True)
MAPPING_CHOICES = (
(6, "Always allow"),
(7, "Always deny"),
(2, "Bullish"),
(3, "Bearish"),
)
def __str__(self):
return f"{self.name}{self.cost})"
CLOSE_NOTIFY_CHOICES = (
("none", "None"),
("close", "Close violating trades"),
("notify", "Notify only"),
)
ADJUST_CLOSE_NOTIFY_CHOICES = (
("none", "None"),
("close", "Close violating trades"),
("notify", "Notify only"),
("adjust", "Adjust violating trades"),
)
ADJUST_WITH_DIRECTION_CHOICES = (
("none", "None"),
("close", "Close violating trades"),
("notify", "Notify only"),
("adjust", "Increase and reduce"),
("adjust_up", "Increase only"),
("adjust_down", "Reduce only"),
)
# class Plan(models.Model):
# name = models.CharField(max_length=255, unique=True)
# description = models.CharField(max_length=1024, null=True, blank=True)
# cost = models.IntegerField()
# product_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
# image = models.CharField(max_length=1024, null=True, blank=True)
# def __str__(self):
# return f"{self.name} (£{self.cost})"
class User(AbstractUser):
# Stripe customer ID
stripe_id = models.CharField(max_length=255, null=True, blank=True)
last_payment = models.DateTimeField(null=True, blank=True)
plans = models.ManyToManyField(Plan, blank=True)
customer_id = models.UUIDField(default=uuid.uuid4, null=True, blank=True)
billing_provider_id = models.CharField(max_length=255, null=True, blank=True)
# last_payment = models.DateTimeField(null=True, blank=True)
# plans = models.ManyToManyField(Plan, blank=True)
email = models.EmailField(unique=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original = self
def save(self, *args, **kwargs):
"""
Override the save function to create a Stripe customer.
"""
if settings.STRIPE_ENABLED:
if not self.stripe_id: # stripe ID not stored
self.stripe_id = get_or_create(
self.email, self.first_name, self.last_name
)
to_update = {}
if self.email != self._original.email:
to_update["email"] = self.email
if self.first_name != self._original.first_name:
to_update["first_name"] = self.first_name
if self.last_name != self._original.last_name:
to_update["last_name"] = self.last_name
update_customer_fields(self.stripe_id, **to_update)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if settings.STRIPE_ENABLED:
if settings.BILLING_ENABLED:
if self.stripe_id:
stripe.Customer.delete(self.stripe_id)
log.info(f"Deleted Stripe customer {self.stripe_id}")
if self.billing_provider_id:
billing.delete_customer(self)
log.info(f"Deleted Billing customer {self.billing_provider_id}")
super().delete(*args, **kwargs)
def has_plan(self, plan):
plan_list = [plan.name for plan in self.plans.all()]
return plan in plan_list
# Override save to update attributes in Lago
def save(self, *args, **kwargs):
if self.customer_id is None:
self.customer_id = uuid.uuid4()
if settings.BILLING_ENABLED:
if not self.stripe_id: # stripe ID not stored
self.stripe_id = billing.get_or_create(
self.email, self.first_name, self.last_name
)
if not self.billing_provider_id:
self.billing_provider_id = billing.create_or_update_customer(self)
billing.update_customer_fields(self)
super().save(*args, **kwargs)
def get_notification_settings(self):
return NotificationSettings.objects.get_or_create(user=self)[0]
class Account(models.Model):
EXCHANGE_CHOICES = (("alpaca", "Alpaca"),)
EXCHANGE_CHOICES = (
("alpaca", "Alpaca"),
("oanda", "OANDA"),
("mexc", "MEXC"),
("fake", "Fake"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
exchange = models.CharField(choices=EXCHANGE_CHOICES, max_length=255)
api_key = models.CharField(max_length=255)
api_secret = models.CharField(max_length=255)
sandbox = models.BooleanField(default=False)
enabled = models.BooleanField(default=True)
supported_symbols = models.JSONField(default=list)
instruments = models.JSONField(default=list)
currency = models.CharField(max_length=255, null=True, blank=True)
initial_balance = models.FloatField(default=0)
def __str__(self):
name = f"{self.name} ({self.exchange})"
@@ -85,27 +171,34 @@ class Account(models.Model):
name += " (sandbox)"
return name
def update_info(self, save=True):
client = self.get_client()
if client:
response = client.get_instruments()
supported_symbols = client.get_supported_assets(response)
acct_info = client.get_account()
log.debug(f"Supported symbols for {self.name}: {supported_symbols}")
self.supported_symbols = supported_symbols
self.instruments = response
if "currency" in acct_info.keys():
currency = acct_info["currency"]
self.currency = currency
if save:
self.save()
def save(self, *args, **kwargs):
"""
Override the save function to update supported symbols.
"""
try:
request = GetAssetsRequest(status="active", asset_class="crypto")
assets = self.client.get_all_assets(filter=request)
asset_list = [x["symbol"] for x in assets if "symbol" in x]
self.supported_symbols = asset_list
print("Supported symbols", self.supported_symbols)
except APIError as e:
log.error(f"Could not get asset list: {e}")
# return False
if self.exchange != "fake":
self.update_info(save=False)
super().save(*args, **kwargs)
def get_client(self):
trading_client = TradingClient(
self.api_key, self.api_secret, paper=self.sandbox, raw_data=True
)
return trading_client
if self.exchange in EXCHANGE_MAP:
return EXCHANGE_MAP[self.exchange](self)
else:
raise Exception(f"Exchange not supported : {self.exchange}")
@property
def client(self):
@@ -114,54 +207,61 @@ class Account(models.Model):
"""
return self.get_client()
@property
def rawclient(self):
"""
Convenience property for one-off API calls.
"""
return self.get_client().client
@classmethod
def get_by_id(cls, account_id, user):
return cls.objects.get(id=account_id, user=user)
class Session(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
request = models.CharField(max_length=255, null=True, blank=True)
session = models.CharField(max_length=255, null=True, blank=True)
subscription_id = models.CharField(max_length=255, null=True, blank=True)
plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE)
@classmethod
def get_by_id_no_user_check(cls, account_id):
return cls.objects.get(id=account_id)
class Hook(models.Model):
DIRECTION_CHOICES = (
("buy", "Buy"),
("sell", "Sell"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=1024, null=True, blank=True, unique=True)
hook = models.CharField(max_length=255, unique=True)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
name = models.CharField(max_length=1024)
hook = models.CharField(max_length=255, unique=True) # hook URL
received = models.IntegerField(default=0)
def __str__(self):
return f"{self.name} ({self.hook})"
class Signal(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=1024)
signal = models.CharField(max_length=256) # signal name
hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
received = models.IntegerField(default=0)
type = models.CharField(choices=SIGNAL_TYPE_CHOICES, max_length=255)
def __str__(self):
return f"{self.name} ({self.hook.name}) - {self.direction}"
class Trade(models.Model):
TYPE_CHOICES = (
("market", "Market"),
("limit", "Limit"),
)
DIRECTION_CHOICES = (
("buy", "Buy"),
("sell", "Sell"),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
hook = models.ForeignKey(Hook, on_delete=models.CASCADE, null=True, blank=True)
signal = models.ForeignKey(Signal, on_delete=models.CASCADE, null=True, blank=True)
symbol = models.CharField(max_length=255)
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
type = models.CharField(choices=TYPE_CHOICES, max_length=255)
amount = models.FloatField(null=True, blank=True)
amount_usd = models.FloatField(null=True, blank=True)
price = models.FloatField(null=True, blank=True)
stop_loss = models.FloatField(null=True, blank=True)
trailing_stop_loss = models.FloatField(null=True, blank=True)
take_profit = models.FloatField(null=True, blank=True)
status = models.CharField(max_length=255, null=True, blank=True)
information = models.JSONField(null=True, blank=True)
direction = models.CharField(choices=DIRECTION_CHOICES, max_length=255)
# To populate from the trade
@@ -174,15 +274,38 @@ class Trade(models.Model):
self._original = self
def post(self):
return trades.post_trade(self)
if self.status in ["rejected", "close"]:
log.debug(f"Trade {self.id} rejected. Not posting.")
log.debug(f"Trade {self.id} information: {self.information}")
else:
return self.account.client.post_trade(self)
def delete(self, *args, **kwargs):
# close the trade
super().delete(*args, **kwargs)
@classmethod
def get_by_id(cls, trade_id, user):
return cls.objects.get(id=trade_id, user=user)
@classmethod
def get_by_id_or_order(cls, trade_id, account_id, user):
try:
account = Account.objects.get(id=account_id, user=user)
except Account.DoesNotExist:
return None
try:
return cls.objects.get(id=trade_id, account=account, user=user)
except cls.DoesNotExist:
try:
return cls.objects.get(order_id=trade_id, account=account, user=user)
except cls.DoesNotExist:
return None
class Callback(models.Model):
hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
signal = models.ForeignKey(Signal, on_delete=models.CASCADE)
title = models.CharField(max_length=1024, null=True, blank=True)
message = models.CharField(max_length=1024, null=True, blank=True)
period = models.CharField(max_length=255, null=True, blank=True)
@@ -196,36 +319,239 @@ class Callback(models.Model):
symbol = models.CharField(max_length=255)
class TradingTime(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
start_day = models.IntegerField(choices=DAY_CHOICES)
end_day = models.IntegerField(choices=DAY_CHOICES)
start_time = models.TimeField()
end_time = models.TimeField()
def within_range(self, ts):
"""
Check if the specified time is within the configured trading times.
:param ts: Timestamp
:type ts: datetime
:return: whether or not the time is within the trading range
:rtype: bool
"""
start_day = self.start_day
end_day = self.end_day
# Check the day is between the start and end day
if not start_day <= ts.weekday() + 1 <= end_day:
return False
start_time = self.start_time
end_time = self.end_time
# Get what the start time would be this week
ts_monday = ts - timedelta(days=ts.weekday())
# Now we need to add our day of week to monday
# Let's set the offset now since it's off by one
offset_start = start_day - 1
# Datetime: monday=0, tuesday=1, us: monday=1, tuesday=2, so we need to subtract
# one from ours to not be off by one
offset_end = end_day - 1
# Now we can add the offset to the monday
start = ts_monday + timedelta(days=offset_start)
start = start.replace(
hour=start_time.hour,
minute=start_time.minute,
second=start_time.second,
microsecond=start_time.microsecond,
)
end = ts_monday + timedelta(days=offset_end)
end = end.replace(
hour=end_time.hour,
minute=end_time.minute,
second=end_time.second,
microsecond=end_time.microsecond,
)
# Check if the ts is between the start and end times
# ts must be more than start and less than end
return ts >= start and ts <= end
return True
def __str__(self):
return (
f"{self.name} ({self.get_start_day_display()} at {self.start_time} - "
f"{self.get_end_day_display()} at {self.end_time})"
)
class Strategy(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
account = models.ForeignKey(Account, on_delete=models.CASCADE)
hooks = models.ManyToManyField(Hook)
trading_times = models.ManyToManyField(TradingTime)
entry_signals = models.ManyToManyField(
Signal, related_name="entry_strategies", blank=True
)
exit_signals = models.ManyToManyField(
Signal, related_name="exit_strategies", blank=True
)
trend_signals = models.ManyToManyField(
Signal, related_name="trend_strategies", blank=True
)
enabled = models.BooleanField(default=False)
take_profit_percent = models.FloatField(default=3.0)
stop_loss_percent = models.FloatField(default=1.0)
price_slippage_percent = models.FloatField(default=2.5)
trade_size_percent = models.FloatField(default=2.5)
signal_trading_enabled = models.BooleanField(default=False)
active_management_enabled = models.BooleanField(default=False)
trends = models.JSONField(null=True, blank=True)
asset_group = models.ForeignKey(
"core.AssetGroup", on_delete=models.PROTECT, null=True, blank=True
)
risk_model = models.ForeignKey(
"core.RiskModel", on_delete=models.PROTECT, null=True, blank=True
)
order_settings = models.ForeignKey(
"core.OrderSettings",
on_delete=models.PROTECT,
)
active_management_policy = models.ForeignKey(
"core.ActiveManagementPolicy",
on_delete=models.PROTECT,
null=True,
blank=True,
)
class Meta:
verbose_name_plural = "strategies"
def __str__(self):
return self.name
# class Perms(models.Model):
# class Meta:
# permissions = (
# ("bypass_hashing", "Can bypass field hashing"), #
# ("bypass_blacklist", "Can bypass the blacklist"), #
# ("bypass_encryption", "Can bypass field encryption"), #
# ("bypass_obfuscation", "Can bypass field obfuscation"), #
# ("bypass_delay", "Can bypass data delay"), #
# ("bypass_randomisation", "Can bypass data randomisation"), #
# ("post_irc", "Can post to IRC"),
# ("post_discord", "Can post to Discord"),
# ("query_search", "Can search with query strings"), #
# ("use_insights", "Can use the Insights page"),
# ("index_int", "Can use the internal index"),
# ("index_meta", "Can use the meta index"),
# ("restricted_sources", "Can access restricted sources"),
# )
class NotificationSettings(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
ntfy_url = models.CharField(max_length=255, null=True, blank=True)
def __str__(self):
return f"Notification settings for {self.user}"
class RiskModel(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
# Maximum amount of money to have lost from the initial balance to stop trading
max_loss_percent = models.FloatField(default=0.05)
# Maximum amount of money to risk on all open trades
max_risk_percent = models.FloatField(default=0.05)
# Maximum number of trades
max_open_trades = models.IntegerField(default=10)
# Maximum number of trades per symbol
max_open_trades_per_symbol = models.IntegerField(default=2)
price_slippage_percent = models.FloatField(default=2.5)
callback_price_deviation_percent = models.FloatField(default=0.5)
def __str__(self):
return self.name
class AssetGroup(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
webhook_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
when_no_data = models.IntegerField(choices=MAPPING_CHOICES, default=7)
when_no_match = models.IntegerField(choices=MAPPING_CHOICES, default=6)
when_no_aggregation = models.IntegerField(choices=MAPPING_CHOICES, default=6)
when_not_in_bounds = models.IntegerField(choices=MAPPING_CHOICES, default=6)
when_bullish = models.IntegerField(choices=MAPPING_CHOICES, default=2)
when_bearish = models.IntegerField(choices=MAPPING_CHOICES, default=3)
def __str__(self):
return self.name
@property
def matches(self):
"""
Get the total number of matches for this group.
"""
asset_rule_total = AssetRule.objects.filter(group=self).count()
asset_rule_positive = AssetRule.objects.filter(group=self, status=2).count()
return f"{asset_rule_positive}/{asset_rule_total}"
class AssetRule(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
asset = models.CharField(max_length=64)
group = models.ForeignKey(AssetGroup, on_delete=models.CASCADE)
aggregation = models.CharField(
choices=AGGREGATION_CHOICES, max_length=255, default="none"
)
value = models.FloatField(null=True, blank=True)
original_status = models.IntegerField(choices=STATUS_CHOICES, default=0)
status = models.IntegerField(choices=STATUS_CHOICES, default=0)
trigger_below = models.FloatField(null=True, blank=True)
trigger_above = models.FloatField(null=True, blank=True)
# Ensure that the asset is unique per group
class Meta:
unique_together = ("asset", "group")
class OrderSettings(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
order_type = models.CharField(
choices=TYPE_CHOICES, max_length=255, default="market"
)
time_in_force = models.CharField(choices=TIF_CHOICES, max_length=255, default="gtc")
take_profit_percent = models.FloatField(default=1.5)
stop_loss_percent = models.FloatField(default=1.0)
trailing_stop_loss_percent = models.FloatField(default=1.0, null=True, blank=True)
trade_size_percent = models.FloatField(default=0.5)
def __str__(self):
return self.name
class ActiveManagementPolicy(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
when_trading_time_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_trends_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_position_size_violated = models.CharField(
choices=ADJUST_CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_protection_violated = models.CharField(
choices=ADJUST_CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_asset_groups_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_max_open_trades_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_max_open_trades_per_symbol_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_max_loss_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_max_risk_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
when_crossfilter_violated = models.CharField(
choices=CLOSE_NOTIFY_CHOICES, max_length=255, default="none"
)
def __str__(self):
return self.name

View File

@@ -1,6 +1,6 @@
{
"background_color": "white",
"description": "Search for anything.",
"description": "Cryptocurrency/Forex/Stocks trading bot",
"display": "fullscreen",
"icons": [
{
@@ -9,7 +9,7 @@
"type": "image/png"
}
],
"name": "Pathogen Data Analytics",
"short_name": "Pathogen",
"name": "Fisk Trading Desk",
"short_name": "Fisk",
"start_url": "/"
}

View File

@@ -1,273 +1,328 @@
{% load static %}
{% load has_plan %}
{% load cache %}
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XF - {{ request.path_info }}</title>
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
<script src="{% static 'js/gridstack-all.js' %}"></script>
<script defer src="{% static 'js/magnet.min.js' %}"></script>
<script>
document.addEventListener("restore-scroll", function(event) {
var scrollpos = localStorage.getItem('scrollpos');
if (scrollpos) {
window.scrollTo(0, scrollpos)
};
});
document.addEventListener("htmx:beforeSwap", function(event) {
localStorage.setItem('scrollpos', window.scrollY);
});
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
{% cache 600 head request.path_info %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>XF - {{ request.path_info }}</title>
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
<script src="{% static 'js/gridstack-all.js' %}"></script>
<script defer src="{% static 'js/magnet.min.js' %}"></script>
<script>
document.addEventListener("restore-scroll", function(event) {
var scrollpos = localStorage.getItem('scrollpos');
if (scrollpos) {
window.scrollTo(0, scrollpos)
};
});
});
</script>
<style>
.icon { border-bottom: 0px !important;}
.wrap {
word-wrap: break-word;
}
.nowrap-parent {
white-space: nowrap;
}
.nowrap-child {
display: inline-block;
}
.htmx-indicator{
opacity:0;
transition: opacity 500ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
.htmx-request.htmx-indicator{
opacity:1
}
document.addEventListener("htmx:beforeSwap", function(event) {
localStorage.setItem('scrollpos', window.scrollY);
.tooltiptext {
visibility: hidden;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
});
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
.rounded-tooltip:hover .tooltiptext {
visibility: visible;
}
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
.table {
background: transparent !important;
}
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
tr {
transition: all 0.2s ease-in-out;
}
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
tr:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
a.panel-block {
transition: all 0.2s ease-in-out;
}
});
});
a.panel-block:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
});
</script>
<style>
.icon { border-bottom: 0px !important;}
.wrap {
word-wrap: break-word;
}
.nowrap-parent {
white-space: nowrap;
}
.nowrap-child {
display: inline-block;
}
.htmx-indicator{
opacity:0;
transition: opacity 500ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
.htmx-request.htmx-indicator{
opacity:1
}
.panel, .box, .modal {
background-color:rgba(250, 250, 250, 0.5) !important;
}
.modal, .modal.box{
background-color:rgba(210, 210, 210, 0.9) !important;
}
.modal-background{
background-color:rgba(255, 255, 255, 0.3) !important;
}
.tooltiptext {
visibility: hidden;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
.has-background-grey-lighter{
background-color:rgba(219, 219, 219, 0.5) !important;
}
.navbar {
background-color:rgba(0, 0, 0, 0.03) !important;
}
.rounded-tooltip:hover .tooltiptext {
visibility: visible;
}
.grid-stack-item-content {
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
}
.table {
background: transparent !important;
}
.panel {
display: flex !important;
flex-direction: column !important;
overflow: hidden;
}
tr {
transition: all 0.2s ease-in-out;
}
.panel-block {
overflow-y:auto;
overflow-x:auto;
min-height: 90%;
display: block;
}
tr:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
.floating-window {
/* background-color:rgba(210, 210, 210, 0.6) !important; */
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
max-height: 300px;
z-index: 9000;
position: absolute;
top: 50px;
left: 50px;
}
a.panel-block {
transition: all 0.2s ease-in-out;
}
.floating-window .panel {
background-color:rgba(250, 250, 250, 0.8) !important;
}
a.panel-block:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
.float-right {
float: right;
padding-right: 5px;
padding-left: 5px;
}
.grid-stack-item:hover .ui-resizable-handle {
display: block !important;
}
.ui-resizable-handle {
z-index: 39 !important;
}
.panel, .box, .modal {
background-color:rgba(250, 250, 250, 0.5) !important;
}
.modal, .modal.box{
background-color:rgba(210, 210, 210, 0.9) !important;
}
.modal-background{
background-color:rgba(255, 255, 255, 0.3) !important;
}
</style>
</head>
.has-background-grey-lighter{
background-color:rgba(219, 219, 219, 0.5) !important;
}
.navbar {
background-color:rgba(0, 0, 0, 0.03) !important;
}
.grid-stack-item-content {
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
}
.panel {
display: flex !important;
flex-direction: column !important;
overflow: hidden;
}
.panel-block {
overflow-y:auto;
overflow-x:auto;
min-height: 90%;
display: block;
}
.floating-window {
/* background-color:rgba(210, 210, 210, 0.6) !important; */
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
max-height: 300px;
z-index: 9000;
position: absolute;
top: 50px;
left: 50px;
}
.floating-window .panel {
background-color:rgba(250, 250, 250, 0.8) !important;
}
.float-right {
float: right;
padding-right: 5px;
padding-left: 5px;
}
.grid-stack-item:hover .ui-resizable-handle {
display: block !important;
}
.ui-resizable-handle {
z-index: 39 !important;
}
</style>
<!-- Piwik --> {# Yes it's in the source, fight me #}
<script type="text/javascript">
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
_paq.push(['setTrackerUrl', 'https://api-f98632bdcf666e3217c6c1a2bafc6c09.s.zm.is']);
_paq.push(['setSiteId', 5]);
_paq.push(['setApiToken', 'En6AFpSwq4vx3fuXEjSUY6jhUPi_MRinYBQw1FxOqsy']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src='https://c87zpt9a74m181wto33r.s.zm.is/embed.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Piwik Code -->
</head>
{% endcache %}
<body>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'home' %}">
<img src="{% static 'logo.svg' %}" width="112" height="28" alt="logo">
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="bar" class="navbar-menu">
<div class="navbar-start">
{% cache 600 nav request.user.id %}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'home' %}">
Home
<img src="{% static 'logo.svg' %}" width="112" height="28" alt="logo">
</a>
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Manage
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'positions' type='page' %}">
Positions
</a>
<a class="navbar-item" href="{% url 'trades' type='page' %}">
Bot Trades
</a>
</div>
</div>
{% endif %}
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Setup
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
Hooks
</a>
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts
</a>
<a class="navbar-item" href="{% url 'strategies' type='page' %}">
Strategies
</a>
</div>
</div>
{% endif %}
{% if settings.STRIPE_ENABLED %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'billing' %}">
Billing
</a>
{% endif %}
{% endif %}
<a class="navbar-item add-button">
Install
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="bar">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
{% if not user.is_authenticated %}
<a class="button is-info" href="{% url 'signup' %}">
<strong>Sign up</strong>
</a>
<a class="button is-light" href="{% url 'login' %}">
Log in
</a>
{% endif %}
<div id="bar" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'home' %}">
Home
</a>
{% if user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Exchange
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'profit' type='page' %}">
Profit
</a>
<a class="navbar-item" href="{% url 'positions' type='page' %}">
Positions
</a>
<a class="navbar-item" href="{% url 'trades' type='page' %}">
Trades
</a>
<a class="navbar-item" href="{% url 'accounts' type='page' %}">
Accounts
</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Setup
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'strategies' type='page' %}">
Strategies
</a>
<a class="navbar-item" href="{% url 'ordersettings' type='page' %}">
Order Settings
</a>
<a class="navbar-item" href="{% url 'signals' type='page' %}">
Signals
</a>
<a class="navbar-item" href="{% url 'hooks' type='page' %}">
Hooks
</a>
<a class="navbar-item" href="{% url 'tradingtimes' type='page' %}">
Trading Times
</a>
<a class="navbar-item" href="{% url 'risks' type='page' %}">
Risk Management
</a>
<a class="navbar-item" href="{% url 'assetgroups' type='page' %}">
Asset Groups
</a>
<a class="navbar-item" href="{% url 'ams' type='page' %}">
Active Management
</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
Account
</a>
<div class="navbar-dropdown">
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
Security
</a>
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
Notifications
</a>
</div>
</div>
{% endif %}
{% if settings.BILLING_ENABLED %}
{% if user.is_authenticated %}
<a class="button is-dark" href="{% url 'logout' %}">Logout</a>
<a class="navbar-item" href="{% url 'billing' %}">
Billing
</a>
{% endif %}
{% endif %}
<a class="navbar-item add-button">
Install
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
{% if not user.is_authenticated %}
<a class="button" href="{% url 'signup' %}">
<strong>Sign up</strong>
</a>
<a class="button" href="{% url 'two_factor:login' %}">
Log in
</a>
{% endif %}
{% if user.is_authenticated %}
<form method="POST" action="{% url 'logout' %}" style="display:inline;">
{% csrf_token %}
<button type="submit" class="button">Logout</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</nav>
</nav>
{% endcache %}
<script>
let deferredPrompt;
const addBtn = document.querySelector('.add-button');
@@ -301,7 +356,9 @@
{% endblock %}
<section class="section">
<div class="container">
{% block content %}
{% block content_wrapper %}
{% block content %}
{% endblock %}
{% endblock %}
<div id="modals-here">
</div>

View File

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

View File

@@ -3,7 +3,7 @@
{% block content %}
<section>
<p>Forgot to add something to your cart? Shop around then come back to pay!</p>
<p class="subtitle">Forgot to add something to your cart? Shop around then come back to pay!</p>
</section>
{% endblock %}

View File

@@ -1,10 +1,23 @@
{% extends "base.html" %}
{% load static %}
{% load joinsep %}
{% block outer_content %}
{% block content %}
<div class="grid-stack" id="grid-stack-main">
<div class="grid-stack-item" gs-w="7" gs-h="25" gs-y="0" gs-x="1">
<!-- <div class="grid-stack-item" gs-w="5" gs-h="14" gs-y="0" gs-x="1">
<div class="grid-stack-item-content">
<nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
Controls
</p>
<article class="panel-block is-active">
{% include 'window-content/controls.html' %}
</article>
</nav>
</div>
</div> -->
<!-- <div class="grid-stack-item" gs-w="4" gs-h="25" gs-y="0" gs-x="6">
<div class="grid-stack-item-content">
<nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
@@ -16,73 +29,105 @@
</article>
</nav>
</div>
</div>
</div>
</div> -->
<script>
var grid = GridStack.init({
cellHeight: 20,
cellWidth: 50,
cellHeightUnit: 'px',
auto: true,
float: true,
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
removable: false,
animate: true,
});
// GridStack.init();
<script>
var grid = GridStack.init({
cellHeight: 20,
cellWidth: 50,
cellHeightUnit: 'px',
auto: true,
float: true,
draggable: {handle: '.panel-heading', scroll: false, appendTo: 'body'},
removable: false,
animate: true,
});
// GridStack.init();
// a widget is ready to be loaded
document.addEventListener('load-widget', function(event) {
let container = htmx.find('#widget');
// 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);
var new_id = widgetelement.id;
// a widget is ready to be loaded
document.addEventListener('load-widget', function(event) {
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
let widgetelement = container.firstElementChild.cloneNode(true);
console.log(widgetelement);
var scripts = htmx.findAll(widgetelement, "script");
var new_id = widgetelement.id;
// check if there's an existing element like the one we want to swap
let grid_element = htmx.find('#grid-stack-main');
let existing_widget = htmx.find(grid_element, "#"+new_id);
// check if there's an existing element like the one we want to swap
let grid_element = htmx.find('#grid-stack-main');
let existing_widget = htmx.find(grid_element, "#"+new_id);
// get the size and position attributes
if (existing_widget) {
let attrs = existing_widget.getAttributeNames();
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
// get the size and position attributes
if (existing_widget) {
let attrs = existing_widget.getAttributeNames();
for (let i = 0, len = attrs.length; i < len; i++) {
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
}
}
}
// clear the queue element
container.outerHTML = "";
// container.firstElementChild.outerHTML = "";
grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement);
// update the size of the widget according to its content
var added_widget = htmx.find(grid_element, "#"+new_id);
var itemContent = htmx.find(added_widget, ".control");
var scrollheight = itemContent.scrollHeight+80;
var verticalmargin = 0;
var cellheight = grid.opts.cellHeight;
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
var opts = {
h: height,
}
grid.update(
added_widget,
opts
);
// run the JS scripts inside the added element again
for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML);
}
}
}
// clear the queue element
container.outerHTML = "";
grid.addWidget(widgetelement);
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
htmx.process(widgetelement);
// update the size of the widget according to its content
var added_widget = htmx.find(grid_element, "#"+new_id);
var itemContent = htmx.find(added_widget, ".control");
var scrollheight = itemContent.scrollHeight+80;
var verticalmargin = 0;
var cellheight = grid.opts.cellHeight;
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
var opts = {
h: height,
}
grid.update(
added_widget,
opts
);
// run the JS scripts inside the added element again
for (var i = 0; i < scripts.length; i++) {
eval(scripts[i].innerHTML);
}
});
</script>
<script>
</script>
// clear the containers we just added
// for (let x = 0, len = containers.length; x < len; x++) {
// container = containers[x];
// container.inner = "";
// }
grid.compact();
});
</script>
<div>
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'positions' type='widget' %}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'strategies' type='widget' %}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
<div
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'profit' type='widget' %}"
hx-target="#widgets-here"
hx-trigger="load"
hx-swap="afterend"
style="display: none;"></div>
</div>
{% endblock %}

View File

@@ -1,96 +1,116 @@
{% include 'partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>exchange</th>
<th>API key</th>
<th>sandbox</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.api_key }}</td>
<td>
{% if item.sandbox %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'account_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Account' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_accounts request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>exchange</th>
<th>currency</th>
<th>initial</th>
<th>API key</th>
<th>sandbox</th>
<th>enabled</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.currency }}</td>
<td>{{ item.initial_balance }}</td>
<td>{{ item.api_key }}</td>
<td>
{% if item.sandbox %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'account_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'account_info' type=type pk=item.id %}"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
{% if item.enabled %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'account_info' type=type pk=item.id %}"
hx-get="{% url 'account_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-success">
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'account_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'account_info' type=type pk=item.id %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'account_info' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
{% endcache %}

View File

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

View File

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

View File

@@ -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,50 +1,54 @@
{% include 'partials/notify.html' %}
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Callback' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_callbacks request.user.id object_list type last %}
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
<thead>
<th>id</th>
<th>hook id</th>
<th>hook name</th>
<th>title</th>
<th>message</th>
<th>period</th>
<th>sent</th>
<th>trade</th>
<th>exchange</th>
<th>symbol</th>
<th>price</th>
<th>contract</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.hook.id }}</td>
<td>
<a
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML">{{ item.hook.name }}
</a>
</td>
<td>{{ item.title }}</td>
<td>{{ item.message }}</td>
<td>{{ item.period }}</td>
<td>{{ item.sent }}</td>
<td>{{ item.trade }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.price }}</td>
<td>{{ item.contract }}</td>
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
<thead>
<th>id</th>
<th>hook id</th>
<th>hook name</th>
<th>title</th>
<th>message</th>
<th>period</th>
<th>sent</th>
<th>trade</th>
<th>exchange</th>
<th>symbol</th>
<th>price</th>
<th>contract</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.hook.id }}</td>
<td>
<a
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML">{{ item.hook.name }}
</a>
</td>
<td>{{ item.title }}</td>
<td>{{ item.message }}</td>
<td>{{ item.period }}</td>
<td>{{ item.sent }}</td>
<td>{{ item.trade }}</td>
<td>{{ item.exchange }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.price }}</td>
<td>{{ item.contract }}</td>
<td>
<div class="buttons">
<td>
<div class="buttons">
</div>
</td>
</tr>
{% endfor %}
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
{% endcache %}

View File

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

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

View File

@@ -1,86 +1,96 @@
{% include 'partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>hook</th>
<th>direction</th>
<th>received hooks</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td><code>{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/</code></td>
<td>{{ item.direction }}</td>
<td>{{ item.received }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'hook_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Hook' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_hooks request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>hook</th>
<th>received hooks</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>
<a
class="has-text-grey"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.hook }}/');">
<span class="icon" data-tooltip="Copy to clipboard">
<i class="fa-solid fa-copy" aria-hidden="true"></i>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'hook_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'callbacks' type='page' pk=item.id %}"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
</a>
</td>
<td>{{ item.received }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type=type pk=item.id %}"
hx-get="{% url 'hook_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-success">
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'hook_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'callbacks' type='page' object_type='hook' object_id=item.id %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type=type object_type='hook' object_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
{% endcache %}

View File

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

@@ -0,0 +1,36 @@
{% extends 'mixins/partials/generic-detail.html' %}
{% load cache %}
{% block tbody %}
{% cache 600 object_position_detail request.user.id object type %}
{% for key, item in object.items %}
<tr>
{% if key == 'trade_ids' %}
<th>{{ key }}</th>
<td>
{% if item is not None %}
{% for trade_id in item %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type account_id=object.account_id trade_id=trade_id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button is-small {% if trade_id in valid_trade_ids %}is-primary{% else %}is-warning{% endif %}">
{{ trade_id }}
</button>
{% endfor %}
{% endif %}
</td>
{% else %}
<th>{{ key }}</th>
<td>
{% if item is not None %}
{{ item }}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
{% endcache %}
{% endblock %}

View File

@@ -1,84 +1,105 @@
{% include 'partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="positions-table">
<thead>
<th>account</th>
<th>asset</th>
<th>price</th>
<th>quantity</th>
<th>value</th>
<th>P/L</th>
<th>side</th>
<th>actions</th>
</thead>
{% for item in items %}
<tr class="
{% if item.unrealized_pl > 0 %}has-background-success-light
{% elif item.unrealized_pl < 0 %}has-background-danger-light
{% endif %}">
<td>{{ item.account_id }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.current_price }}</td>
<td>{{ item.qty }}</td>
<td>{{ item.market_value }}</td>
<td>{{ item.unrealized_pl }}</td>
<td>{{ item.side }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
{% load cache %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_positions request.user.id object_list type %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body, every 5s"
hx-get="{{ list_url }}">
<thead>
<th>account</th>
<th>asset</th>
<th>price</th>
<th>units</th>
<th>quote</th>
<th>P/L</th>
<th>side</th>
<th>trades</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr class="
{% if item.unrealized_pl > 0 %}has-background-success-light
{% elif item.unrealized_pl < 0 %}has-background-danger-light
{% endif %}">
<td>{{ item.account }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.price }}</td>
<td>{{ item.units }}</td>
<td>{{ item.value }}</td>
<td>{{ item.unrealized_pl }}</td>
<td>
{% if item.side == 'long' %}
<span class="icon has-text-success" data-tooltip="long">
<i class="fa-solid fa-up"></i>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="#trade-close-confirm"
hx-trigger="click"
hx-target="#positions-table"
hx-confirm="Are you sure you wish to close {{ item.symbol }}?"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
{% elif item.side == 'short' %}
<span class="icon has-text-danger" data-tooltip="short">
<i class="fa-solid fa-down"></i>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'position_action' type=type account_id=item.account_id asset_id=item.asset_id %}">
{% endif %}
</td>
<td>{{ item.trade_ids|length }}</td>
<td>
<div class="buttons">
<!-- <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#"
hx-trigger="click"
hx-target="#{{ type }}s-here"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button> -->
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'position_action' side=item.side account_id=item.account_id symbol=item.symbol %}"
hx-trigger="click"
hx-target="#notification"
hx-swap="outerHTML"
hx-confirm="Are you sure you wish to close {{ item.symbol }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}">
<button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
class="button is-success">
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'position_action' type=type account_id=item.account_id symbol=item.symbol %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'position_action' type=type account_id=item.account_id asset_id=item.asset_id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
{% endcache %}

View File

@@ -1,48 +1,50 @@
{% load static %}
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Plan' as last %}
{% cache 600 objects_plans request.user.id plans last %}
{% for plan in plans %}
<div class="box">
<article class="media">
<div class="media-left">
<figure class="image is-64x64">
<img src="{% static plan.image %}" alt="Image">
</figure>
</div>
<div class="media-content">
<div class="content">
<p>
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
{% if plan in user_plans %}
<i class="fas fa-check" aria-hidden="true"></i>
{% endif %}
<br>
{{ plan.description }}
</p>
{% for plan in plans %}
<div class="box">
<article class="media">
<div class="media-left">
<figure class="image is-64x64">
<img src="{% static plan.image %}" alt="Image">
</figure>
</div>
<nav class="level is-mobile">
<div class="level-left">
{% if plan not in user_plans %}
<a class="level-item" href="/order/{{ plan.name }}">
<span class="icon is-small has-text-success">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
</a>
{% endif %}
{% if plan in user_plans %}
<a class="level-item" href="/cancel_subscription/{{ plan.name }}">
<span class="icon is-small has-text-info">
<i class="fas fa-cancel" aria-hidden="true"></i>
</span>
</a>
{% endif %}
<div class="media-content">
<div class="content">
<p class="subtitle">
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
{% if plan in user_plans %}
<i class="fas fa-check" aria-hidden="true"></i>
{% endif %}
<br>
{{ plan.description }}
</p>
</div>
</nav>
</div>
</article>
</div>
{% endfor %}
<nav class="level is-mobile">
<div class="level-left">
{% if plan not in user_plans %}
<a class="level-item" href="/order/{{ plan.name }}">
<span class="icon is-small has-text-success">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
</a>
{% endif %}
{% if plan in user_plans %}
<a class="level-item" href="/cancel_subscription/{{ plan.name }}">
<span class="icon is-small has-text-info">
<i class="fas fa-cancel" aria-hidden="true"></i>
</span>
</a>
{% endif %}
</div>
</nav>
</div>
</article>
</div>
{% endfor %}
{% endcache %}

View File

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

@@ -0,0 +1,103 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Signal' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_signals request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>signal</th>
<th>hook</th>
<th>direction</th>
<th>received hooks</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr class="
{% if item.direction == 'buy' %}has-background-success-light
{% elif item.direction == 'sell' %}has-background-danger-light
{% endif %}">
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.signal }}</td>
<td>
<a
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'hook_update' type=type pk=item.hook.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML">{{ item.hook.name }}
</a>
</td>
<td>{{ item.direction }}</td>
<td>{{ item.received }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'signal_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'callbacks' type='page' object_type='signal' object_id=item.id %}"><button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type=type object_type='signal' object_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
{% endcache %}

View File

@@ -1,98 +1,124 @@
{% include 'partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>name</th>
<th>description</th>
<th>account</th>
<th>enabled</th>
<th>TP</th>
<th>SL</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
<td>{{ item.account }}</td>
<td>
{% if item.enabled %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>{{ item.take_profit_percent }}</td>
<td>{{ item.stop_loss_percent }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'strategy_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-info">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-pencil"></i>
</span>
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Strategy' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_strategies request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>name</th>
<th>description</th>
<th>account</th>
<th>signal trading</th>
<th>active management</th>
<th>enabled</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.name }}</td>
<td>{{ item.description|truncatechars:80 }}</td>
<td>{{ item.account }}</td>
<td>
{% if item.signal_trading_enabled %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'strategy_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="#"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
{% if item.active_management_enabled %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
{% if item.enabled %}
<span class="icon">
<i class="fa-solid fa-check"></i>
</span>
{% else %}
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
{% endif %}
</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#"
hx-get="{% url 'strategy_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-success">
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'strategy_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="{% url 'trenddirections' type=type strategy_id=item.id %}"><button
class="button">
<span class="icon-text">
<span class="icon" data-tooltip="View trends">
<i class="fa-solid fa-arrows-up-down"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trenddirections' type=type strategy_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon" data-tooltip="View trends">
<i class="fa-solid fa-arrows-up-down"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
{% endcache %}

View File

@@ -1,91 +1,98 @@
{% include 'partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>status</th>
<th>account id</th>
<th>symbol</th>
<th>type</th>
<th>amount</th>
<th>price</th>
<th>SL</th>
<th>TL</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.status }}</td>
<td>{{ item.account.id }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.type }}</td>
<td>{{ item.amount }}</td>
<td>{{ item.price }}</td>
<td>{{ item.stop_loss }}</td>
<td>{{ item.take_profit }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-info">
<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 'trade_delete' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#modals-here"
hx-swap="innerHTML"
class="button is-danger">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-trash"></i>
</span>
</span>
</button>
{% if type == 'page' %}
<a href="#"><button
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Trade' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_trades request.user.id object_list type last %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>id</th>
<th>user</th>
<th>status</th>
<th>account id</th>
<th>symbol</th>
<th>type</th>
<th>amount</th>
<th>price</th>
<th>SL</th>
<th>TL</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.user }}</td>
<td>{{ item.status }}</td>
<td>{{ item.account.id }}</td>
<td>{{ item.symbol }}</td>
<td>{{ item.type }}</td>
<td>{{ item.amount }}</td>
<td>{{ item.price }}</td>
<td>{{ item.stop_loss }}</td>
<td>{{ item.take_profit }}</td>
<td>
<div class="buttons">
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="#"
hx-get="{% url 'trade_update' type=type pk=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button is-success">
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-pencil"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete="{% url 'trade_delete' type=type pk=item.id %}"
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>
{% if type == 'page' %}
<a href="{% url 'trade_action' type=type trade_id=item.id %}">
<button
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'trade_action' type=type account_id=item.account.id trade_id=item.id %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
class="button">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</table>
</table>
{% endcache %}

View File

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

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

@@ -7,12 +7,12 @@
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
<form method="POST" class="box">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<button class="button is-success">
<button class="button">
Login
</button>
</div>

View File

@@ -7,7 +7,7 @@
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
<div class="box">
<p class="has-text-danger">Registration closed.</p>
</div>

View File

@@ -7,12 +7,12 @@
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
<form method="POST" class="box">
{% csrf_token %}
{{ form|crispy }}
<div class="field">
<button class="button is-success">
<button class="button">
Sign up
</button>
</div>

View File

@@ -3,7 +3,7 @@
{% block content %}
<section>
<p>Subscription {{ plan }} cancelled!</p>
<p class="subtitle">Subscription {{ plan }} cancelled!</p>
</section>
{% endblock %}

View File

@@ -0,0 +1 @@
{% extends 'base.html' %}

View File

@@ -0,0 +1,16 @@
{% extends "two_factor/_base.html" %}
{% block content_wrapper %}
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column box is-5-tablet is-5-desktop is-4-widescreen">
{% block content %}{% endblock content %}
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% load i18n %}
<div class="buttons">
{% if cancel_url %}
<a href="{{ cancel_url }}"
class="button">{% trans "Cancel" %}</a>
{% endif %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit"
value="{{ wizard.steps.prev }}"
class="button">{% trans "Back" %}</button>
{% else %}
<button disabled name="" type="button" class="button">{% trans "Back" %}</button>
{% endif %}
<button type="submit" class="button">{% trans "Next" %}</button>
</div>

View File

@@ -0,0 +1,6 @@
{% load crispy_forms_tags %}
<table class="is-3">
{{ wizard.management_form|crispy }}
{{ wizard.form|crispy }}
</table>

View File

@@ -0,0 +1,28 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}Backup tokens can be used when your primary and backup
phone numbers aren't available. The backup tokens below can be used
for login verification. If you've used up all your backup tokens, you
can generate a new set of backup tokens. Only the backup tokens shown
below will be valid.{% endblocktrans %}</p>
{% if device.token_set.count %}
<ul>
{% for token in device.token_set.all %}
<li>{{ token.token }}</li>
{% endfor %}
</ul>
<p class="subtitle">{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p>
{% else %}
<p class="subtitle">{% trans "You don't have any backup codes yet." %}</p>
{% endif %}
<form method="post">{% csrf_token %}{{ form }}
<a href="{% url 'two_factor:profile'%}"
class="float-right button">{% trans "Back to Account Security" %}</a>
<button class="button" type="submit">{% trans "Generate Tokens" %}</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,52 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Login" %}{% endblock %}</h1>
{% if wizard.steps.current == 'auth' %}
<p class="subtitle">{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'token' %}
{% if device.method == 'call' %}
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% else %}
<p class="subtitle">{% blocktrans trimmed %}Please enter the tokens generated by your token
generator.{% endblocktrans %}</p>
{% endif %}
{% elif wizard.steps.current == 'backup' %}
<p class="subtitle">{% blocktrans trimmed %}Use this form for entering backup tokens for logging in.
These tokens have been generated for you to print and keep safe. Please
enter one of these backup tokens to login to your account.{% endblocktrans %}</p>
{% endif %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" style="display:none" />
{% if other_devices %}
<p class="subtitle">{% trans "Or, alternatively, use one of your backup phones:" %}</p>
<p class="subtitle">
{% for other in other_devices %}
<button name="challenge_device" value="{{ other.persistent_id }}"
class="button" type="submit">
{{ other.generate_challenge_button_title }}
</button>
{% endfor %}</p>
{% endif %}
{% if backup_tokens %}
<p class="subtitle">{% trans "As a last resort, you can use a backup token:" %}</p>
<p class="subtitle">
<button name="wizard_goto_step" type="submit" value="backup"
class="button">{% trans "Use Backup Token" %}</button>
</p>
{% endif %}
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}The page you requested, enforces users to verify using
two-factor authentication for security reasons. You need to enable these
security features in order to access this page.{% endblocktrans %}</p>
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.{% endblocktrans %}</p>
<div class="buttons">
<a href="javascript:history.go(-1)"
class="float-right button">{% trans "Go back" %}</a>
<a href="{% url 'two_factor:setup' %}" class="button">
{% trans "Enable Two-Factor Authentication" %}</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>
{% if wizard.steps.current == 'setup' %}
<p class="subtitle">{% blocktrans trimmed %}You'll be adding a backup phone number to your
account. This number will be used if your primary method of
registration is not available.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'validation' %}
<p class="subtitle">{% blocktrans trimmed %}We've sent a token to your phone number. Please
enter the token you've received.{% endblocktrans %}</p>
{% endif %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" style="display:none" />
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@@ -0,0 +1,56 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
{% if wizard.steps.current == 'welcome' %}
<p class="subtitle">{% blocktrans trimmed %}You are about to take your account security to the
next level. Follow the steps in this wizard to enable two-factor
authentication.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'method' %}
<p class="subtitle">{% blocktrans trimmed %}Please select which authentication method you would
like to use.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'generator' %}
<p class="subtitle">{% blocktrans trimmed %}To start using a token generator, please use your
smartphone to scan the QR code below. For example, use Google
Authenticator. Then, enter the token generated by the app.
{% endblocktrans %}</p>
<p class="subtitle"><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
{% elif wizard.steps.current == 'sms' %}
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to receive the
text messages on. This number will be validated in the next step.
{% endblocktrans %}</p>
{% elif wizard.steps.current == 'call' %}
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to be called on.
This number will be validated in the next step. {% endblocktrans %}</p>
{% elif wizard.steps.current == 'validation' %}
{% if challenge_succeeded %}
{% if device.method == 'call' %}
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
digits you hear.{% endblocktrans %}</p>
{% elif device.method == 'sms' %}
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
sent.{% endblocktrans %}</p>
{% endif %}
{% else %}
<p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've
encountered an issue with the selected authentication method. Please
go back and verify that you entered your information correctly, try
again, or use a different authentication method instead. If the issue
persists, contact the site administrator.{% endblocktrans %}</p>
{% endif %}
{% elif wizard.steps.current == 'yubikey' %}
<p class="subtitle">{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a
token in the field below. Your YubiKey will be linked to your
account.{% endblocktrans %}</p>
{% endif %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" style="display:none" />
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
authentication.{% endblocktrans %}</p>
{% if not phone_methods %}
<p class="subtitle"><a href="{% url 'two_factor:profile' %}"
class="button">{% trans "Back to Account Security" %}</a></p>
{% else %}
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
your primary token device. To enable account recovery, add a phone
number.{% endblocktrans %}</p>
<a href="{% url 'two_factor:profile' %}"
class="float-right button">{% trans "Back to Account Security" %}</a>
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
class="button">{% trans "Add Phone Number" %}</a></p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1 class="title">{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1>
<p class="subtitle">{% blocktrans trimmed %}You are about to disable two-factor authentication. This
weakens your account security, are you sure?{% endblocktrans %}</p>
<form method="post">
{% csrf_token %}
<table>{{ form }}</table>
<button class="button"
type="submit">{% trans "Disable" %}</button>
</form>
{% endblock %}

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