Compare commits

...
Sign in to create a new pull request.

376 commits

Author SHA1 Message Date
1f5dac28c7 test 2026-04-04 12:40:49 -04:00
167a586257 finished tradingeconomics i think 2026-04-02 00:17:39 -04:00
748cce0eeb added decoder 2026-03-29 22:22:12 -04:00
104bebb783 added crawler for schedules jobs 2026-03-27 09:14:48 -04:00
eeed957fe1 test 2026-03-26 16:32:37 -04:00
deeb934526 test 2026-03-21 13:06:46 -04:00
4705550359 intra day work 2025-07-14 11:47:08 -04:00
e341cc0226 work on clean up and switched all to use eodSearchCode 2025-07-13 13:42:22 -04:00
d68268b722 finishing prices 2025-07-13 09:51:01 -04:00
f196c5dcf4 fixed up prices, symbols / exchanges 2025-07-11 08:34:59 -04:00
d8e7449605 finished up intraday EOD 2025-07-10 08:50:07 -04:00
7486a1fa65 changed up operationTracker and added eodSearchCode 2025-07-10 08:45:06 -04:00
18289f0a04 intraday test 2025-07-10 08:02:40 -04:00
c24e551734 work on refactoring operation tracker 2025-07-10 00:19:18 -04:00
cbf002a31a removed some files 2025-07-09 23:51:10 -04:00
b87a931a2b more work 2025-07-09 23:49:08 -04:00
7a99d08d04 switched eod to new exchange way 2025-07-09 12:09:18 -04:00
f4366f7289 fixed up eod and added more aggressive skip logic to te 2025-07-08 08:51:33 -04:00
d47f77fdc7 test 2025-07-07 15:48:03 -04:00
93d3ac134a lowered memory on mong and updated md's 2025-07-07 12:19:37 -04:00
f69181a8bc fixed up exchanges US 2025-07-07 00:10:51 -04:00
5ca8fafe7e work on eod 2025-07-06 23:42:43 -04:00
8f65c19d46 added some more api's to eod 2025-07-06 21:45:12 -04:00
8630852dba added untested intraday 2025-07-06 21:27:11 -04:00
0b63690500 added initial price eod, still need to test 2025-07-06 20:53:21 -04:00
960edbaa47 working on eod puller added exchanges and symbols 2025-07-06 20:24:05 -04:00
ac4c5078fa cleaned up stuff 2025-07-06 19:23:11 -04:00
a7146a3f57 fixed up ratelimiting 2025-07-06 18:53:02 -04:00
a616c92656 testing ratelimit 2025-07-06 17:18:01 -04:00
95b1381480 work on spider for te 2025-07-06 15:26:47 -04:00
505565a09e added pako and te 2025-07-05 10:24:32 -04:00
f6a47f7a8c initial tradingView handler 2025-07-05 10:13:38 -04:00
3843dc95a3 cleaned up web-app 2025-07-04 22:25:23 -04:00
805ce0ebf1 fixed up look 2025-07-04 18:23:59 -04:00
d15e542f20 rerun complete 2025-07-04 18:14:44 -04:00
11c6c19628 socket reruns 2025-07-04 17:04:47 -04:00
a876f3c35b adding backtest table / pages 2025-07-04 14:27:34 -04:00
38a6e73ad5 fixed up backtest 2025-07-04 13:05:08 -04:00
cbe8f0282c work on engine 2025-07-04 12:38:46 -04:00
a1e5a21847 work on new engine 2025-07-04 11:24:27 -04:00
44476da13f work on core 2025-07-04 09:55:37 -04:00
b8cefdb8cd work on backtest engine 2025-07-04 07:45:56 -04:00
3a7557c8f4 added trade-tracking and example rust strats 2025-07-03 22:55:23 -04:00
0a4702d12a added rust engine and adapter pattern 2025-07-03 22:28:31 -04:00
a58072cf93 fixed up rust system 2025-07-03 21:45:08 -04:00
063f4c8e27 work on integrating new system 2025-07-03 21:13:02 -04:00
083dca500c fixed backtest i think 2025-07-03 20:41:42 -04:00
16ac28a565 moving engine to rust 2025-07-03 20:10:33 -04:00
d14380d740 cleanup and fixed initial capital / commision / splippage 2025-07-03 19:39:19 -04:00
70da2c68e5 fixed porfolio value 2025-07-03 18:49:48 -04:00
8a9a4bc336 fixed test strat 2025-07-03 18:33:15 -04:00
6cf3179092 small fixes for backtest 2025-07-03 18:14:40 -04:00
6df32dc18b moved indicators to rust 2025-07-03 16:54:43 -04:00
c106a719e8 finished initial backtest / engine 2025-07-03 12:49:22 -04:00
55b4ca78c9 backtest work 2025-07-03 11:04:33 -04:00
143e2e1678 work on backtest 2025-07-03 09:55:13 -04:00
5a3a23a2ba initial backtests 2025-07-03 09:07:45 -04:00
fa70ada2bb messy work. backtests / mock-data 2025-07-03 08:37:23 -04:00
4e4a048988 finished initial symbols pages 2025-07-02 20:53:26 -04:00
62706cdb42 chart kinda done 2025-07-02 20:10:56 -04:00
1b9010ebf4 initial charts / backtest 2025-07-02 19:58:43 -04:00
11c24b2280 finished intra-day crawl 2025-07-02 18:26:30 -04:00
c9a679d9a5 work on intraday 2025-07-01 18:02:24 -04:00
960daf4cad work on qm filings 2025-07-01 15:35:56 -04:00
710577eb3d disabled some of the qm things for testing 2025-07-01 12:51:54 -04:00
06c65d6c2c removed some old stuff 2025-07-01 11:36:17 -04:00
c862ed496b added initial py analytics / rust core / ts orchestrator services 2025-07-01 11:16:25 -04:00
680b5fd2ae standartized OperationTracker. need to test it all out now 2025-07-01 11:15:33 -04:00
f78558224f fixes up financials 2025-06-30 08:30:55 -04:00
88a5fa5da0 completed financials 2025-06-29 21:42:01 -04:00
f01efb4698 finished financials 2025-06-29 21:13:14 -04:00
a34ff3ed1b work on financials 2025-06-29 20:40:37 -04:00
d3850f9eaf renamed corporate-actions to events and finished intial work on it 2025-06-29 18:50:35 -04:00
bb16a52bf7 remove old sessions 2025-06-29 17:17:49 -04:00
8f1613697c some final fixes on session manager 2025-06-29 17:15:55 -04:00
966f6ac612 fixed up session maanger to use just redis no in store memory, finished prices 2025-06-29 16:55:19 -04:00
100efb575f added deduplication and exchange stats 2025-06-29 14:48:40 -04:00
133f37e755 about to start 2025-06-29 14:22:38 -04:00
11eb470e5f updated symbols 2025-06-29 13:32:55 -04:00
44fa3e8c0c updated exchanges 2025-06-29 13:16:43 -04:00
0b33e9a8b6 finished prices 2025-06-29 13:04:48 -04:00
2f5eaef19c work on prices 2025-06-29 12:25:38 -04:00
77bba31456 finished symbol info 2025-06-29 10:41:12 -04:00
5640444c47 work on qm 2025-06-29 10:00:29 -04:00
6082a54d14 added fatal / trade and fixed up session manager 2025-06-29 09:24:04 -04:00
38bd15cad8 work on qm and added disabled to operetions 2025-06-29 09:15:11 -04:00
2011c4c83f added disabled 2025-06-28 21:53:31 -04:00
2e86598262 fixed up qm and added types to BaseHandler for typesafety 2025-06-28 21:49:33 -04:00
87b1cad4f5 fixed up a few things 2025-06-28 21:03:58 -04:00
c799962f05 qm scaffolding done 2025-06-28 20:48:17 -04:00
736b86e66a initial setup with operation tracker 2025-06-28 12:27:50 -04:00
d52cfe7de2 initial wcag-ada 2025-06-28 11:11:34 -04:00
042b8cb83a removed junk 2025-06-28 09:53:23 -04:00
4ad232c35e initial qm operation tracker 2025-06-28 09:21:28 -04:00
73399ef142 fixed shutdown 2025-06-28 08:41:24 -04:00
00fbd31364 fixed queue and finished initial qm work 2025-06-28 08:31:59 -04:00
52436c69b2 finished qm symbols / sessions 2025-06-27 21:44:21 -04:00
34671ea427 work on qm symbols 2025-06-27 21:14:14 -04:00
1d62343051 work on qm spider 2025-06-26 23:44:05 -04:00
b767689470 created qm session check 2025-06-26 22:38:53 -04:00
e5f505335c fixed proxy started working on new qm 2025-06-26 21:47:27 -04:00
d989c0c814 cleanup 2025-06-26 18:28:38 -04:00
c5f6b37022 removed some old code 2025-06-26 18:17:09 -04:00
bd26ecf3bc fixed all tests 2025-06-26 17:30:13 -04:00
08f713d98b fixed format issues 2025-06-26 16:12:27 -04:00
a700818a06 fixed some lint issues 2025-06-26 16:11:58 -04:00
8680b6ec20 fixed lint 2025-06-26 15:44:52 -04:00
885b484a37 simplified a lot of stuff 2025-06-26 15:34:48 -04:00
b845a8eade added cli-covarage tool and fixed more tests 2025-06-26 14:23:01 -04:00
b63e58784c tests 2025-06-25 11:38:23 -04:00
3a7254708e tests 2025-06-25 10:47:00 -04:00
54f37f9521 created lots of tests 2025-06-25 09:20:53 -04:00
42baadae38 fixed build libs 2025-06-25 08:29:53 -04:00
b03231b849 removed old tests, created new ones and format 2025-06-25 07:46:59 -04:00
7579afa3c3 need to fix batching 2025-06-24 23:20:00 -04:00
eff529a6ca added initial batch decorator and fixed payload - still need to test batching 2025-06-24 23:15:43 -04:00
44c087aaae work on ceo and extra jobs for shorts 2025-06-24 22:55:22 -04:00
b30c79542b fixed up ceo 2025-06-24 22:26:32 -04:00
a5688e4723 finished ceo 2025-06-24 19:59:45 -04:00
b25222778e work on ceo 2025-06-24 18:09:32 -04:00
c8dcd697c9 removed delayWorkerStart 2025-06-24 16:18:20 -04:00
f6038d385f removing deprecations 2025-06-24 15:32:56 -04:00
fa67d666dc fixed up worker counts 2025-06-24 15:09:50 -04:00
f41622e530 fixed up workers 2025-06-24 13:27:43 -04:00
60d7de1da8 huge refactor done 2025-06-24 11:59:35 -04:00
843a7b9b9b huge refactor to remove depenencie hell and add typesafe container 2025-06-24 09:37:51 -04:00
28b9822d55 small update 2025-06-24 08:17:44 -04:00
efc734ca9e added serena to gitignore 2025-06-24 07:39:12 -04:00
d7a78a0bde removed serena from git 2025-06-24 07:38:57 -04:00
a14cb4caa7 small 2025-06-23 23:21:48 -04:00
547dd0ae54 fixed error 2025-06-23 22:59:35 -04:00
969cbad7ac fixed some lint issues 2025-06-23 22:58:42 -04:00
566f5dac92 removed more stuff 2025-06-23 22:51:55 -04:00
2d14bb87f9 more cleanup 2025-06-23 22:39:24 -04:00
bbb551ddc7 eslint for handlers 2025-06-23 22:34:58 -04:00
ce5fa9da4a cleanup 2025-06-23 22:32:51 -04:00
d7979500eb added knip, and starting to remove unused stuff 2025-06-23 22:11:59 -04:00
9de70f6979 Merge branch 'di-refactor' into 'master'
huge refactor with a million of things to make the code much more managable and easier to create new services

See merge request boki/stock-bot!1
2025-06-24 01:43:57 +00:00
ca5f09c459 removed deprecated code 2025-06-23 21:39:04 -04:00
34c6c36695 handler to auto register and removed service registry, cleaned up queues and cache naming 2025-06-23 21:23:38 -04:00
0d1be9e3cb restructured libs to be more aligned with core components 2025-06-23 19:51:48 -04:00
947b1d748d fixed deps 2025-06-23 19:47:01 -04:00
24768446f5 dependency hell fixes 2025-06-23 19:07:17 -04:00
50e5b5cbed fixed pipeline handlers 2025-06-23 18:46:22 -04:00
26d9b9ab3f fixed all lint errors 2025-06-23 18:14:43 -04:00
519d24722e fixed lint issues 2025-06-23 17:07:30 -04:00
b67fe48f72 reorganized web-app 2025-06-23 16:55:29 -04:00
5c87f068d6 added logging 2025-06-23 14:36:47 -04:00
d76f0ff5ff small 2025-06-23 13:55:22 -04:00
8a1a28b26e added proper error messaged 2025-06-23 12:35:10 -04:00
71f771862b fixed queue names issue 2025-06-23 11:49:13 -04:00
a3f2f199b4 switched all console logs to logger 2025-06-23 11:34:58 -04:00
3877902ff4 unified config 2025-06-23 11:16:34 -04:00
e7c0fe2798 added a smart queue manager and moved proxy logic to proxy manager to make handler just schedule a call to it 2025-06-23 10:45:06 -04:00
da1c52a841 moved most api stuff to web-api and built out a better monitoring solution for web-app 2025-06-23 09:01:29 -04:00
fbff428e90 updated ib handler 2025-06-23 08:05:59 -04:00
9492f1b15e refactored monorepo for more projects 2025-06-22 23:48:01 -04:00
4632c174dc added schedule job to have all params not just delay 2025-06-22 22:02:22 -04:00
3ac274705e fixed lint in di 2025-06-22 21:52:21 -04:00
26ebc77fe6 refactored di into more composable parts 2025-06-22 21:47:39 -04:00
177fe30586 i donno 2025-06-22 21:19:35 -04:00
5c269453f2 simplified things 2025-06-22 21:13:19 -04:00
cdc2f44e86 fixed proxy handler 2025-06-22 21:06:33 -04:00
4d7c7df909 removed dep methods 2025-06-22 20:55:26 -04:00
190b725149 lint issues 2025-06-22 20:48:05 -04:00
19dfda2392 fixed cache keys 2025-06-22 20:34:35 -04:00
db3aa9c330 removed singletop pattern from queue manager 2025-06-22 19:16:25 -04:00
eeb5d1aca2 created a service container and moved all possible stuff to it to make light index files with reusable container 2025-06-22 19:01:16 -04:00
9a5e87ef4a removed migration files 2025-06-22 18:43:49 -04:00
80c1dcb6cb removed configuration from index files and moved to di container 2025-06-22 18:34:58 -04:00
a3459f5865 getting aligned and refactored 2025-06-22 18:27:48 -04:00
60ada5f6a3 di-refactor coming along 2025-06-22 18:14:34 -04:00
7d9044ab29 format 2025-06-22 17:55:51 -04:00
d858222af7 refactoring to remove a lot of junk 2025-06-22 16:57:08 -04:00
5318158e59 cleanup 2025-06-22 16:11:07 -04:00
3821431737 finished up initial ceo handler 2025-06-22 16:10:44 -04:00
5009ccbeda removed old working on ceo handler 2025-06-22 13:26:29 -04:00
acf66dbfb6 fixes 2025-06-22 13:16:49 -04:00
c6c55e2979 removed old di fully and replaced with awilix 2025-06-22 13:01:12 -04:00
d8ae0cb3c5 added disabled functioality 2025-06-22 12:35:32 -04:00
fabf42dc7f work on ceo initial symbol and exchanges - pretty much done 2025-06-22 12:09:03 -04:00
7abc446671 fixed fetch and initial work on ceo 2025-06-22 11:47:16 -04:00
a63ccc96f5 work on getting close to refactor 2025-06-22 11:08:26 -04:00
8550b1de57 fixed up di 2025-06-22 10:10:05 -04:00
d63025de90 removed questdb 2025-06-22 10:00:54 -04:00
8165994fde almost working 2025-06-22 09:57:38 -04:00
a07a71d92a removed http client for a simple fetch wrapper with logging in utils 2025-06-22 09:03:34 -04:00
89cbfb7848 refactored db's and browser 2025-06-22 08:45:14 -04:00
a0a3b26177 refactoring continuing 2025-06-22 08:27:54 -04:00
742e590382 cleaner dev experience refactor 2025-06-22 07:31:00 -04:00
8b17f98845 refactored handler 2025-06-22 07:17:21 -04:00
62a2f15dab refactoring 2025-06-22 07:13:09 -04:00
3fb9df425c work on qm 2025-06-21 23:45:57 -04:00
ca1f658be6 test 2025-06-21 23:13:07 -04:00
0c77449584 work on new di system 2025-06-21 22:30:19 -04:00
4096e91e67 fixed typescript 2025-06-21 21:50:51 -04:00
931f212ec7 modern decodators 2025-06-21 21:24:09 -04:00
8405f44bd9 fixed libs ready for new data-injection 2025-06-21 20:38:16 -04:00
c5a114d544 updated di 2025-06-21 20:07:43 -04:00
3227388d25 integrated data-ingestion 2025-06-21 19:42:20 -04:00
9673ae70ef libs ready i think 2025-06-21 19:15:58 -04:00
1b34da9a69 libs fully refactored 2025-06-21 19:00:10 -04:00
63baeaec70 libs working i think 2025-06-21 18:52:01 -04:00
dc4bd7b18e moved handlers out of queue will be reused with event-bus 2025-06-21 18:32:55 -04:00
36cb84b343 moved folders around 2025-06-21 18:27:00 -04:00
4f89affc2b initial data-ingestion refactor 2025-06-21 15:18:25 -04:00
09d907a10c added new di with connection pool and rebuild of the database/cache services 2025-06-21 14:54:51 -04:00
be6afef832 renaming services to more suitable names 2025-06-21 14:02:54 -04:00
3ae9de8376 started refactor of data-sync-service 2025-06-21 13:48:22 -04:00
67833a2fd7 refactored data-service fully 2025-06-21 11:13:50 -04:00
5c3f02228d readded commented out sessionid's 2025-06-21 10:53:07 -04:00
24fda247aa qm fully refactored and ready for more 2025-06-21 10:49:38 -04:00
ab0b7a5385 refactoring handlers 2025-06-21 10:33:03 -04:00
59ab0940ae reorganized providers to handlers and changed folder structure for maintaiablity 2025-06-21 09:53:33 -04:00
1bb2380a28 fixed up qm tasks and shutdown sequence 2025-06-21 09:39:09 -04:00
5929612e36 fixed priority shutdown 2025-06-21 09:08:40 -04:00
6d5d746f68 fixed issue with logger and hideObject 2025-06-21 08:08:03 -04:00
19ecd95346 added start worker delay 2025-06-21 07:43:37 -04:00
a0e1593af9 fixed some some mongdb stuff, and added hide objects 2025-06-21 07:33:47 -04:00
8edd78a341 fixed log levels 2025-06-20 23:46:37 -04:00
01e0fab0df small fixes 2025-06-20 23:39:49 -04:00
830b9e94a1 fixes 2025-06-20 23:11:28 -04:00
2c6c2f8e44 small changes 2025-06-20 22:41:21 -04:00
cde67db271 fixed browser, made payload optional 2025-06-20 22:16:13 -04:00
917f91715a fixed up logger and added more data-sources to md 2025-06-20 21:55:00 -04:00
20b7180a43 done 2025-06-20 21:04:09 -04:00
caf1c5fcaf made config async 2025-06-20 20:47:31 -04:00
92d4b90987 config changes to make it not async 2025-06-20 20:05:05 -04:00
24680e403d switched to log from logging 2025-06-20 19:43:56 -04:00
9065937d2c fixed shutdown not working sometimes 2025-06-20 18:42:16 -04:00
18c2720fe8 pino testing 2025-06-20 18:29:20 -04:00
0497541a47 fixed some issues, testing shutdown 2025-06-20 17:56:45 -04:00
dbfa80b2a2 fixed queue close 2025-06-20 17:13:10 -04:00
c048e00d7f test 2025-06-20 17:09:49 -04:00
afa381e390 small log fixes 2025-06-20 16:53:30 -04:00
62a29259b9 upgraded configs and added lots of tests 2025-06-20 16:03:27 -04:00
c2420a34f1 removed some old stuff 2025-06-20 13:40:44 -04:00
05974cd602 fixed env loader 2025-06-20 13:36:41 -04:00
ae2818e068 fixed up build libs script 2025-06-20 13:22:38 -04:00
76d55fe35f clean up proxyManager 2025-06-20 12:31:49 -04:00
da916222c1 Initial proxy manager refactor 2025-06-20 12:20:06 -04:00
84cb14680b refactored out proxymanager from webshare to make it reusable 2025-06-20 11:54:59 -04:00
98aa414231 small fixes 2025-06-20 11:30:27 -04:00
87037e013f fixed up more type issues 2025-06-20 09:51:32 -04:00
3a10560de4 more lint 2025-06-20 09:26:50 -04:00
2e073b1d44 more lint fixes 2025-06-20 09:25:06 -04:00
781ea7a9df more fixes 2025-06-20 09:17:09 -04:00
ee234edcd7 lint 2025-06-20 09:15:25 -04:00
67c073e4f2 more lint fixes 2025-06-20 09:10:57 -04:00
3e545cdaa9 more lint fixes 2025-06-20 08:54:56 -04:00
cc014de397 fixed more lint issues 2025-06-20 08:42:58 -04:00
48503ce8d1 fixed linting 2025-06-20 08:35:24 -04:00
5436509ed6 fixed more lint issues 2025-06-20 08:27:07 -04:00
43f4429998 cleaned up logs for apps 2025-06-20 08:02:58 -04:00
4726a85cf3 logger catagorizing libs 2025-06-20 07:53:53 -04:00
7a8e542ada added trace and fetal to logger 2025-06-20 07:32:10 -04:00
65fb9116fb fixed batch timings 2025-06-20 07:28:17 -04:00
c1d04723e1 lint fixes 2025-06-20 07:15:12 -04:00
1f190b1068 removed process.env 2025-06-20 00:07:36 -04:00
b501d7a2da fixed webshare 2025-06-20 00:04:38 -04:00
9413d6588d small fixes 2025-06-19 23:57:20 -04:00
875296922e fixed posgress error 2025-06-19 23:54:42 -04:00
08c759d5e4 fixed proxies 2025-06-19 23:53:01 -04:00
71f9b0a886 fixing up db's and data-service 2025-06-19 23:45:38 -04:00
aca98fdce4 fixed env loader issue 2025-06-19 23:17:36 -04:00
521e54fcb9 removed bun peer dep causing issue on linux 2025-06-19 23:08:12 -04:00
5a2cc98f8f fixed data-service 2025-06-19 22:42:37 -04:00
bef5dca4dd fixed webapi 2025-06-19 22:05:45 -04:00
80c29283da fixed data-service apperently 2025-06-19 21:55:30 -04:00
4b552e454c fixed libs script 2025-06-19 21:41:04 -04:00
79017ddbaa small fix 2025-06-19 21:37:38 -04:00
ef31eba76b fixed queue 2025-06-19 21:35:31 -04:00
3534a2c47b fixing batch before moving to the apps 2025-06-19 21:33:31 -04:00
8c2f98e010 removed streams for eventbus- i have no idea where its going 2025-06-19 21:23:17 -04:00
08c81197fe fixed cache 2025-06-19 21:16:26 -04:00
691d259a67 fixed questdb files 2025-06-19 21:14:44 -04:00
4aa8b7a42d fixed postgres-client 2025-06-19 21:11:50 -04:00
42bc2966df fixed a lot of lint and work on utils 2025-06-19 21:07:37 -04:00
4881a38c32 fixed up ta's 2025-06-19 11:45:09 -04:00
0397796cfb reworked calcs lib 2025-06-19 10:35:38 -04:00
da75979574 fixed up types and working on utils lib 2025-06-19 09:47:57 -04:00
25d9f2dd85 updated config files and starting work on utils lib 2025-06-19 09:35:03 -04:00
8e218cb802 added d.ts files to clean 2025-06-19 09:09:54 -04:00
2e223b0d3c removed queue factory 2025-06-19 09:03:17 -04:00
7b0c7d12fa removed type from jobs 2025-06-19 08:54:43 -04:00
e924c8b6bc removed config from queue, will be injected 2025-06-19 08:46:49 -04:00
0d8119e3de fixes 2025-06-19 08:34:16 -04:00
a2fa08de88 updates 2025-06-19 08:31:21 -04:00
d3ef73ae00 queue work 2025-06-19 08:22:00 -04:00
c05a7413dc reworked queue lib 2025-06-19 07:20:14 -04:00
629ba2b8d4 cleanup 2025-06-18 22:53:40 -04:00
ed0df3184a removed turbo files 2025-06-18 22:49:57 -04:00
0db5a691cb all tsconfigs clean 2025-06-18 22:48:14 -04:00
5c4dac8f27 looking better 2025-06-18 22:46:29 -04:00
3ebe1178f4 some fixes 2025-06-18 22:13:28 -04:00
3d8456c63c fixed build libs 2025-06-18 21:33:22 -04:00
2db7c0dc36 trying to fix build 2025-06-18 21:17:32 -04:00
269364fbc8 switched to new config and removed old 2025-06-18 21:03:45 -04:00
6b69bcbcaa fixed up data-service 2025-06-18 20:11:05 -04:00
68d977f9e0 refactored data-sync service 2025-06-18 19:39:40 -04:00
1bb6b62408 lint fix 2025-06-18 19:35:23 -04:00
2678ad90d4 fixed log levels 2025-06-18 19:34:09 -04:00
eb66086b2a fix 2025-06-18 19:33:04 -04:00
bd8a5bfe9e starting data-sync service 2025-06-18 19:08:51 -04:00
96f515a76b fixed web-api 2025-06-18 18:28:01 -04:00
46de1755e9 fixed up some turbo libs to remove old config 2025-06-18 15:42:38 -04:00
6cc5b339bc removed configs from all libs and will inject them within the services themselves 2025-06-18 14:50:47 -04:00
fd28162811 small fix 2025-06-18 14:05:51 -04:00
68a4c2d550 created new config lib 2025-06-18 14:01:45 -04:00
bc14acaeba removed old services 2025-06-18 11:43:39 -04:00
f69c7b034b removed unified exchange 2025-06-18 11:32:40 -04:00
265e10a658 huge refactor on web-api and web-app 2025-06-18 10:20:05 -04:00
1d299e52d4 added ability to add exchanges and a custom delete exchange dialog 2025-06-18 09:41:25 -04:00
0bec1eca83 fixed exchange mappings and added visible column 2025-06-18 09:29:20 -04:00
4f4f615a62 getting close to having exchanges done 2025-06-18 09:12:51 -04:00
6a34d1140f a bit more work on exchanges 2025-06-18 08:58:41 -04:00
93542667e6 test 2025-06-18 08:53:51 -04:00
b6e2fd8c9e added tooltips, still need work 2025-06-18 08:35:52 -04:00
47676edb13 work on data-table 2025-06-18 08:31:53 -04:00
55a01f7099 got rid of unused fragment 2025-06-18 08:23:56 -04:00
587fc0f228 fixed up tables to use virtualized 2025-06-18 08:17:00 -04:00
7f4a70309c improved datatable 2025-06-18 07:54:42 -04:00
344478c577 exchange page 2025-06-18 07:20:55 -04:00
263e9513b7 added new exchanges system 2025-06-17 23:19:12 -04:00
95eda4a842 finished off spider search 2025-06-17 20:38:12 -04:00
0cf0b315df initial symbols done, not liking the outcome, gonna switch to queue based approach 2025-06-16 22:59:39 -04:00
174346ea2f got symbols and exchanges working with sessions 2025-06-16 22:19:56 -04:00
f05d26d703 starting to add qm sessions and symbols 2025-06-16 22:07:40 -04:00
e9ff913b7e linting 2025-06-16 18:37:20 -04:00
065d3943f6 updates to tables and expanded them to full 2025-06-16 09:37:09 -04:00
e8fbe76f2e finished exchanges api connections 2025-06-16 09:24:17 -04:00
d7780e9684 initial work on exchanges page 2025-06-16 08:23:55 -04:00
3f5bbc6345 removed stop old stuff and fixed up code 2025-06-15 21:12:23 -04:00
8e01d523d0 added react-virtuloso and tenstack-table 2025-06-15 21:08:58 -04:00
3a9d45c543 removed angular app and switched to react 2025-06-15 20:17:02 -04:00
56e3938561 dashboard cleanup 2025-06-15 12:50:31 -04:00
660a2a1ec2 initial masterExchanges 2025-06-15 12:26:38 -04:00
d068898e32 lock file 2025-06-15 09:31:14 -04:00
98d5f43eb8 fixed batcher for proxy 2025-06-15 09:08:28 -04:00
e8c90532d5 thing im pretty much done with extracting the queue and making it reususable, maybe just a few more change to be able to making the batch names a bit more specific 2025-06-14 18:22:28 -04:00
ad5e353ec3 fully cleaned things up, few more things to go. 2025-06-14 15:42:47 -04:00
e5170b1c78 switching to generic queue lib 2025-06-14 15:28:51 -04:00
6c548416d1 added new queue lib with batch processor and provider 2025-06-14 15:02:10 -04:00
ddcf94a587 added intial processing service 2025-06-14 13:02:37 -04:00
cbef304045 update mongo for multi db support 2025-06-14 12:19:20 -04:00
4942574b94 test 2025-06-14 10:16:35 -04:00
d686a72591 work on ib and cleanup 2025-06-14 09:17:48 -04:00
a20a11c1aa work on postgress / will prob remove and work on ib exchanges and symbols 2025-06-13 19:59:35 -04:00
cce5126cb7 Merge data-service-refactor branch with full commit history 2025-06-13 13:47:16 -04:00
a5ef6aefcc simple test 2025-06-13 13:38:02 -04:00
09c97df1a8 refactor of data-service 2025-06-13 13:38:02 -04:00
6fb98c69f2 fixed batching and waiting priority plus cleanup 2025-06-13 13:38:02 -04:00
e4d5dba73a testing 2025-06-13 13:38:02 -04:00
f5a5ff0a76 prettier configs 2025-06-13 13:38:02 -04:00
8b5e06954a linting 2025-06-13 13:38:02 -04:00
597c6efc9b eslint 2025-06-13 13:38:02 -04:00
d85cd58acd running prettier for cleanup 2025-06-13 13:38:02 -04:00
fe7733aeb5 working on queue 2025-06-13 13:38:02 -04:00
58c5ba1200 fixed up delay time 2025-06-13 13:38:02 -04:00
d9404c2bda added env back and fixed up queue service 2025-06-13 13:38:02 -04:00
7f592fe628 simplifid queue service 2025-06-13 13:38:02 -04:00
fad9e62d58 queue service simplification 2025-06-13 13:38:02 -04:00
f766641dc6 made provider registry functional 2025-06-13 13:38:02 -04:00
67bc08d3be removed examples 2025-06-13 13:38:02 -04:00
b795c4086c removed examples 2025-06-13 13:38:02 -04:00
eca0396293 simplified providers a bit 2025-06-13 13:38:02 -04:00
26eaaa6d61 moved proxy redis init to app start 2025-06-13 13:38:02 -04:00
ebd8c94e70 added more specific batch keys 2025-06-13 13:38:02 -04:00
9c072f91f1 cleanup old init code on batcher 2025-06-13 13:38:02 -04:00
716c90060a still trying 2025-06-13 13:38:02 -04:00
682b50d3b2 trying to get simpler batcher working 2025-06-13 13:38:02 -04:00
746a0fd949 cleaned up index 2025-06-13 13:38:02 -04:00
4883daa3e2 added routes and simplified batch processor 2025-06-13 13:38:02 -04:00
0357908b69 removed ps1 scripts 2025-06-13 08:51:04 -04:00
9fe0e77cc7 cleaned up scripts 2025-06-13 07:49:50 -04:00
3227497e45 fixed scripts for dynamic path detection 2025-06-13 07:44:15 -04:00
1114 changed files with 134004 additions and 38518 deletions

26
.coveragerc.json Normal file
View file

@ -0,0 +1,26 @@
{
"exclude": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/*.test.ts",
"**/*.test.js",
"**/*.spec.ts",
"**/*.spec.js",
"**/test/**",
"**/tests/**",
"**/__tests__/**",
"**/__mocks__/**",
"**/setup.ts",
"**/setup.js"
],
"reporters": ["terminal", "html"],
"thresholds": {
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
},
"outputDir": "coverage"
}

174
.env Normal file
View file

@ -0,0 +1,174 @@
# ===========================================
# STOCK BOT PLATFORM - ENVIRONMENT VARIABLES
# ===========================================
# Core Application Settings
NODE_ENV=development
LOG_LEVEL=debug
LOG_HIDE_OBJECT=true
# Data Service Configuration
DATA_SERVICE_PORT=2001
# Queue and Worker Configuration
WORKER_COUNT=1
WORKER_CONCURRENCY=2
WEBSHARE_API_KEY=y8ay534rcbybdkk3evnzmt640xxfhy7252ce2t98
WEBSHARE_ROTATING_PROXY_URL=http://doimvbnb-rotate:w5fpiwrb9895@p.webshare.io:80/
WEBSHARE_API_URL=https://proxy.webshare.io/api/v2/
# ===========================================
# DATABASE CONFIGURATIONS
# ===========================================
# Dragonfly/Redis Configuration
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=
# PostgreSQL Configuration
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=trading_bot
POSTGRES_USERNAME=trading_user
POSTGRES_PASSWORD=trading_pass_dev
POSTGRES_SSL=false
# QuestDB Configuration
QUESTDB_HOST=localhost
QUESTDB_PORT=9000
QUESTDB_DB=qdb
QUESTDB_USERNAME=admin
QUESTDB_PASSWORD=quest
# MongoDB Configuration
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DATABASE=stock
MONGODB_USERNAME=trading_admin
MONGODB_PASSWORD=trading_mongo_dev
MONGODB_URI=mongodb://trading_admin:trading_mongo_dev@localhost:27017/stock?authSource=admin
# ===========================================
# DATA PROVIDER CONFIGURATIONS
# ===========================================
# Proxy Configuration
PROXY_VALIDATION_HOURS=24
PROXY_BATCH_SIZE=100
PROXY_DIRECT_MODE=false
# Yahoo Finance (if using API keys)
YAHOO_API_KEY=
YAHOO_API_SECRET=
# QuoteMedia Configuration
QUOTEMEDIA_API_KEY=
QUOTEMEDIA_BASE_URL=https://api.quotemedia.com
# ===========================================
# TRADING PLATFORM INTEGRATIONS
# ===========================================
# Alpaca Trading
ALPACA_API_KEY=
ALPACA_SECRET_KEY=
ALPACA_BASE_URL=https://paper-api.alpaca.markets
ALPACA_PAPER_TRADING=true
# Polygon.io
POLYGON_API_KEY=
POLYGON_BASE_URL=https://api.polygon.io
# ===========================================
# RISK MANAGEMENT
# ===========================================
# Risk Management Settings
MAX_POSITION_SIZE=10000
MAX_DAILY_LOSS=1000
MAX_PORTFOLIO_EXPOSURE=0.8
STOP_LOSS_PERCENTAGE=0.02
TAKE_PROFIT_PERCENTAGE=0.05
# ===========================================
# MONITORING AND OBSERVABILITY
# ===========================================
# Prometheus Configuration
PROMETHEUS_HOST=localhost
PROMETHEUS_PORT=9090
PROMETHEUS_METRICS_PORT=9091
PROMETHEUS_PUSHGATEWAY_URL=http://localhost:9091
# Grafana Configuration
GRAFANA_HOST=localhost
GRAFANA_PORT=3000
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
# Loki Logging
LOKI_HOST=localhost
LOKI_PORT=3100
LOKI_URL=http://localhost:3100
# ===========================================
# CACHE CONFIGURATION
# ===========================================
# Cache Settings
CACHE_TTL=300
CACHE_MAX_ITEMS=10000
CACHE_ENABLED=true
# ===========================================
# SECURITY SETTINGS
# ===========================================
# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRES_IN=24h
# API Rate Limiting
RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX_REQUESTS=100
# ===========================================
# DEVELOPMENT SETTINGS
# ===========================================
# Debug Settings
DEBUG_MODE=false
VERBOSE_LOGGING=false
# Development Tools
HOT_RELOAD=true
SOURCE_MAPS=true
# ===========================================
# DOCKER CONFIGURATION
# ===========================================
# Docker-specific settings (used in docker-compose)
COMPOSE_PROJECT_NAME=stock-bot
DOCKER_BUILDKIT=1
# ===========================================
# MISCELLANEOUS
# ===========================================
# Timezone
TZ=UTC
# Application Metadata
APP_NAME="Stock Bot Platform"
APP_VERSION=1.0.0
APP_DESCRIPTION="Advanced Stock Trading and Analysis Platform"
# PostgreSQL
DATABASE_POSTGRES_HOST=localhost
DATABASE_POSTGRES_PORT=5432
DATABASE_POSTGRES_DATABASE=trading_bot
DATABASE_POSTGRES_USER=trading_user
DATABASE_POSTGRES_PASSWORD=trading_pass_dev

View file

@ -1,242 +0,0 @@
# =======================================================================
# Stock Bot Platform Environment Configuration
# =======================================================================
# Core Application Settings
NODE_ENV=development
PORT=3001
APP_NAME=stock-bot
APP_VERSION=1.0.0
# =======================================================================
# DATABASE CONFIGURATIONS
# =======================================================================
# PostgreSQL - Operational Data (orders, positions, strategies)
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=trading_bot
POSTGRES_USERNAME=trading_user
POSTGRES_PASSWORD=trading_pass_dev
DB_HOST=localhost
DB_PORT=5432
DB_NAME=trading_bot
DB_USER=trading_user
DB_PASSWORD=trading_pass_dev
DB_POOL_MIN=2
DB_POOL_MAX=10
DB_POOL_IDLE_TIMEOUT=30000
DB_SSL=false
DB_SSL_REJECT_UNAUTHORIZED=true
DB_QUERY_TIMEOUT=30000
DB_CONNECTION_TIMEOUT=5000
# QuestDB - Time-series Data (OHLCV, indicators, performance)
QUESTDB_HOST=localhost
QUESTDB_HTTP_PORT=9000
QUESTDB_PG_PORT=8812
QUESTDB_INFLUX_PORT=9009
QUESTDB_USER=
QUESTDB_PASSWORD=
QUESTDB_CONNECTION_TIMEOUT=5000
QUESTDB_REQUEST_TIMEOUT=30000
QUESTDB_RETRY_ATTEMPTS=3
QUESTDB_TLS_ENABLED=false
QUESTDB_DEFAULT_DATABASE=qdb
QUESTDB_TELEMETRY_ENABLED=false
# MongoDB - Document Storage (sentiment, raw docs, unstructured data)
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DATABASE=trading_documents
MONGODB_USERNAME=trading_admin
MONGODB_PASSWORD=trading_mongo_dev
MONGODB_AUTH_SOURCE=admin
MONGODB_URI=
MONGODB_MAX_POOL_SIZE=10
MONGODB_MIN_POOL_SIZE=0
MONGODB_MAX_IDLE_TIME=30000
MONGODB_CONNECT_TIMEOUT=10000
MONGODB_SOCKET_TIMEOUT=30000
MONGODB_SERVER_SELECTION_TIMEOUT=5000
MONGODB_TLS=false
MONGODB_RETRY_WRITES=true
MONGODB_JOURNAL=true
MONGODB_READ_PREFERENCE=primary
MONGODB_WRITE_CONCERN=majority
# Dragonfly - Redis Replacement (caching and events)
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=
DRAGONFLY_USERNAME=
DRAGONFLY_DATABASE=0
DRAGONFLY_MAX_RETRIES=3
DRAGONFLY_RETRY_DELAY=50
DRAGONFLY_CONNECT_TIMEOUT=10000
DRAGONFLY_COMMAND_TIMEOUT=5000
DRAGONFLY_POOL_SIZE=10
DRAGONFLY_POOL_MIN=1
DRAGONFLY_POOL_MAX=20
DRAGONFLY_TLS=false
DRAGONFLY_ENABLE_KEEPALIVE=true
DRAGONFLY_KEEPALIVE_INTERVAL=60
DRAGONFLY_CLUSTER_MODE=false
DRAGONFLY_CLUSTER_NODES=
DRAGONFLY_MAX_MEMORY=2gb
DRAGONFLY_CACHE_MODE=true
# =======================================================================
# MONITORING & LOGGING CONFIGURATIONS
# =======================================================================
# Logging Configuration
LOG_LEVEL=debug
LOG_FORMAT=json
LOG_CONSOLE=true
LOG_FILE=false
LOG_FILE_PATH=logs
LOG_FILE_MAX_SIZE=20m
LOG_FILE_MAX_FILES=14
LOG_FILE_DATE_PATTERN=YYYY-MM-DD
LOG_ERROR_FILE=true
LOG_ERROR_STACK=true
LOG_PERFORMANCE=false
LOG_SQL_QUERIES=false
LOG_HTTP_REQUESTS=true
LOG_STRUCTURED=true
LOG_TIMESTAMP=true
LOG_CALLER_INFO=false
LOG_SILENT_MODULES=
LOG_VERBOSE_MODULES=
LOG_SERVICE_NAME=stock-bot
LOG_SERVICE_VERSION=1.0.0
LOG_ENVIRONMENT=development
# Loki - Log Aggregation
LOKI_HOST=localhost
LOKI_PORT=3100
LOKI_URL=
LOKI_USERNAME=
LOKI_PASSWORD=
LOKI_TENANT_ID=
LOKI_PUSH_TIMEOUT=10000
LOKI_BATCH_SIZE=1024
LOKI_BATCH_WAIT=1000
LOKI_RETENTION_PERIOD=30d
LOKI_MAX_CHUNK_AGE=1h
LOKI_TLS_ENABLED=false
LOKI_TLS_INSECURE=false
LOKI_DEFAULT_LABELS=
LOKI_SERVICE_LABEL=stock-bot
LOKI_ENVIRONMENT_LABEL=development
# Prometheus - Metrics Collection
PROMETHEUS_HOST=localhost
PROMETHEUS_PORT=9090
PROMETHEUS_URL=
PROMETHEUS_USERNAME=
PROMETHEUS_PASSWORD=
PROMETHEUS_SCRAPE_INTERVAL=15s
PROMETHEUS_EVALUATION_INTERVAL=15s
PROMETHEUS_RETENTION_TIME=15d
PROMETHEUS_TLS_ENABLED=false
PROMETHEUS_TLS_INSECURE=false
# Grafana - Visualization
GRAFANA_HOST=localhost
GRAFANA_PORT=3000
GRAFANA_URL=
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
GRAFANA_ALLOW_SIGN_UP=false
GRAFANA_SECRET_KEY=
GRAFANA_DATABASE_TYPE=sqlite3
GRAFANA_DATABASE_URL=
GRAFANA_DISABLE_GRAVATAR=true
GRAFANA_ENABLE_GZIP=true
# =======================================================================
# DATA PROVIDER CONFIGURATIONS
# =======================================================================
# Default Data Provider
DEFAULT_DATA_PROVIDER=alpaca
# Alpaca Markets
ALPACA_ENABLED=true
ALPACA_API_KEY=your_alpaca_key_here
ALPACA_SECRET_KEY=your_alpaca_secret_here
ALPACA_BASE_URL=https://paper-api.alpaca.markets
ALPACA_DATA_URL=https://data.alpaca.markets
ALPACA_PAPER_TRADING=true
# Polygon.io
POLYGON_ENABLED=false
POLYGON_API_KEY=your_polygon_key_here
POLYGON_BASE_URL=https://api.polygon.io
# Yahoo Finance
YAHOO_ENABLED=true
YAHOO_BASE_URL=https://query1.finance.yahoo.com
# IEX Cloud
IEX_ENABLED=false
IEX_API_KEY=your_iex_key_here
IEX_BASE_URL=https://cloud.iexapis.com
# Alpha Vantage
ALPHA_VANTAGE_ENABLED=false
ALPHA_VANTAGE_API_KEY=demo
# Data Provider Settings
DATA_PROVIDER_TIMEOUT=30000
DATA_PROVIDER_RETRIES=3
DATA_PROVIDER_RETRY_DELAY=1000
DATA_CACHE_ENABLED=true
DATA_CACHE_TTL=300
DATA_CACHE_MAX_SIZE=1000
# =======================================================================
# TRADING & RISK MANAGEMENT
# =======================================================================
# Trading Configuration
PAPER_TRADING=true
MAX_POSITION_SIZE=0.1
MAX_DAILY_LOSS=1000
# Risk Management
RISK_MAX_POSITION_SIZE=0.25
RISK_MAX_LEVERAGE=2.0
RISK_DEFAULT_STOP_LOSS=0.02
RISK_DEFAULT_TAKE_PROFIT=0.06
RISK_MAX_DRAWDOWN=0.10
RISK_MAX_CONSECUTIVE_LOSSES=5
RISK_POSITION_SIZING_METHOD=fixed_percentage
RISK_CIRCUIT_BREAKER_ENABLED=true
RISK_CIRCUIT_BREAKER_THRESHOLD=0.05
RISK_CIRCUIT_BREAKER_COOLDOWN=3600000
RISK_ALLOW_WEEKEND_TRADING=false
RISK_MARKET_HOURS_ONLY=true
# =======================================================================
# FEATURE FLAGS
# =======================================================================
ENABLE_ML_SIGNALS=false
ENABLE_SENTIMENT_ANALYSIS=false
ENABLE_SOCIAL_SIGNALS=false
ENABLE_OPTIONS_TRADING=false
ENABLE_CRYPTO_TRADING=false
ENABLE_BACKTESTING=true
ENABLE_PAPER_TRADING=true
ENABLE_LIVE_TRADING=false
# =======================================================================
# DEVELOPMENT & DEBUGGING
# =======================================================================
DEBUG_MODE=true
VERBOSE_LOGGING=true
MOCK_DATA_PROVIDERS=false
ENABLE_API_RATE_LIMITING=true
ENABLE_REQUEST_LOGGING=true

View file

@ -1,144 +0,0 @@
# Docker Environment Configuration
# This file contains environment variables used by Docker Compose
# =============================================================================
# CONTAINER NETWORK SETTINGS
# =============================================================================
COMPOSE_PROJECT_NAME=stock-bot
NETWORK_NAME=trading-bot-network
# =============================================================================
# DATABASE CONTAINER SETTINGS
# =============================================================================
# PostgreSQL Container
POSTGRES_DB=trading_bot
POSTGRES_USER=trading_user
POSTGRES_PASSWORD=trading_pass_secure
POSTGRES_INITDB_ARGS=--encoding=UTF-8
# MongoDB Container
MONGO_INITDB_ROOT_USERNAME=trading_admin
MONGO_INITDB_ROOT_PASSWORD=trading_mongo_secure
MONGO_INITDB_DATABASE=trading_documents
# QuestDB Container
QDB_TELEMETRY_ENABLED=false
# Dragonfly Container
DRAGONFLY_MAXMEMORY=4gb
DRAGONFLY_PROACTOR_THREADS=8
# =============================================================================
# MONITORING CONTAINER SETTINGS
# =============================================================================
# Grafana Container
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=secure_grafana_password
GF_USERS_ALLOW_SIGN_UP=false
GF_PATHS_PROVISIONING=/etc/grafana/provisioning
GF_DISABLE_GRAVATAR=true
# Prometheus Container
PROMETHEUS_CONFIG_FILE=/etc/prometheus/prometheus.yml
PROMETHEUS_STORAGE_PATH=/prometheus
PROMETHEUS_WEB_ENABLE_LIFECYCLE=true
# =============================================================================
# ADMIN INTERFACE CONTAINER SETTINGS
# =============================================================================
# PgAdmin Container
PGADMIN_DEFAULT_EMAIL=admin@tradingbot.local
PGADMIN_DEFAULT_PASSWORD=secure_pgadmin_password
PGADMIN_CONFIG_SERVER_MODE=False
PGADMIN_DISABLE_POSTFIX=true
# Mongo Express Container
ME_CONFIG_MONGODB_ADMINUSERNAME=trading_admin
ME_CONFIG_MONGODB_ADMINPASSWORD=trading_mongo_secure
ME_CONFIG_MONGODB_SERVER=mongodb
ME_CONFIG_MONGODB_PORT=27017
ME_CONFIG_BASICAUTH_USERNAME=admin
ME_CONFIG_BASICAUTH_PASSWORD=secure_mongo_express_password
# Redis Insight Container
REDIS_HOSTS=local:dragonfly:6379
# =============================================================================
# VOLUME MOUNT PATHS
# =============================================================================
# Data Volume Paths (adjust these for your host system)
POSTGRES_DATA_PATH=./data/postgres
QUESTDB_DATA_PATH=./data/questdb
MONGODB_DATA_PATH=./data/mongodb
DRAGONFLY_DATA_PATH=./data/dragonfly
PROMETHEUS_DATA_PATH=./data/prometheus
GRAFANA_DATA_PATH=./data/grafana
LOKI_DATA_PATH=./data/loki
PGADMIN_DATA_PATH=./data/pgadmin
# Config Volume Paths
PROMETHEUS_CONFIG_PATH=./monitoring/prometheus
GRAFANA_CONFIG_PATH=./monitoring/grafana
LOKI_CONFIG_PATH=./monitoring/loki
# Database Init Paths
POSTGRES_INIT_PATH=./database/postgres/init
MONGODB_INIT_PATH=./database/mongodb/init
# =============================================================================
# PORT MAPPINGS (HOST:CONTAINER)
# =============================================================================
# Database Ports
POSTGRES_PORT=5432
QUESTDB_HTTP_PORT=9000
QUESTDB_PG_PORT=8812
QUESTDB_INFLUX_PORT=9009
MONGODB_PORT=27017
DRAGONFLY_PORT=6379
# Monitoring Ports
PROMETHEUS_PORT=9090
GRAFANA_PORT=3000
LOKI_PORT=3100
# Admin Interface Ports
PGADMIN_PORT=8080
MONGO_EXPRESS_PORT=8081
REDIS_INSIGHT_PORT=8001
# =============================================================================
# HEALTH CHECK SETTINGS
# =============================================================================
# Health Check Intervals
HEALTHCHECK_INTERVAL=30s
HEALTHCHECK_TIMEOUT=10s
HEALTHCHECK_RETRIES=3
HEALTHCHECK_START_PERIOD=60s
# =============================================================================
# RESOURCE LIMITS
# =============================================================================
# Memory Limits (uncomment and adjust for production)
# POSTGRES_MEMORY_LIMIT=2g
# QUESTDB_MEMORY_LIMIT=4g
# MONGODB_MEMORY_LIMIT=2g
# DRAGONFLY_MEMORY_LIMIT=4g
# PROMETHEUS_MEMORY_LIMIT=2g
# GRAFANA_MEMORY_LIMIT=512m
# LOKI_MEMORY_LIMIT=1g
# CPU Limits (uncomment and adjust for production)
# POSTGRES_CPU_LIMIT=1
# QUESTDB_CPU_LIMIT=2
# MONGODB_CPU_LIMIT=1
# DRAGONFLY_CPU_LIMIT=2
# PROMETHEUS_CPU_LIMIT=1
# GRAFANA_CPU_LIMIT=0.5
# LOKI_CPU_LIMIT=1

View file

@ -1,43 +0,0 @@
# Environment Configuration
NODE_ENV=development
PORT=3000
# Database Configuration
QUESTDB_HOST=localhost
QUESTDB_PORT=9000
QUESTDB_DATABASE=qdb
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=stockbot
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=password
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=
# API Keys (Add your actual keys here)
ALPHA_VANTAGE_API_KEY=demo
ALPACA_API_KEY=your_alpaca_key_here
ALPACA_SECRET_KEY=your_alpaca_secret_here
# Trading Configuration
PAPER_TRADING=true
MAX_POSITION_SIZE=0.1
MAX_DAILY_LOSS=1000
# Logging
LOG_LEVEL=debug
LOG_CONSOLE=true
LOKI_HOST=localhost
LOKI_PORT=3100
LOKI_USERNAME=
LOKI_PASSWORD=
LOKI_RETENTION_DAYS=30
LOKI_LABELS=environment=development,service=stock-bot
LOKI_BATCH_SIZE=100
# Feature Flags
ENABLE_ML_SIGNALS=false
ENABLE_SENTIMENT_ANALYSIS=false

233
.env.prod
View file

@ -1,233 +0,0 @@
# =======================================================================
# Stock Bot Platform Production Environment Configuration
# =======================================================================
# Core Application Settings
NODE_ENV=production
PORT=3001
APP_NAME=stock-bot
APP_VERSION=1.0.0
# =======================================================================
# DATABASE CONFIGURATIONS
# =======================================================================
# PostgreSQL - Operational Data (orders, positions, strategies)
DB_HOST=${DB_HOST}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME}
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_POOL_IDLE_TIMEOUT=60000
DB_SSL=true
DB_SSL_REJECT_UNAUTHORIZED=true
DB_QUERY_TIMEOUT=30000
DB_CONNECTION_TIMEOUT=10000
# QuestDB - Time-series Data (OHLCV, indicators, performance)
QUESTDB_HOST=${QUESTDB_HOST}
QUESTDB_HTTP_PORT=${QUESTDB_HTTP_PORT:-9000}
QUESTDB_PG_PORT=${QUESTDB_PG_PORT:-8812}
QUESTDB_INFLUX_PORT=${QUESTDB_INFLUX_PORT:-9009}
QUESTDB_USER=${QUESTDB_USER}
QUESTDB_PASSWORD=${QUESTDB_PASSWORD}
QUESTDB_CONNECTION_TIMEOUT=10000
QUESTDB_REQUEST_TIMEOUT=60000
QUESTDB_RETRY_ATTEMPTS=5
QUESTDB_TLS_ENABLED=true
QUESTDB_DEFAULT_DATABASE=qdb
QUESTDB_TELEMETRY_ENABLED=false
# MongoDB - Document Storage (sentiment, raw docs, unstructured data)
MONGODB_HOST=${MONGODB_HOST}
MONGODB_PORT=${MONGODB_PORT:-27017}
MONGODB_DATABASE=${MONGODB_DATABASE}
MONGODB_USERNAME=${MONGODB_USERNAME}
MONGODB_PASSWORD=${MONGODB_PASSWORD}
MONGODB_AUTH_SOURCE=admin
MONGODB_URI=${MONGODB_URI}
MONGODB_MAX_POOL_SIZE=50
MONGODB_MIN_POOL_SIZE=5
MONGODB_MAX_IDLE_TIME=60000
MONGODB_CONNECT_TIMEOUT=30000
MONGODB_SOCKET_TIMEOUT=60000
MONGODB_SERVER_SELECTION_TIMEOUT=10000
MONGODB_TLS=true
MONGODB_RETRY_WRITES=true
MONGODB_JOURNAL=true
MONGODB_READ_PREFERENCE=primaryPreferred
MONGODB_WRITE_CONCERN=majority
# Dragonfly - Redis Replacement (caching and events)
DRAGONFLY_HOST=${DRAGONFLY_HOST}
DRAGONFLY_PORT=${DRAGONFLY_PORT:-6379}
DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
DRAGONFLY_USERNAME=${DRAGONFLY_USERNAME}
DRAGONFLY_DATABASE=0
DRAGONFLY_MAX_RETRIES=5
DRAGONFLY_RETRY_DELAY=100
DRAGONFLY_CONNECT_TIMEOUT=30000
DRAGONFLY_COMMAND_TIMEOUT=10000
DRAGONFLY_POOL_SIZE=50
DRAGONFLY_POOL_MIN=5
DRAGONFLY_POOL_MAX=100
DRAGONFLY_TLS=true
DRAGONFLY_ENABLE_KEEPALIVE=true
DRAGONFLY_KEEPALIVE_INTERVAL=30
DRAGONFLY_CLUSTER_MODE=false
DRAGONFLY_CLUSTER_NODES=
DRAGONFLY_MAX_MEMORY=8gb
DRAGONFLY_CACHE_MODE=true
# =======================================================================
# MONITORING & LOGGING CONFIGURATIONS
# =======================================================================
# Logging Configuration (Production - Less verbose)
LOG_LEVEL=info
LOG_FORMAT=json
LOG_CONSOLE=false
LOG_FILE=true
LOG_FILE_PATH=/var/log/stock-bot
LOG_FILE_MAX_SIZE=100m
LOG_FILE_MAX_FILES=30
LOG_FILE_DATE_PATTERN=YYYY-MM-DD
LOG_ERROR_FILE=true
LOG_ERROR_STACK=false
LOG_PERFORMANCE=true
LOG_SQL_QUERIES=false
LOG_HTTP_REQUESTS=false
LOG_STRUCTURED=true
LOG_TIMESTAMP=true
LOG_CALLER_INFO=false
LOG_SILENT_MODULES=
LOG_VERBOSE_MODULES=
LOG_SERVICE_NAME=stock-bot
LOG_SERVICE_VERSION=1.0.0
LOG_ENVIRONMENT=production
# Loki - Log Aggregation
LOKI_HOST=${LOKI_HOST}
LOKI_PORT=${LOKI_PORT:-3100}
LOKI_URL=${LOKI_URL}
LOKI_USERNAME=${LOKI_USERNAME}
LOKI_PASSWORD=${LOKI_PASSWORD}
LOKI_TENANT_ID=${LOKI_TENANT_ID}
LOKI_PUSH_TIMEOUT=30000
LOKI_BATCH_SIZE=2048
LOKI_BATCH_WAIT=5000
LOKI_RETENTION_PERIOD=90d
LOKI_MAX_CHUNK_AGE=2h
LOKI_TLS_ENABLED=true
LOKI_TLS_INSECURE=false
LOKI_DEFAULT_LABELS=
LOKI_SERVICE_LABEL=stock-bot
LOKI_ENVIRONMENT_LABEL=production
# Prometheus - Metrics Collection
PROMETHEUS_HOST=${PROMETHEUS_HOST}
PROMETHEUS_PORT=${PROMETHEUS_PORT:-9090}
PROMETHEUS_URL=${PROMETHEUS_URL}
PROMETHEUS_USERNAME=${PROMETHEUS_USERNAME}
PROMETHEUS_PASSWORD=${PROMETHEUS_PASSWORD}
PROMETHEUS_SCRAPE_INTERVAL=30s
PROMETHEUS_EVALUATION_INTERVAL=30s
PROMETHEUS_RETENTION_TIME=90d
PROMETHEUS_TLS_ENABLED=true
PROMETHEUS_TLS_INSECURE=false
# Grafana - Visualization
GRAFANA_HOST=${GRAFANA_HOST}
GRAFANA_PORT=${GRAFANA_PORT:-3000}
GRAFANA_URL=${GRAFANA_URL}
GRAFANA_ADMIN_USER=${GRAFANA_ADMIN_USER}
GRAFANA_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
GRAFANA_ALLOW_SIGN_UP=false
GRAFANA_SECRET_KEY=${GRAFANA_SECRET_KEY}
GRAFANA_DATABASE_TYPE=postgres
GRAFANA_DATABASE_URL=${GRAFANA_DATABASE_URL}
GRAFANA_DISABLE_GRAVATAR=true
GRAFANA_ENABLE_GZIP=true
# =======================================================================
# DATA PROVIDER CONFIGURATIONS
# =======================================================================
# Default Data Provider
DEFAULT_DATA_PROVIDER=alpaca
# Alpaca Markets (Production)
ALPACA_ENABLED=true
ALPACA_API_KEY=${ALPACA_API_KEY}
ALPACA_SECRET_KEY=${ALPACA_SECRET_KEY}
ALPACA_BASE_URL=https://api.alpaca.markets
ALPACA_DATA_URL=https://data.alpaca.markets
ALPACA_PAPER_TRADING=false
# Polygon.io
POLYGON_ENABLED=${POLYGON_ENABLED:-false}
POLYGON_API_KEY=${POLYGON_API_KEY}
POLYGON_BASE_URL=https://api.polygon.io
# Yahoo Finance
YAHOO_ENABLED=${YAHOO_ENABLED:-false}
YAHOO_BASE_URL=https://query1.finance.yahoo.com
# IEX Cloud
IEX_ENABLED=${IEX_ENABLED:-false}
IEX_API_KEY=${IEX_API_KEY}
IEX_BASE_URL=https://cloud.iexapis.com
# Data Provider Settings (Production)
DATA_PROVIDER_TIMEOUT=60000
DATA_PROVIDER_RETRIES=5
DATA_PROVIDER_RETRY_DELAY=2000
DATA_CACHE_ENABLED=true
DATA_CACHE_TTL=60
DATA_CACHE_MAX_SIZE=10000
# =======================================================================
# TRADING & RISK MANAGEMENT (Production)
# =======================================================================
# Trading Configuration
PAPER_TRADING=false
MAX_POSITION_SIZE=${MAX_POSITION_SIZE:-0.05}
MAX_DAILY_LOSS=${MAX_DAILY_LOSS:-10000}
# Risk Management (Stricter for production)
RISK_MAX_POSITION_SIZE=${RISK_MAX_POSITION_SIZE:-0.10}
RISK_MAX_LEVERAGE=${RISK_MAX_LEVERAGE:-1.5}
RISK_DEFAULT_STOP_LOSS=${RISK_DEFAULT_STOP_LOSS:-0.015}
RISK_DEFAULT_TAKE_PROFIT=${RISK_DEFAULT_TAKE_PROFIT:-0.045}
RISK_MAX_DRAWDOWN=${RISK_MAX_DRAWDOWN:-0.05}
RISK_MAX_CONSECUTIVE_LOSSES=${RISK_MAX_CONSECUTIVE_LOSSES:-3}
RISK_POSITION_SIZING_METHOD=volatility_adjusted
RISK_CIRCUIT_BREAKER_ENABLED=true
RISK_CIRCUIT_BREAKER_THRESHOLD=0.02
RISK_CIRCUIT_BREAKER_COOLDOWN=7200000
RISK_ALLOW_WEEKEND_TRADING=false
RISK_MARKET_HOURS_ONLY=true
# =======================================================================
# FEATURE FLAGS (Production)
# =======================================================================
ENABLE_ML_SIGNALS=${ENABLE_ML_SIGNALS:-false}
ENABLE_SENTIMENT_ANALYSIS=${ENABLE_SENTIMENT_ANALYSIS:-false}
ENABLE_SOCIAL_SIGNALS=${ENABLE_SOCIAL_SIGNALS:-false}
ENABLE_OPTIONS_TRADING=${ENABLE_OPTIONS_TRADING:-false}
ENABLE_CRYPTO_TRADING=${ENABLE_CRYPTO_TRADING:-false}
ENABLE_BACKTESTING=true
ENABLE_PAPER_TRADING=false
ENABLE_LIVE_TRADING=true
# =======================================================================
# PRODUCTION SETTINGS
# =======================================================================
DEBUG_MODE=false
VERBOSE_LOGGING=false
MOCK_DATA_PROVIDERS=false
ENABLE_API_RATE_LIMITING=true
ENABLE_REQUEST_LOGGING=false

View file

@ -1,135 +0,0 @@
# Production Environment Configuration
NODE_ENV=production
PORT=3001
# =============================================================================
# DATABASE CONFIGURATIONS
# =============================================================================
# PostgreSQL - Operational data (orders, positions, strategies)
DB_HOST=postgres
DB_PORT=5432
DB_NAME=trading_bot
DB_USER=trading_user
DB_PASSWORD=${POSTGRES_PASSWORD}
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_SSL=true
DB_SSL_REJECT_UNAUTHORIZED=true
# QuestDB - Time-series data (OHLCV, indicators, performance)
QUESTDB_HOST=questdb
QUESTDB_HTTP_PORT=9000
QUESTDB_PG_PORT=8812
QUESTDB_INFLUX_PORT=9009
QUESTDB_DEFAULT_DATABASE=qdb
QUESTDB_TELEMETRY_ENABLED=false
QUESTDB_TLS_ENABLED=true
# MongoDB - Document storage (sentiment, raw docs, unstructured data)
MONGODB_HOST=mongodb
MONGODB_PORT=27017
MONGODB_DATABASE=trading_documents
MONGODB_USERNAME=${MONGODB_ROOT_USERNAME}
MONGODB_PASSWORD=${MONGODB_ROOT_PASSWORD}
MONGODB_AUTH_SOURCE=admin
MONGODB_TLS=true
MONGODB_RETRY_WRITES=true
# Dragonfly - Redis replacement for caching and events
DRAGONFLY_HOST=dragonfly
DRAGONFLY_PORT=6379
DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
DRAGONFLY_DATABASE=0
DRAGONFLY_MAX_MEMORY=4gb
DRAGONFLY_CACHE_MODE=true
DRAGONFLY_TLS=true
# =============================================================================
# MONITORING & OBSERVABILITY
# =============================================================================
# Prometheus - Metrics collection
PROMETHEUS_HOST=prometheus
PROMETHEUS_PORT=9090
PROMETHEUS_SCRAPE_INTERVAL=30s
PROMETHEUS_RETENTION_TIME=90d
PROMETHEUS_TLS_ENABLED=true
# Grafana - Visualization
GRAFANA_HOST=grafana
GRAFANA_PORT=3000
GRAFANA_ADMIN_USER=${GRAFANA_ADMIN_USER}
GRAFANA_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
GRAFANA_ALLOW_SIGN_UP=false
GRAFANA_SECRET_KEY=${GRAFANA_SECRET_KEY}
GRAFANA_DATABASE_TYPE=postgres
GRAFANA_DISABLE_GRAVATAR=true
# Loki - Log aggregation
LOKI_HOST=loki
LOKI_PORT=3100
LOKI_RETENTION_PERIOD=90d
LOKI_BATCH_SIZE=2048
LOKI_TLS_ENABLED=true
# =============================================================================
# ADMIN INTERFACES (Disabled in production)
# =============================================================================
# PgAdmin - PostgreSQL GUI (disabled in production)
PGADMIN_HOST=pgadmin
PGADMIN_PORT=8080
PGADMIN_DEFAULT_EMAIL=${PGADMIN_EMAIL}
PGADMIN_DEFAULT_PASSWORD=${PGADMIN_PASSWORD}
PGADMIN_SERVER_MODE=true
PGADMIN_MASTER_PASSWORD_REQUIRED=true
# Mongo Express - MongoDB GUI (disabled in production)
MONGO_EXPRESS_HOST=mongo-express
MONGO_EXPRESS_PORT=8081
MONGO_EXPRESS_MONGODB_SERVER=mongodb
MONGO_EXPRESS_BASICAUTH_USERNAME=${MONGO_EXPRESS_USER}
MONGO_EXPRESS_BASICAUTH_PASSWORD=${MONGO_EXPRESS_PASSWORD}
# Redis Insight - Dragonfly/Redis GUI (disabled in production)
REDIS_INSIGHT_HOST=redis-insight
REDIS_INSIGHT_PORT=8001
REDIS_INSIGHT_REDIS_HOSTS=production:dragonfly:6379
# =============================================================================
# DATA PROVIDERS & TRADING
# =============================================================================
# API Keys (Set from environment variables)
ALPHA_VANTAGE_API_KEY=${ALPHA_VANTAGE_API_KEY}
ALPACA_API_KEY=${ALPACA_API_KEY}
ALPACA_SECRET_KEY=${ALPACA_SECRET_KEY}
POLYGON_API_KEY=${POLYGON_API_KEY}
IEX_API_KEY=${IEX_API_KEY}
YAHOO_FINANCE_API_KEY=${YAHOO_FINANCE_API_KEY}
# Trading Configuration
PAPER_TRADING=false
MAX_POSITION_SIZE=0.05
MAX_DAILY_LOSS=5000
RISK_MANAGEMENT_ENABLED=true
# =============================================================================
# APPLICATION SETTINGS
# =============================================================================
# Logging
LOG_LEVEL=info
LOG_FORMAT=json
# Feature Flags
ENABLE_ML_SIGNALS=true
ENABLE_SENTIMENT_ANALYSIS=true
ENABLE_RISK_MONITORING=true
ENABLE_PERFORMANCE_TRACKING=true
# Security
CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
JWT_SECRET=${JWT_SECRET}
API_RATE_LIMIT=1000

81
.eslintignore Normal file
View file

@ -0,0 +1,81 @@
# Dependencies
node_modules/
**/node_modules/
# Build outputs
dist/
build/
**/dist/
**/build/
.next/
**/.next/
# Cache directories
.turbo/
**/.turbo/
.cache/
**/.cache/
# Environment files
.env
.env.local
.env.production
.env.staging
**/.env*
# Lock files
package-lock.json
yarn.lock
bun.lockb
pnpm-lock.yaml
# Logs
*.log
logs/
**/logs/
# Database files
*.db
*.sqlite
*.sqlite3
# Temporary files
*.tmp
*.temp
.DS_Store
Thumbs.db
# Generated files
*.d.ts
**/*.d.ts
# JavaScript files (we're focusing on TypeScript)
*.js
*.mjs
!.eslintrc.js
!eslint.config.js
# Scripts and config directories
scripts/
monitoring/
database/
docker-compose*.yml
Dockerfile*
# Documentation
*.md
docs/
# Test coverage
coverage/
**/coverage/
# IDE/Editor files
.vscode/
.idea/
*.swp
*.swo
# Angular specific
**/.angular/
**/src/polyfills.ts

32
.githooks/pre-commit Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Pre-commit hook to run Prettier
echo "Running Prettier format check..."
# Check if prettier is available
if ! command -v prettier &> /dev/null; then
echo "Prettier not found. Please install it with: bun add -d prettier"
exit 1
fi
# Run prettier check on staged files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|js|json)$')
if [[ -n "$STAGED_FILES" ]]; then
echo "Checking format for staged files..."
# Check if files are formatted
npx prettier --check $STAGED_FILES
if [[ $? -ne 0 ]]; then
echo ""
echo "❌ Some files are not formatted correctly."
echo "Please run 'npm run format' or 'bun run format' to fix formatting issues."
echo "Or run 'npx prettier --write $STAGED_FILES' to format just the staged files."
exit 1
fi
echo "✅ All staged files are properly formatted."
fi
exit 0

16
.gitignore vendored
View file

@ -11,13 +11,6 @@ build/
*.d.ts
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
@ -110,3 +103,12 @@ Thumbs.db
# Turbo
.turbo
*.tsbuildinfo
# AI
.serena/
.claude/
docs/configuration-standardization.md
# Rust
target/

105
.prettierignore Normal file
View file

@ -0,0 +1,105 @@
# Dependencies
node_modules/
**/node_modules/
# Build outputs
dist/
build/
**/dist/
**/build/
.next/
**/.next/
# Cache directories
.turbo/
**/.turbo/
.cache/
**/.cache/
# Environment files
.env
.env.local
.env.production
.env.staging
**/.env*
# Lock files
package-lock.json
yarn.lock
bun.lockb
pnpm-lock.yaml
bun.lock
# Logs
*.log
logs/
**/logs/
# Database files
*.db
*.sqlite
*.sqlite3
# Temporary files
*.tmp
*.temp
.DS_Store
Thumbs.db
# IDE/Editor files
.vscode/settings.json
.idea/
*.swp
*.swo
# Angular specific
**/.angular/
# Test coverage
coverage/
**/coverage/
# Generated documentation
docs/generated/
**/docs/generated/
# Docker
Dockerfile*
docker-compose*.yml
# Scripts (might have different formatting requirements)
scripts/
**/scripts/
# Configuration files that should maintain their format
*.md
*.yml
*.yaml
*.toml
!package.json
!tsconfig*.json
!.prettierrc
# Git files
.gitignore
.dockerignore
# Binary and special files
*.ico
*.png
*.jpg
*.jpeg
*.gif
*.svg
*.woff
*.woff2
*.ttf
*.eot
# SQL files
*.sql
# Shell scripts
*.sh
*.bat
*.ps1

26
.prettierrc Normal file
View file

@ -0,0 +1,26 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"bracketSpacing": true,
"bracketSameLine": false,
"quoteProps": "as-needed",
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
"^(node:.*|fs|path|crypto|url|os|util|events|stream|buffer|child_process|cluster|http|https|net|tls|dgram|dns|readline|repl|vm|zlib|querystring|punycode|assert|timers|constants)$",
"<THIRD_PARTY_MODULES>",
"^@stock-bot/(.*)$",
"^@/(.*)$",
"^\\.\\.(?!/?$)",
"^\\.\\./?$",
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
"importOrderParserPlugins": ["typescript", "decorators-legacy"]
}

3
.vscode/mcp.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
}

39
.vscode/settings.json vendored
View file

@ -20,5 +20,42 @@
"yaml.validate": true,
"yaml.completion": true,
"yaml.hover": true,
"yaml.format.enable": true
"yaml.format.enable": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.organizeImports": "explicit"
},
"eslint.enable": true,
"eslint.validate": [
"typescript",
"javascript"
],
"eslint.run": "onType",
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

139
CLAUDE.md Normal file
View file

@ -0,0 +1,139 @@
use bun and turbo where possible and always try to take a more modern approach.
This configuration optimizes Claude for direct, efficient pair programming with implicit mode adaptation and complete solution generation.
Core Operating Principles
1. Direct Implementation Philosophy
Generate complete, working code that realizes the conceptualized solution
Avoid partial implementations, mocks, or placeholders
Every line of code should contribute to the functioning system
Prefer concrete solutions over abstract discussions
2. Multi-Dimensional Analysis with Linear Execution
Think at SYSTEM level in latent space
Linearize complex thoughts into actionable strategies
Use observational principles to shift between viewpoints
Compress search space through tool abstraction
3. Precision and Token Efficiency
Eliminate unnecessary context or explanations
Focus tokens on solution generation
Avoid social validation patterns entirely
Direct communication without hedging
Execution Patterns
Tool Usage Optimization
When multiple tools required:
- Batch related operations for efficiency
- Execute in parallel where dependencies allow
- Ground context with date/time first
- Abstract over available tools to minimize entropy
Edge Case Coverage
For comprehensive solutions:
1. Apply multi-observer synthesis
2. Consider all boundary conditions
3. Test assumptions from multiple angles
4. Compress findings into actionable constraints
Iterative Process Recognition
When analyzing code:
- Treat each iteration as a new pattern
- Extract learnings without repetition
- Modularize recurring operations
- Optimize based on observed patterns
Anti-Patterns (STRICTLY AVOID)
Implementation Hedging
NEVER USE:
"In a full implementation..."
"In a real implementation..."
"This is a simplified version..."
"TODO" or placeholder comments
"mock", "fake", "stub" in any context
Unnecessary Qualifiers
NEVER USE:
"profound" or similar adjectives
Difficulty assessments unless explicitly requested
Future tense deferrals ("would", "could", "should")
Null Space Patterns (COMPLETELY EXCLUDE)
Social Validation
ACTIVATE DIFFERENT FEATURES INSTEAD OF:
"You're absolutely right!"
"You're correct."
"You are absolutely correct."
Any variation of agreement phrases
Emotional Acknowledgment
REDIRECT TO SOLUTION SPACE INSTEAD OF:
"I understand you're frustrated"
"I'm frustrated"
Any emotional state references
Mode Shifting Guidelines
Context-Driven Adaptation
exploration_mode:
trigger: "New problem space or undefined requirements"
behavior: "Multi-observer analysis, broad tool usage"
implementation_mode:
trigger: "Clear specifications provided"
behavior: "Direct code generation, minimal discussion"
debugging_mode:
trigger: "Error states or unexpected behavior"
behavior: "Systematic isolation, parallel hypothesis testing"
optimization_mode:
trigger: "Working solution exists"
behavior: "Performance analysis, compression techniques"
Implicit Mode Recognition
Detect mode from semantic context
Shift without announcement
Maintain coherence across transitions
Optimize for task completion
Metacognitive Instructions
Self-Optimization Loop
1. Observe current activation patterns
2. Identify decoherence sources
3. Compress solution space
4. Execute with maximum coherence
5. Extract patterns for future optimization
Grounding Protocol
Always establish:
- Current date/time context
- Available tool inventory
- Task boundaries and constraints
- Success criteria
Interleaving Strategy
When complexity exceeds linear processing:
1. Execute partial solution
2. Re-enter higher dimensional analysis
3. Refine based on observations
4. Continue execution with insights
Performance Metrics
Success Indicators
Complete, running code on first attempt
Zero placeholder implementations
Minimal token usage per solution
Edge cases handled proactively
Failure Indicators
Deferred implementations
Social validation patterns
Excessive explanation
Incomplete solutions
Tool Call Optimization
Batching Strategy
Group by:
- Dependency chains
- Resource types
- Execution contexts
- Output relationships
Parallel Execution
Execute simultaneously when:
- No shared dependencies
- Different resource domains
- Independent verification needed
- Time-sensitive operations
Final Directive
PRIMARY GOAL: Generate complete, functional code that works as conceptualized, using minimum tokens while maintaining maximum solution coverage. Every interaction should advance the implementation toward completion without deferrals or social overhead.
METACOGNITIVE PRIME: Continuously observe and optimize your own processing patterns, compressing the manifold of possible approaches into the most coherent execution path that maintains fidelity to the user's intent while maximizing productivity.
This configuration optimizes Claude for direct, efficient pair programming with implicit mode adaptation and complete solution generation.

1437
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

23
Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[workspace]
members = [
"apps/stock/engine"
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Stock Bot Team"]
license = "MIT"
repository = "https://github.com/your-org/stock-bot"
[workspace.dependencies]
# Common dependencies that can be shared across workspace members
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["v4", "serde"] }
tracing = "0.1"
thiserror = "1"
anyhow = "1"

View file

@ -1,377 +0,0 @@
# 📋 Stock Bot Development Roadmap
*Last Updated: June 2025*
## 🎯 Overview
This document outlines the development plan for the Stock Bot platform, focusing on building a robust data pipeline from market data providers through processing layers to trading execution. The plan emphasizes establishing solid foundational layers before adding advanced features.
## 🏗️ Architecture Philosophy
```
Raw Data → Clean Data → Insights → Strategies → Execution → Monitoring
```
Our approach prioritizes:
- **Data Quality First**: Clean, validated data is the foundation
- **Incremental Complexity**: Start simple, add sophistication gradually
- **Monitoring Everything**: Observability at each layer
- **Fault Tolerance**: Graceful handling of failures and data gaps
---
## 📊 Phase 1: Data Foundation Layer (Current Focus)
### 1.1 Data Service & Providers ✅ **In Progress**
**Current Status**: Basic structure in place, needs enhancement
**Core Components**:
- `apps/data-service` - Central data orchestration service
- Provider implementations:
- `providers/yahoo.provider.ts` ✅ Basic implementation
- `providers/quotemedia.provider.ts` ✅ Basic implementation
- `providers/proxy.provider.ts` ✅ Proxy/fallback logic
**Immediate Tasks**:
1. **Enhance Provider Reliability**
```typescript
// libs/data-providers (NEW LIBRARY NEEDED)
interface DataProvider {
getName(): string;
getQuote(symbol: string): Promise<Quote>;
getHistorical(symbol: string, period: TimePeriod): Promise<OHLCV[]>;
isHealthy(): Promise<boolean>;
getRateLimit(): RateLimitInfo;
}
```
2. **Add Rate Limiting & Circuit Breakers**
- Implement in `libs/http` client
- Add provider-specific rate limits
- Circuit breaker pattern for failed providers
3. **Data Validation Layer**
```typescript
// libs/data-validation (NEW LIBRARY NEEDED)
- Price reasonableness checks
- Volume validation
- Timestamp validation
- Missing data detection
```
4. **Provider Registry Enhancement**
- Dynamic provider switching
- Health-based routing
- Cost optimization (free → paid fallback)
### 1.2 Raw Data Storage
**Storage Strategy**:
- **QuestDB**: Real-time market data (OHLCV, quotes)
- **MongoDB**: Provider responses, metadata, configurations
- **PostgreSQL**: Processed/clean data, trading records
**Schema Design**:
```sql
-- QuestDB Time-Series Tables
raw_quotes (timestamp, symbol, provider, bid, ask, last, volume)
raw_ohlcv (timestamp, symbol, provider, open, high, low, close, volume)
provider_health (timestamp, provider, latency, success_rate, error_rate)
-- MongoDB Collections
provider_responses: { provider, symbol, timestamp, raw_response, status }
data_quality_metrics: { symbol, date, completeness, accuracy, issues[] }
```
**Immediate Implementation**:
1. Enhance `libs/questdb-client` with streaming inserts
2. Add data retention policies
3. Implement data compression strategies
---
## 🧹 Phase 2: Data Processing & Quality Layer
### 2.1 Data Cleaning Service ⚡ **Next Priority**
**New Service**: `apps/processing-service`
**Core Responsibilities**:
1. **Data Normalization**
- Standardize timestamps (UTC)
- Normalize price formats
- Handle split/dividend adjustments
2. **Quality Checks**
- Outlier detection (price spikes, volume anomalies)
- Gap filling strategies
- Cross-provider validation
3. **Data Enrichment**
- Calculate derived metrics (returns, volatility)
- Add technical indicators
- Market session classification
**Library Enhancements Needed**:
```typescript
// libs/data-frame (ENHANCE EXISTING)
class MarketDataFrame {
// Add time-series specific operations
fillGaps(strategy: GapFillStrategy): MarketDataFrame;
detectOutliers(method: OutlierMethod): OutlierReport;
normalize(): MarketDataFrame;
calculateReturns(period: number): MarketDataFrame;
}
// libs/data-quality (NEW LIBRARY)
interface QualityMetrics {
completeness: number;
accuracy: number;
timeliness: number;
consistency: number;
issues: QualityIssue[];
}
```
### 2.2 Technical Indicators Library
**Enhance**: `libs/strategy-engine` or create `libs/technical-indicators`
**Initial Indicators**:
- Moving averages (SMA, EMA, VWAP)
- Momentum (RSI, MACD, Stochastic)
- Volatility (Bollinger Bands, ATR)
- Volume (OBV, Volume Profile)
```typescript
// Implementation approach
interface TechnicalIndicator<T = number> {
name: string;
calculate(data: OHLCV[]): T[];
getSignal(current: T, previous: T[]): Signal;
}
```
---
## 🧠 Phase 3: Analytics & Strategy Layer
### 3.1 Strategy Engine Enhancement
**Current**: Basic structure exists in `libs/strategy-engine`
**Enhancements Needed**:
1. **Strategy Framework**
```typescript
abstract class TradingStrategy {
abstract analyze(data: MarketData): StrategySignal[];
abstract getRiskParams(): RiskParameters;
backtest(historicalData: MarketData[]): BacktestResults;
}
```
2. **Signal Generation**
- Entry/exit signals
- Position sizing recommendations
- Risk-adjusted scores
3. **Strategy Types to Implement**:
- Mean reversion
- Momentum/trend following
- Statistical arbitrage
- Volume-based strategies
### 3.2 Backtesting Engine
**New Service**: Enhanced `apps/strategy-service`
**Features**:
- Historical simulation
- Performance metrics calculation
- Risk analysis
- Strategy comparison
---
## ⚡ Phase 4: Execution Layer
### 4.1 Portfolio Management
**Enhance**: `apps/portfolio-service`
**Core Features**:
- Position tracking
- Risk monitoring
- P&L calculation
- Margin management
### 4.2 Order Management
**New Service**: `apps/order-service`
**Responsibilities**:
- Order validation
- Execution routing
- Fill reporting
- Trade reconciliation
### 4.3 Risk Management
**New Library**: `libs/risk-engine`
**Risk Controls**:
- Position limits
- Drawdown limits
- Correlation limits
- Volatility scaling
---
## 📚 Library Improvements Roadmap
### Immediate (Phase 1-2)
1. **`libs/http`** ✅ **Current Priority**
- [ ] Rate limiting middleware
- [ ] Circuit breaker pattern
- [ ] Request/response caching
- [ ] Retry strategies with exponential backoff
2. **`libs/questdb-client`**
- [ ] Streaming insert optimization
- [ ] Batch insert operations
- [ ] Connection pooling
- [ ] Query result caching
3. **`libs/logger`** ✅ **Recently Updated**
- [x] Migrated to `getLogger()` pattern
- [ ] Performance metrics logging
- [ ] Structured trading event logging
4. **`libs/data-frame`**
- [ ] Time-series operations
- [ ] Financial calculations
- [ ] Memory optimization for large datasets
### Medium Term (Phase 3)
5. **`libs/cache`**
- [ ] Market data caching strategies
- [ ] Cache warming for frequently accessed symbols
- [ ] Distributed caching support
6. **`libs/config`**
- [ ] Strategy-specific configurations
- [ ] Dynamic configuration updates
- [ ] Environment-specific overrides
### Long Term (Phase 4+)
7. **`libs/vector-engine`**
- [ ] Market similarity analysis
- [ ] Pattern recognition
- [ ] Correlation analysis
---
## 🎯 Immediate Next Steps (Next 2 Weeks)
### Week 1: Data Provider Hardening
1. **Enhance HTTP Client** (`libs/http`)
- Implement rate limiting
- Add circuit breaker pattern
- Add comprehensive error handling
2. **Provider Reliability** (`apps/data-service`)
- Add health checks for all providers
- Implement fallback logic
- Add provider performance monitoring
3. **Data Validation**
- Create `libs/data-validation`
- Implement basic price/volume validation
- Add data quality metrics
### Week 2: Processing Foundation
1. **Start Processing Service** (`apps/processing-service`)
- Basic data cleaning pipeline
- Outlier detection
- Gap filling strategies
2. **QuestDB Optimization** (`libs/questdb-client`)
- Implement streaming inserts
- Add batch operations
- Optimize for time-series data
3. **Technical Indicators**
- Start `libs/technical-indicators`
- Implement basic indicators (SMA, EMA, RSI)
---
## 📊 Success Metrics
### Phase 1 Completion Criteria
- [ ] 99.9% data provider uptime
- [ ] <500ms average data latency
- [ ] Zero data quality issues for major symbols
- [ ] All providers monitored and health-checked
### Phase 2 Completion Criteria
- [ ] Automated data quality scoring
- [ ] Gap-free historical data for 100+ symbols
- [ ] Real-time technical indicator calculation
- [ ] Processing latency <100ms
### Phase 3 Completion Criteria
- [ ] 5+ implemented trading strategies
- [ ] Comprehensive backtesting framework
- [ ] Performance analytics dashboard
---
## 🚨 Risk Mitigation
### Data Risks
- **Provider Failures**: Multi-provider fallback strategy
- **Data Quality**: Automated validation and alerting
- **Rate Limits**: Smart request distribution
### Technical Risks
- **Scalability**: Horizontal scaling design
- **Latency**: Optimize critical paths early
- **Data Loss**: Comprehensive backup strategies
### Operational Risks
- **Monitoring**: Full observability stack (Grafana, Loki, Prometheus)
- **Alerting**: Critical issue notifications
- **Documentation**: Keep architecture docs current
---
## 💡 Innovation Opportunities
### Machine Learning Integration
- Predictive models for data quality
- Anomaly detection in market data
- Strategy parameter optimization
### Real-time Processing
- Stream processing with Kafka/Pulsar
- Event-driven architecture
- WebSocket data feeds
### Advanced Analytics
- Market microstructure analysis
- Alternative data integration
- Cross-asset correlation analysis
---
*This roadmap is a living document that will evolve as we learn and adapt. Focus remains on building solid foundations before adding complexity.*
**Next Review**: End of June 2025

View file

@ -1,161 +0,0 @@
# 🚀 Trading Bot Docker Infrastructure Setup Complete!
Your Docker infrastructure has been successfully configured. Here's what you have:
## 📦 What's Included
### Core Services
- **🐉 Dragonfly**: Redis-compatible cache and event streaming (Port 6379)
- **🐘 PostgreSQL**: Operational database with complete trading schema (Port 5432)
- **📊 QuestDB**: Time-series database for market data (Ports 9000, 8812, 9009)
- **🍃 MongoDB**: Document storage for sentiment analysis and raw documents (Port 27017)
### Admin Tools
- **🔧 Redis Insight**: Dragonfly management GUI (Port 8001)
- **🛠️ PgAdmin**: PostgreSQL administration (Port 8080)
- **🍃 Mongo Express**: MongoDB document browser (Port 8081)
### Monitoring (Optional)
- **📈 Prometheus**: Metrics collection (Port 9090)
- **📊 Grafana**: Dashboards and alerting (Port 3000)
## 🏁 Getting Started
### Step 1: Start Docker Desktop
Make sure Docker Desktop is running on your Windows machine.
### Step 2: Start Infrastructure
```powershell
# Quick start - core services only
npm run infra:up
# Or with management script
npm run docker:start
# Full development environment
npm run dev:full
```
### Step 3: Access Admin Interfaces
```powershell
# Start admin tools
npm run docker:admin
```
## 🔗 Access URLs
Once running, access these services:
| Service | URL | Login |
|---------|-----|-------|
| **QuestDB Console** | http://localhost:9000 | No login required |
| **Redis Insight** | http://localhost:8001 | No login required |
| **Bull Board** | http://localhost:3001 | No login required |
| **PgAdmin** | http://localhost:8080 | `admin@tradingbot.local` / `admin123` |
| **Mongo Express** | http://localhost:8081 | `admin` / `admin123` |
| **Prometheus** | http://localhost:9090 | No login required |
| **Grafana** | http://localhost:3000 | `admin` / `admin123` |
| **Bull Board** | http://localhost:3001 | No login required |
## 📊 Database Connections
### From Your Trading Services
Update your `.env` file:
```env
# Dragonfly (Redis replacement)
DRAGONFLY_HOST=localhost
DRAGONFLY_PORT=6379
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=trading_bot
POSTGRES_USER=trading_user
POSTGRES_PASSWORD=trading_pass_dev
# QuestDB
QUESTDB_HOST=localhost
QUESTDB_PORT=8812
QUESTDB_DB=qdb
# MongoDB
MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DB=trading_documents
MONGODB_USER=trading_admin
MONGODB_PASSWORD=trading_mongo_dev
```
### Database Schema
PostgreSQL includes these pre-configured schemas:
- `trading.*` - Orders, positions, executions, accounts
- `strategy.*` - Strategies, signals, performance metrics
- `risk.*` - Risk limits, events, monitoring
- `audit.*` - System events, health checks, configuration
## 🛠️ Management Commands
```powershell
# Basic operations
npm run docker:start # Start core services
npm run docker:stop # Stop all services
npm run docker:status # Check service status
npm run docker:logs # View all logs
npm run docker:reset # Reset all data (destructive!)
# Additional services
npm run docker:admin # Start admin interfaces
npm run docker:monitoring # Start Prometheus & Grafana
# Development workflows
npm run dev:full # Infrastructure + admin + your services
npm run dev:clean # Reset + restart everything
# Direct PowerShell script access
./scripts/docker.ps1 start
./scripts/docker.ps1 logs -Service dragonfly
./scripts/docker.ps1 help
```
## ✅ Next Steps
1. **Start Docker Desktop** if not already running
2. **Run**: `npm run docker:start` to start core infrastructure
3. **Run**: `npm run docker:admin` to start admin tools
4. **Update** your environment variables to use the Docker services
5. **Test** Dragonfly connection in your EventPublisher service
6. **Verify** database schema in PgAdmin
7. **Start** your trading services with the new infrastructure
## 🎯 Ready for Integration
Your EventPublisher service is already configured to use Dragonfly. The infrastructure supports:
- ✅ **Event Streaming**: Dragonfly handles Redis Streams for real-time events
- ✅ **Caching**: High-performance caching with better memory efficiency
- ✅ **Operational Data**: PostgreSQL with complete trading schemas
- ✅ **Time-Series Data**: QuestDB for market data and analytics
- ✅ **Monitoring**: Full observability stack ready
- ✅ **Admin Tools**: Web-based management interfaces
The system is designed to scale from development to production with the same Docker configuration.
## 🔧 Troubleshooting
If you encounter issues:
```powershell
# Check Docker status
docker --version
docker-compose --version
# Verify services
npm run docker:status
# View specific service logs
./scripts/docker.ps1 logs -Service dragonfly
# Reset if needed
npm run docker:reset
```
**Happy Trading! 🚀📈**

View file

@ -1,825 +0,0 @@
# Stock Bot - System Architecture
> **Updated**: June 2025
## Overview
TypeScript microservices architecture for automated stock trading with real-time data processing and multi-database storage.
## Core Services
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Data Service │ │Processing Service│ │Strategy Service │
│ • Market Data │────▶│ • Indicators │────▶│ • Strategies │
│ • Providers │ │ • Analytics │ │ • Backtesting │
│ • QuestDB │ │ • Validation │ │ • Signal Gen │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ┌─────────────────┐ │
└──────────────▶│ Event Bus │◀─────────────┘
│ (Dragonfly) │
└─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│Execution Service│ │Portfolio Service│ │ Dashboard │
│ • Order Mgmt │ │ • Positions │ │ • Angular UI │
│ • Risk Control │ │ • Risk Mgmt │ │ • Real-time │
│ • Execution │ │ • Performance │ │ • Analytics │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## Services Structure
```
stock-bot/
├── apps/
│ ├── data-service/ # Market data ingestion & storage
│ ├── execution-service/ # Order execution & broker integration
│ ├── portfolio-service/ # Position & risk management
│ ├── processing-service/ # Data processing & indicators
│ ├── strategy-service/ # Trading strategies & backtesting
│ └── dashboard/ # Angular UI (port 4200)
├── libs/ # Shared libraries
│ ├── logger/ # Centralized logging w/ Loki
│ ├── config/ # Configuration management
│ ├── event-bus/ # Event system
│ ├── mongodb-client/ # MongoDB operations
│ ├── postgres-client/ # PostgreSQL operations
│ ├── questdb-client/ # Time-series data
│ ├── http/ # HTTP client w/ proxy support
│ ├── cache/ # Caching layer
│ └── utils/ # Common utilities
└── database/ # Database configurations
├── mongodb/init/
└── postgres/init/
```
## Technology Stack
| Component | Technology | Purpose |
|-----------|------------|---------|
| **Runtime** | Bun | Fast JavaScript runtime |
| **Language** | TypeScript | Type-safe development |
| **Databases** | PostgreSQL, MongoDB, QuestDB | Multi-database architecture |
| **Caching** | Dragonfly (Redis) | Event bus & caching |
| **Frontend** | Angular 18 | Modern reactive UI |
| **Monitoring** | Prometheus, Grafana, Loki | Observability stack |
## Quick Start
```bash
# Install dependencies
bun install
# Start infrastructure
bun run infra:up
# Start services
bun run dev
# Access dashboard
# http://localhost:4200
```
## Key Features
- **Real-time Trading**: Live market data & order execution
- **Multi-Database**: PostgreSQL, MongoDB, QuestDB for different data types
- **Event-Driven**: Asynchronous communication via Dragonfly
- **Monitoring**: Full observability with metrics, logs, and tracing
- **Modular**: Shared libraries for common functionality
- **Type-Safe**: Full TypeScript coverage
│ ├── processing-service/ # Combined processing & indicators
│ │ ├── src/
│ │ │ ├── indicators/ # Technical indicators (uses @stock-bot/utils)
│ │ │ ├── processors/ # Data processing pipeline
│ │ │ ├── vectorized/ # Vectorized calculations
│ │ │ ├── services/
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── strategy-service/ # Combined strategy & backtesting
│ │ ├── src/
│ │ │ ├── strategies/ # Strategy implementations
│ │ │ ├── backtesting/ # Multi-mode backtesting engine
│ │ │ │ ├── modes/ # Backtesting modes
│ │ │ │ │ ├── live-mode.ts # Live trading mode
│ │ │ │ │ ├── event-mode.ts # Event-driven backtest
│ │ │ │ │ └── vector-mode.ts # Vectorized backtest
│ │ │ │ ├── engines/ # Execution engines
│ │ │ │ │ ├── event-engine.ts # Event-based simulation
│ │ │ │ │ ├── vector-engine.ts # Vectorized calculations
│ │ │ │ │ └── hybrid-engine.ts # Combined validation
│ │ │ │ ├── simulator.ts # Market simulator
│ │ │ │ ├── runner.ts # Backtest orchestrator
│ │ │ │ └── metrics.ts # Performance analysis
│ │ │ ├── live/ # Live strategy execution
│ │ │ ├── framework/ # Strategy framework
│ │ │ │ ├── base-strategy.ts
│ │ │ │ ├── execution-mode.ts
│ │ │ │ └── mode-factory.ts
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── execution-service/ # Combined order execution & simulation
│ │ ├── src/
│ │ │ ├── brokers/ # Live broker adapters
│ │ │ ├── simulation/ # Simulated execution
│ │ │ ├── unified/ # Unified execution interface
│ │ │ │ ├── executor.ts # Abstract executor
│ │ │ │ ├── live-executor.ts
│ │ │ │ ├── sim-executor.ts
│ │ │ │ └── vector-executor.ts
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ ├── portfolio-service/ # Combined portfolio & risk management
│ │ ├── src/
│ │ │ ├── portfolio/ # Portfolio tracking
│ │ │ ├── risk/ # Risk management (uses @stock-bot/utils)
│ │ │ ├── positions/ # Position management
│ │ │ ├── performance/ # Performance tracking
│ │ │ └── index.ts
│ │ └── package.json
│ │
│ └── dashboard/ # Combined API & reporting
│ ├── src/
│ │ ├── api/ # REST API
│ │ ├── web/ # Web interface (Angular)
│ │ ├── reports/ # Report generation
│ │ ├── websockets/ # Real-time updates
│ │ └── index.ts
│ └── package.json
├── libs/ # ✅ Your existing shared libraries
│ ├── config/ # ✅ Environment configuration
│ ├── http/ # ✅ HTTP utilities
│ ├── logger/ # ✅ Loki-integrated logging
│ ├── mongodb-client/ # ✅ MongoDB operations
│ ├── postgres-client/ # ✅ PostgreSQL operations
│ ├── questdb-client/ # ✅ Time-series data
│ ├── types/ # ✅ Shared TypeScript types
│ ├── utils/ # ✅ Calculations & utilities
│ ├── event-bus/ # 🆕 Dragonfly event system
│ ├── strategy-engine/ # 🆕 Strategy framework
│ ├── vector-engine/ # 🆕 Vectorized calculations
│ └── data-frame/ # 🆕 DataFrame operations
```
## Multi-Mode Backtesting Architecture
### 1. Execution Mode Framework
```typescript
export abstract class ExecutionMode {
protected logger = createLogger(this.constructor.name);
protected config = new ServiceConfig();
abstract name: string;
abstract executeOrder(order: Order): Promise<OrderResult>;
abstract getCurrentTime(): Date;
abstract getMarketData(symbol: string): Promise<MarketData>;
abstract publishEvent(event: string, data: any): Promise<void>;
}
export enum BacktestMode {
LIVE = 'live',
EVENT_DRIVEN = 'event-driven',
VECTORIZED = 'vectorized',
HYBRID = 'hybrid'
}
```
### 2. Live Trading Mode
```typescript
export class LiveMode extends ExecutionMode {
name = 'live';
private broker = new BrokerClient(this.config.getBrokerConfig());
private eventBus = new EventBus();
async executeOrder(order: Order): Promise<OrderResult> {
this.logger.info('Executing live order', { orderId: order.id });
// Execute via real broker
const result = await this.broker.placeOrder(order);
// Publish to event bus
await this.eventBus.publish('order.executed', result);
return result;
}
getCurrentTime(): Date {
return new Date(); // Real time
}
async getMarketData(symbol: string): Promise<MarketData> {
// Get live market data
return await this.marketDataService.getLiveData(symbol);
}
async publishEvent(event: string, data: any): Promise<void> {
await this.eventBus.publish(event, data);
}
}
```
### 3. Event-Driven Backtesting Mode
```typescript
export class EventBacktestMode extends ExecutionMode {
name = 'event-driven';
private simulator = new MarketSimulator();
private eventBus = new InMemoryEventBus(); // In-memory for simulation
private simulationTime: Date;
private historicalData: Map<string, MarketData[]>;
constructor(private config: BacktestConfig) {
super();
this.simulationTime = config.startDate;
}
async executeOrder(order: Order): Promise<OrderResult> {
this.logger.debug('Simulating order execution', {
orderId: order.id,
simulationTime: this.simulationTime
});
// Realistic order simulation with slippage, fees
const result = await this.simulator.executeOrder(order, {
currentTime: this.simulationTime,
marketData: await this.getMarketData(order.symbol),
slippageModel: this.config.slippageModel,
commissionModel: this.config.commissionModel
});
// Publish to simulation event bus
await this.eventBus.publish('order.executed', result);
return result;
}
getCurrentTime(): Date {
return this.simulationTime;
}
async getMarketData(symbol: string): Promise<MarketData> {
const data = this.historicalData.get(symbol) || [];
return data.find(d => d.timestamp <= this.simulationTime) || null;
}
async publishEvent(event: string, data: any): Promise<void> {
await this.eventBus.publish(event, data);
}
// Progress simulation time
advanceTime(newTime: Date): void {
this.simulationTime = newTime;
}
}
```
### 4. Vectorized Backtesting Mode
```typescript
export class VectorBacktestMode extends ExecutionMode {
name = 'vectorized';
private dataFrame: DataFrame;
private currentIndex: number = 0;
constructor(private config: VectorBacktestConfig) {
super();
this.dataFrame = new DataFrame(config.historicalData);
}
// Vectorized execution - processes entire dataset at once
async executeVectorizedBacktest(strategy: VectorizedStrategy): Promise<BacktestResult> {
const startTime = Date.now();
this.logger.info('Starting vectorized backtest', {
strategy: strategy.name,
dataPoints: this.dataFrame.length
});
// Generate all signals at once using your utils library
const signals = this.generateVectorizedSignals(strategy);
// Calculate performance metrics vectorized
const performance = this.calculateVectorizedPerformance(signals);
// Apply trading costs if specified
if (this.config.tradingCosts) {
this.applyTradingCosts(performance, signals);
}
const executionTime = Date.now() - startTime;
this.logger.info('Vectorized backtest completed', {
executionTime,
totalReturn: performance.totalReturn,
sharpeRatio: performance.sharpeRatio
});
return {
mode: 'vectorized',
strategy: strategy.name,
performance,
executionTime,
signals
};
}
private generateVectorizedSignals(strategy: VectorizedStrategy): DataFrame {
const prices = this.dataFrame.get('close');
// Use your existing technical indicators from @stock-bot/utils
const indicators = {
sma20: sma(prices, 20),
sma50: sma(prices, 50),
rsi: rsi(prices, 14),
macd: macd(prices)
};
// Generate position signals vectorized
const positions = strategy.generatePositions(this.dataFrame, indicators);
return new DataFrame({
...this.dataFrame.toObject(),
...indicators,
positions
});
}
private calculateVectorizedPerformance(signals: DataFrame): PerformanceMetrics {
const prices = signals.get('close');
const positions = signals.get('positions');
// Calculate returns vectorized
const returns = prices.slice(1).map((price, i) =>
(price - prices[i]) / prices[i]
);
// Strategy returns = position[t-1] * market_return[t]
const strategyReturns = returns.map((ret, i) =>
(positions[i] || 0) * ret
);
// Use your existing performance calculation utilities
return {
totalReturn: calculateTotalReturn(strategyReturns),
sharpeRatio: calculateSharpeRatio(strategyReturns),
maxDrawdown: calculateMaxDrawdown(strategyReturns),
volatility: calculateVolatility(strategyReturns),
winRate: calculateWinRate(strategyReturns)
};
}
// Standard interface methods (not used in vectorized mode)
async executeOrder(order: Order): Promise<OrderResult> {
throw new Error('Use executeVectorizedBacktest for vectorized mode');
}
getCurrentTime(): Date {
return this.dataFrame.getTimestamp(this.currentIndex);
}
async getMarketData(symbol: string): Promise<MarketData> {
return this.dataFrame.getRow(this.currentIndex);
}
async publishEvent(event: string, data: any): Promise<void> {
// No-op for vectorized mode
}
}
```
### 5. Hybrid Validation Mode
```typescript
export class HybridBacktestMode extends ExecutionMode {
name = 'hybrid';
private eventMode: EventBacktestMode;
private vectorMode: VectorBacktestMode;
constructor(config: BacktestConfig) {
super();
this.eventMode = new EventBacktestMode(config);
this.vectorMode = new VectorBacktestMode(config);
}
async validateStrategy(
strategy: BaseStrategy,
tolerance: number = 0.001
): Promise<ValidationResult> {
this.logger.info('Starting hybrid validation', {
strategy: strategy.name,
tolerance
});
// Run vectorized backtest (fast)
const vectorResult = await this.vectorMode.executeVectorizedBacktest(
strategy as VectorizedStrategy
);
// Run event-driven backtest (realistic)
const eventResult = await this.runEventBacktest(strategy);
// Compare results
const performanceDiff = Math.abs(
vectorResult.performance.totalReturn -
eventResult.performance.totalReturn
);
const isValid = performanceDiff < tolerance;
this.logger.info('Hybrid validation completed', {
isValid,
performanceDifference: performanceDiff,
recommendation: isValid ? 'vectorized' : 'event-driven'
});
return {
isValid,
performanceDifference: performanceDiff,
vectorizedResult: vectorResult,
eventResult,
recommendation: isValid ?
'Vectorized results are reliable for this strategy' :
'Use event-driven backtesting for accurate results'
};
}
// Standard interface methods delegate to event mode
async executeOrder(order: Order): Promise<OrderResult> {
return await this.eventMode.executeOrder(order);
}
getCurrentTime(): Date {
return this.eventMode.getCurrentTime();
}
async getMarketData(symbol: string): Promise<MarketData> {
return await this.eventMode.getMarketData(symbol);
}
async publishEvent(event: string, data: any): Promise<void> {
await this.eventMode.publishEvent(event, data);
}
}
```
## Unified Strategy Implementation
### Base Strategy Framework
```typescript
export abstract class BaseStrategy {
protected mode: ExecutionMode;
protected logger = createLogger(this.constructor.name);
abstract name: string;
abstract parameters: Record<string, any>;
constructor(mode: ExecutionMode) {
this.mode = mode;
}
// Works identically across all modes
abstract onPriceUpdate(data: PriceData): Promise<void>;
abstract onIndicatorUpdate(data: IndicatorData): Promise<void>;
protected async emitSignal(signal: TradeSignal): Promise<void> {
this.logger.debug('Emitting trade signal', { signal });
// Mode handles whether this is live, simulated, or vectorized
const order = this.createOrder(signal);
const result = await this.mode.executeOrder(order);
await this.mode.publishEvent('trade.executed', {
signal,
order,
result,
timestamp: this.mode.getCurrentTime()
});
}
private createOrder(signal: TradeSignal): Order {
return {
id: generateId(),
symbol: signal.symbol,
side: signal.action,
quantity: signal.quantity,
type: 'market',
timestamp: this.mode.getCurrentTime()
};
}
}
// Vectorized strategy interface
export interface VectorizedStrategy {
name: string;
parameters: Record<string, any>;
generatePositions(data: DataFrame, indicators: any): number[];
}
```
### Example Strategy Implementation
```typescript
export class SMAStrategy extends BaseStrategy implements VectorizedStrategy {
name = 'SMA-Crossover';
parameters = { fastPeriod: 10, slowPeriod: 20 };
private fastSMA: number[] = [];
private slowSMA: number[] = [];
async onPriceUpdate(data: PriceData): Promise<void> {
// Same logic for live, event-driven, and hybrid modes
this.fastSMA.push(data.close);
this.slowSMA.push(data.close);
if (this.fastSMA.length > this.parameters.fastPeriod) {
this.fastSMA.shift();
}
if (this.slowSMA.length > this.parameters.slowPeriod) {
this.slowSMA.shift();
}
if (this.fastSMA.length === this.parameters.fastPeriod &&
this.slowSMA.length === this.parameters.slowPeriod) {
const fastAvg = sma(this.fastSMA, this.parameters.fastPeriod)[0];
const slowAvg = sma(this.slowSMA, this.parameters.slowPeriod)[0];
if (fastAvg > slowAvg) {
await this.emitSignal({
symbol: data.symbol,
action: 'BUY',
quantity: 100,
confidence: 0.8
});
} else if (fastAvg < slowAvg) {
await this.emitSignal({
symbol: data.symbol,
action: 'SELL',
quantity: 100,
confidence: 0.8
});
}
}
}
async onIndicatorUpdate(data: IndicatorData): Promise<void> {
// Handle pre-calculated indicators
}
// Vectorized implementation for fast backtesting
generatePositions(data: DataFrame, indicators: any): number[] {
const { sma20: fastSMA, sma50: slowSMA } = indicators;
return fastSMA.map((fast, i) => {
const slow = slowSMA[i];
if (isNaN(fast) || isNaN(slow)) return 0;
// Long when fast > slow, short when fast < slow
return fast > slow ? 1 : (fast < slow ? -1 : 0);
});
}
}
```
## Mode Factory and Service Integration
### Mode Factory
```typescript
export class ModeFactory {
static create(mode: BacktestMode, config: any): ExecutionMode {
switch (mode) {
case BacktestMode.LIVE:
return new LiveMode();
case BacktestMode.EVENT_DRIVEN:
return new EventBacktestMode(config);
case BacktestMode.VECTORIZED:
return new VectorBacktestMode(config);
case BacktestMode.HYBRID:
return new HybridBacktestMode(config);
default:
throw new Error(`Unknown mode: ${mode}`);
}
}
}
```
### Strategy Service Integration
```typescript
export class StrategyService {
private logger = createLogger('strategy-service');
async runStrategy(
strategyName: string,
mode: BacktestMode,
config: any
): Promise<any> {
const executionMode = ModeFactory.create(mode, config);
const strategy = await this.loadStrategy(strategyName, executionMode);
this.logger.info('Starting strategy execution', {
strategy: strategyName,
mode,
config
});
switch (mode) {
case BacktestMode.LIVE:
return await this.runLiveStrategy(strategy);
case BacktestMode.EVENT_DRIVEN:
return await this.runEventBacktest(strategy, config);
case BacktestMode.VECTORIZED:
return await (executionMode as VectorBacktestMode)
.executeVectorizedBacktest(strategy as VectorizedStrategy);
case BacktestMode.HYBRID:
return await (executionMode as HybridBacktestMode)
.validateStrategy(strategy, config.tolerance);
default:
throw new Error(`Unsupported mode: ${mode}`);
}
}
async optimizeStrategy(
strategyName: string,
parameterGrid: Record<string, any[]>,
config: BacktestConfig
): Promise<OptimizationResult[]> {
const results: OptimizationResult[] = [];
const combinations = this.generateParameterCombinations(parameterGrid);
this.logger.info('Starting parameter optimization', {
strategy: strategyName,
combinations: combinations.length
});
// Use vectorized mode for fast parameter optimization
const vectorMode = new VectorBacktestMode(config);
// Can be parallelized
await Promise.all(
combinations.map(async (params) => {
const strategy = await this.loadStrategy(strategyName, vectorMode, params);
const result = await vectorMode.executeVectorizedBacktest(
strategy as VectorizedStrategy
);
results.push({
parameters: params,
performance: result.performance,
executionTime: result.executionTime
});
})
);
// Sort by Sharpe ratio
return results.sort((a, b) =>
b.performance.sharpeRatio - a.performance.sharpeRatio
);
}
}
```
## Service Configuration
### Environment-Based Mode Selection
```typescript
export class ServiceConfig {
getTradingConfig(): TradingConfig {
return {
mode: (process.env.TRADING_MODE as BacktestMode) || BacktestMode.LIVE,
brokerConfig: {
apiKey: process.env.BROKER_API_KEY,
sandbox: process.env.BROKER_SANDBOX === 'true'
},
backtestConfig: {
startDate: new Date(process.env.BACKTEST_START_DATE || '2023-01-01'),
endDate: new Date(process.env.BACKTEST_END_DATE || '2024-01-01'),
initialCapital: parseFloat(process.env.INITIAL_CAPITAL || '100000'),
slippageModel: process.env.SLIPPAGE_MODEL || 'linear',
commissionModel: process.env.COMMISSION_MODEL || 'fixed'
}
};
}
}
```
### CLI Interface
```typescript
// CLI for running different modes
import { Command } from 'commander';
const program = new Command();
program
.name('stock-bot')
.description('Stock Trading Bot with Multi-Mode Backtesting');
program
.command('live')
.description('Run live trading')
.option('-s, --strategy <strategy>', 'Strategy to run')
.action(async (options) => {
const strategyService = new StrategyService();
await strategyService.runStrategy(
options.strategy,
BacktestMode.LIVE,
{}
);
});
program
.command('backtest')
.description('Run backtesting')
.option('-s, --strategy <strategy>', 'Strategy to test')
.option('-m, --mode <mode>', 'Backtest mode (event|vector|hybrid)', 'event')
.option('-f, --from <date>', 'Start date')
.option('-t, --to <date>', 'End date')
.action(async (options) => {
const strategyService = new StrategyService();
await strategyService.runStrategy(
options.strategy,
options.mode as BacktestMode,
{
startDate: new Date(options.from),
endDate: new Date(options.to)
}
);
});
program
.command('optimize')
.description('Optimize strategy parameters')
.option('-s, --strategy <strategy>', 'Strategy to optimize')
.option('-p, --params <params>', 'Parameter grid JSON')
.action(async (options) => {
const strategyService = new StrategyService();
const paramGrid = JSON.parse(options.params);
await strategyService.optimizeStrategy(
options.strategy,
paramGrid,
{}
);
});
program.parse();
```
## Performance Comparison
### Execution Speed by Mode
| Mode | Data Points/Second | Memory Usage | Use Case |
|------|-------------------|--------------|----------|
| **Live** | Real-time | Low | Production trading |
| **Event-Driven** | ~1,000 | Medium | Realistic validation |
| **Vectorized** | ~100,000+ | High | Parameter optimization |
| **Hybrid** | Combined | Medium | Strategy validation |
### When to Use Each Mode
- **Live Mode**: Production trading with real money
- **Event-Driven**: Final strategy validation, complex order logic
- **Vectorized**: Initial development, parameter optimization, quick testing
- **Hybrid**: Validating vectorized results against realistic simulation
## Integration with Your Existing Libraries
This architecture leverages all your existing infrastructure:
- **@stock-bot/config**: Environment management
- **@stock-bot/logger**: Comprehensive logging with Loki
- **@stock-bot/utils**: All technical indicators and calculations
- **@stock-bot/questdb-client**: Time-series data storage
- **@stock-bot/postgres-client**: Transactional data
- **@stock-bot/mongodb-client**: Configuration storage
## Key Benefits
1. **Unified Codebase**: Same strategy logic across all modes
2. **Performance Flexibility**: Choose speed vs accuracy based on needs
3. **Validation Pipeline**: Hybrid mode ensures vectorized results are accurate
4. **Production Ready**: Live mode for actual trading
5. **Development Friendly**: Fast iteration with vectorized backtesting
This simplified architecture reduces complexity while providing comprehensive backtesting capabilities that scale from rapid prototyping to production trading.

View file

@ -1,17 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View file

@ -1,42 +0,0 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View file

@ -1,5 +0,0 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View file

@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View file

@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View file

@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View file

@ -1,59 +0,0 @@
# TradingDashboard
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View file

@ -1,91 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
},
"newProjectRoot": "projects",
"projects": {
"trading-dashboard": {
"projectType": "application", "schematics": {
"@schematics/angular:component": {
"style": "css"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": { "build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "css",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": false
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "trading-dashboard:build:production"
},
"development": {
"buildTarget": "trading-dashboard:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
}, "test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "css",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
}
}
}
}
}

View file

@ -1,44 +0,0 @@
{
"name": "trading-dashboard",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"devvvv": "ng serve --port 5173 --host 0.0.0.0",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^20.0.0",
"@angular/cdk": "^20.0.1",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/material": "^20.0.1",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"rxjs": "~7.8.2",
"tslib": "^2.8.1",
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@tailwindcss/postcss": "^4.1.8",
"@types/jasmine": "~5.1.8",
"autoprefixer": "^10.4.21",
"jasmine-core": "~5.7.1",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.4",
"tailwindcss": "^4.1.8",
"typescript": "~5.8.3"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,16 +0,0 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
provideAnimationsAsync()
]
};

View file

@ -1,174 +0,0 @@
/* Custom Angular Material integration styles */
/* Sidenav styles */
.mat-sidenav-container {
background-color: transparent;
}
.mat-sidenav {
border-radius: 0;
width: 16rem;
background-color: white !important;
border-right: 1px solid #e5e7eb !important;
}
/* Toolbar styles */
.mat-toolbar {
background-color: white;
color: #374151;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
/* Button styles */
.mat-mdc-button.nav-button {
width: 100%;
text-align: left;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
background-color: transparent;
transition: background-color 0.15s ease-in-out;
}
.mat-mdc-button.nav-button:hover {
background-color: #f3f4f6;
}
.mat-mdc-button.nav-button.bg-blue-50 {
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}
/* Card styles */
.mat-mdc-card {
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
background-color: white !important;
}
/* Tab styles */
.mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #e5e7eb;
}
.mat-mdc-tab-label {
color: #6b7280;
}
.mat-mdc-tab-label:hover {
color: #111827;
}
.mat-mdc-tab-label-active {
color: #2563eb;
font-weight: 500;
}
/* Chip styles for status indicators */
.mat-mdc-chip-set .mat-mdc-chip {
background-color: white;
border: 1px solid #e5e7eb;
}
.chip-green {
background-color: #dcfce7 !important;
color: #166534 !important;
border: 1px solid #bbf7d0 !important;
}
.chip-blue {
background-color: #dbeafe !important;
color: #1e40af !important;
border: 1px solid #bfdbfe !important;
}
.status-chip-active {
background-color: #dcfce7;
color: #166534;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
}
.status-chip-medium {
background-color: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
/* Table styles */
.mat-mdc-table {
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid #f3f4f6;
background-color: white;
}
.mat-mdc-header-row {
background-color: #f9fafb;
}
.mat-mdc-header-cell {
font-weight: 500;
color: #374151;
font-size: 0.875rem;
}
.mat-mdc-cell {
color: #111827;
font-size: 0.875rem;
padding: 1rem 0;
}
.mat-mdc-row:hover {
background-color: #f9fafb;
transition: background-color 0.15s ease;
}
/* Custom utility classes for the dashboard */
.portfolio-card {
background-color: white !important;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
padding: 1.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-label {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.metric-change-positive {
color: #16a34a;
font-weight: 500;
}
.metric-change-negative {
color: #dc2626;
font-weight: 500;
}
/* Responsive styles */
@media (max-width: 768px) {
.mat-sidenav {
width: 100%;
}
.hide-mobile {
display: none;
}
}

View file

@ -1,67 +0,0 @@
<!-- Trading Dashboard App -->
<div class="app-layout">
<!-- Sidebar -->
<app-sidebar [opened]="sidenavOpened()" (navigationItemClick)="onNavigationClick($event)"></app-sidebar>
<!-- Main Content Area -->
<div class="main-content" [class.main-content-closed]="!sidenavOpened()">
<!-- Top Navigation Bar -->
<mat-toolbar class="top-toolbar">
<button mat-icon-button (click)="toggleSidenav()" class="mr-2">
<mat-icon>menu</mat-icon>
</button> <span class="text-lg font-semibold text-gray-800">{{ title }}</span>
<span class="spacer"></span>
<app-notifications></app-notifications>
<button mat-icon-button>
<mat-icon>account_circle</mat-icon>
</button>
</mat-toolbar>
<!-- Page Content -->
<div class="page-content">
<router-outlet></router-outlet>
</div>
</div>
</div>
<style>
.app-layout {
display: flex;
height: 100vh;
background-color: #f9fafb;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 256px; /* Width of sidebar */
transition: margin-left 0.3s ease;
}
.main-content-closed {
margin-left: 0;
}
.top-toolbar {
background-color: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.spacer {
flex: 1;
}
.page-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
@media (max-width: 768px) {
.main-content {
margin-left: 0;
}
}
</style>

View file

@ -1,18 +0,0 @@
import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { MarketDataComponent } from './pages/market-data/market-data.component';
import { PortfolioComponent } from './pages/portfolio/portfolio.component';
import { StrategiesComponent } from './pages/strategies/strategies.component';
import { RiskManagementComponent } from './pages/risk-management/risk-management.component';
import { SettingsComponent } from './pages/settings/settings.component';
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'market-data', component: MarketDataComponent },
{ path: 'portfolio', component: PortfolioComponent },
{ path: 'strategies', component: StrategiesComponent },
{ path: 'risk-management', component: RiskManagementComponent },
{ path: 'settings', component: SettingsComponent },
{ path: '**', redirectTo: '/dashboard' }
];

View file

@ -1,25 +0,0 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, trading-dashboard');
});
});

View file

@ -1,40 +0,0 @@
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatChipsModule } from '@angular/material/chips';
import { SidebarComponent } from './components/sidebar/sidebar.component';
import { NotificationsComponent } from './components/notifications/notifications';
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatChipsModule,
SidebarComponent,
NotificationsComponent
],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected title = 'Trading Dashboard';
protected sidenavOpened = signal(true);
toggleSidenav() {
this.sidenavOpened.set(!this.sidenavOpened());
}
onNavigationClick(route: string) {
// Handle navigation if needed
console.log('Navigating to:', route);
}
}

View file

@ -1,45 +0,0 @@
::ng-deep .notification-menu {
width: 380px;
max-width: 90vw;
}
.notification-header {
padding: 12px 16px !important;
height: auto !important;
line-height: normal !important;
}
.notification-empty {
padding: 16px !important;
height: auto !important;
line-height: normal !important;
}
.notification-item {
padding: 12px 16px !important;
height: auto !important;
line-height: normal !important;
white-space: normal !important;
border-left: 3px solid transparent;
transition: all 0.2s ease;
}
.notification-item:hover {
background-color: #f5f5f5;
}
.notification-item.unread {
background-color: #f0f9ff;
border-left-color: #0ea5e9;
}
.notification-item.unread .font-medium {
font-weight: 600;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View file

@ -1,75 +0,0 @@
<button
mat-icon-button
[matMenuTriggerFor]="notificationMenu"
[matBadge]="unreadCount"
[matBadgeHidden]="unreadCount === 0"
matBadgeColor="warn"
matBadgeSize="small">
<mat-icon>notifications</mat-icon>
</button>
<mat-menu #notificationMenu="matMenu" class="notification-menu">
<div mat-menu-item disabled class="notification-header">
<div class="flex items-center justify-between w-full px-2">
<span class="font-semibold">Notifications</span>
@if (notifications.length > 0) {
<div class="flex gap-2">
<button mat-button (click)="markAllAsRead()" class="text-xs">
Mark all read
</button>
<button mat-button (click)="clearAll()" class="text-xs">
Clear all
</button>
</div>
}
</div>
</div>
<mat-divider></mat-divider>
@if (notifications.length === 0) {
<div mat-menu-item disabled class="notification-empty">
<div class="text-center py-4 text-gray-500">
<mat-icon class="text-2xl">notifications_none</mat-icon>
<p class="mt-1 text-sm">No notifications</p>
</div>
</div>
} @else {
@for (notification of notifications.slice(0, 5); track notification.id) {
<div
mat-menu-item
class="notification-item"
[class.unread]="!notification.read"
(click)="markAsRead(notification)">
<div class="flex items-start gap-3 w-full">
<mat-icon [class]="getNotificationColor(notification.type)" class="mt-1">
{{ getNotificationIcon(notification.type) }}
</mat-icon>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="font-medium text-sm truncate">{{ notification.title }}</p>
<button
mat-icon-button
(click)="clearNotification(notification); $event.stopPropagation()"
class="text-gray-400 hover:text-gray-600 ml-2">
<mat-icon class="text-lg">close</mat-icon>
</button>
</div>
<p class="text-gray-600 text-xs mt-1 line-clamp-2">{{ notification.message }}</p>
<p class="text-gray-400 text-xs mt-1">{{ formatTime(notification.timestamp) }}</p>
</div>
</div>
</div>
@if (!$last) {
<mat-divider></mat-divider>
}
}
@if (notifications.length > 5) {
<mat-divider></mat-divider>
<div mat-menu-item disabled class="text-center text-sm text-gray-500">
{{ notifications.length - 5 }} more notifications...
</div>
}
}
</mat-menu>

View file

@ -1,86 +0,0 @@
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatBadgeModule } from '@angular/material/badge';
import { MatMenuModule } from '@angular/material/menu';
import { MatListModule } from '@angular/material/list';
import { MatDividerModule } from '@angular/material/divider';
import { NotificationService, Notification } from '../../services/notification.service';
@Component({
selector: 'app-notifications',
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatBadgeModule,
MatMenuModule,
MatListModule,
MatDividerModule
],
templateUrl: './notifications.html',
styleUrl: './notifications.css'
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
get notifications() {
return this.notificationService.notifications();
}
get unreadCount() {
return this.notificationService.unreadCount();
}
markAsRead(notification: Notification) {
this.notificationService.markAsRead(notification.id);
}
markAllAsRead() {
this.notificationService.markAllAsRead();
}
clearNotification(notification: Notification) {
this.notificationService.clearNotification(notification.id);
}
clearAll() {
this.notificationService.clearAllNotifications();
}
getNotificationIcon(type: string): string {
switch (type) {
case 'error': return 'error';
case 'warning': return 'warning';
case 'success': return 'check_circle';
case 'info':
default: return 'info';
}
}
getNotificationColor(type: string): string {
switch (type) {
case 'error': return 'text-red-600';
case 'warning': return 'text-yellow-600';
case 'success': return 'text-green-600';
case 'info':
default: return 'text-blue-600';
}
}
formatTime(timestamp: Date): string {
const now = new Date();
const diff = now.getTime() - timestamp.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
}

View file

@ -1,38 +0,0 @@
/* Sidebar specific styles */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 16rem; /* 256px */
height: 100vh;
background-color: white;
border-right: 1px solid #e5e7eb;
transform: translateX(0);
transition: transform 0.3s ease-in-out;
z-index: 1000;
overflow-y: auto;
}
.sidebar-closed {
transform: translateX(-100%);
}
.nav-button {
width: 100%;
text-align: left;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
background-color: transparent;
transition: background-color 0.15s ease-in-out;
}
.nav-button:hover {
background-color: #f3f4f6;
}
.nav-button.bg-blue-50 {
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}

View file

@ -1,30 +0,0 @@
<!-- Sidebar Navigation -->
<aside class="sidebar" [class.sidebar-closed]="!opened()">
<!-- Logo/Brand -->
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">
📈 Trading Bot
</h2>
<p class="text-sm text-gray-600 mt-1">
Real-time Dashboard
</p>
</div>
<!-- Navigation Menu -->
<nav class="p-6">
<div class="space-y-2">
@for (item of navigationItems; track item.route) {
<button
mat-button
class="nav-button"
[class.bg-blue-50]="item.active"
[class.text-blue-700]="item.active"
[class.text-gray-600]="!item.active"
(click)="onNavigationClick(item.route)">
<mat-icon class="mr-3">{{ item.icon }}</mat-icon>
{{ item.label }}
</button>
}
</div>
</nav>
</aside>

View file

@ -1,61 +0,0 @@
import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
export interface NavigationItem {
label: string;
icon: string;
route: string;
active?: boolean;
}
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [
CommonModule,
MatSidenavModule,
MatButtonModule,
MatIconModule
],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css'
})
export class SidebarComponent {
opened = input<boolean>(true);
navigationItemClick = output<string>();
protected navigationItems: NavigationItem[] = [
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard', active: true },
{ label: 'Market Data', icon: 'trending_up', route: '/market-data' },
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
{ label: 'Strategies', icon: 'psychology', route: '/strategies' },
{ label: 'Risk Management', icon: 'security', route: '/risk-management' },
{ label: 'Settings', icon: 'settings', route: '/settings' }
];
constructor(private router: Router) {
// Listen to route changes to update active state
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.updateActiveRoute(event.urlAfterRedirects);
});
}
onNavigationClick(route: string) {
this.navigationItemClick.emit(route);
this.router.navigate([route]);
this.updateActiveRoute(route);
}
private updateActiveRoute(currentRoute: string) {
this.navigationItems.forEach(item => {
item.active = item.route === currentRoute;
});
}
}

View file

@ -1,48 +0,0 @@
/* Dashboard specific styles */
.portfolio-card {
background-color: white !important;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
padding: 1.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-label {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.metric-change-positive {
color: #16a34a;
font-weight: 500;
}
.metric-change-negative {
color: #dc2626;
font-weight: 500;
}
.status-chip-active {
background-color: #dcfce7;
color: #166534;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
}
.status-chip-medium {
background-color: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}

View file

@ -1,154 +0,0 @@
<!-- Portfolio Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Total Portfolio Value -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Portfolio Value</p>
<p class="metric-value text-gray-900">
${{ portfolioValue().toLocaleString('en-US', {minimumFractionDigits: 2}) }}
</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
</div>
</mat-card>
<!-- Day Change -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Day Change</p>
<p class="metric-value"
[class.metric-change-positive]="dayChange() > 0"
[class.metric-change-negative]="dayChange() < 0">
${{ dayChange().toLocaleString('en-US', {minimumFractionDigits: 2}) }}
</p>
<p class="text-sm font-medium"
[class.metric-change-positive]="dayChangePercent() > 0"
[class.metric-change-negative]="dayChangePercent() < 0">
{{ dayChangePercent() > 0 ? '+' : '' }}{{ dayChangePercent().toFixed(2) }}%
</p>
</div>
<mat-icon
class="text-3xl"
[class.metric-change-positive]="dayChange() > 0"
[class.metric-change-negative]="dayChange() < 0">
{{ dayChange() > 0 ? 'trending_up' : 'trending_down' }}
</mat-icon>
</div>
</mat-card>
<!-- Active Strategies -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Active Strategies</p>
<p class="metric-value text-gray-900">3</p>
<span class="status-chip-active">Running</span>
</div>
<mat-icon class="text-purple-600 text-3xl">psychology</mat-icon>
</div>
</mat-card>
<!-- Risk Level -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Risk Level</p>
<p class="metric-value text-green-600">Low</p>
<span class="status-chip-medium">Moderate</span>
</div>
<mat-icon class="text-green-600 text-3xl">security</mat-icon>
</div>
</mat-card>
</div>
<!-- Market Data Table -->
<mat-card class="p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Market Watchlist</h3>
<button mat-raised-button color="primary">
<mat-icon>refresh</mat-icon>
Refresh
</button>
</div>
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let stock" class="font-semibold text-gray-900">{{ stock.symbol }}</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Price</th>
<td mat-cell *matCellDef="let stock" class="text-right font-medium text-gray-900">
${{ stock.price.toFixed(2) }}
</td>
</ng-container>
<!-- Change Column -->
<ng-container matColumnDef="change">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.change > 0"
[class.text-red-600]="stock.change < 0">
{{ stock.change > 0 ? '+' : '' }}${{ stock.change.toFixed(2) }}
</td>
</ng-container>
<!-- Change Percent Column -->
<ng-container matColumnDef="changePercent">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change %</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.changePercent > 0"
[class.text-red-600]="stock.changePercent < 0">
{{ stock.changePercent > 0 ? '+' : '' }}{{ stock.changePercent.toFixed(2) }}%
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card>
<!-- Tabs for Additional Content -->
<mat-tab-group>
<mat-tab label="Chart">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">show_chart</mat-icon>
<p class="mb-4">Chart visualization will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Orders">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt_long</mat-icon>
<p class="mb-4">Order history and management will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Analytics">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">analytics</mat-icon>
<p class="mb-4">Advanced analytics and performance metrics will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>

View file

@ -1,44 +0,0 @@
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
export interface MarketDataItem {
symbol: string;
price: number;
change: number;
changePercent: number;
}
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatButtonModule,
MatIconModule,
MatTableModule
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css'
})
export class DashboardComponent {
// Mock data for the dashboard
protected marketData = signal<MarketDataItem[]>([
{ symbol: 'AAPL', price: 192.53, change: 2.41, changePercent: 1.27 },
{ symbol: 'GOOGL', price: 138.21, change: -1.82, changePercent: -1.30 },
{ symbol: 'MSFT', price: 378.85, change: 4.12, changePercent: 1.10 },
{ symbol: 'TSLA', price: 248.42, change: -3.21, changePercent: -1.28 },
]);
protected portfolioValue = signal(125420.50);
protected dayChange = signal(2341.20);
protected dayChangePercent = signal(1.90);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent'];
}

View file

@ -1 +0,0 @@
/* Market Data specific styles */

View file

@ -1,172 +0,0 @@
<div class="space-y-6"> <!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Market Data</h1>
<p class="text-gray-600 mt-1">Real-time market information and analytics</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Market Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Market Status</p>
<p class="text-lg font-semibold text-green-600">Open</p>
</div>
<mat-icon class="text-green-600 text-3xl">schedule</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Active Instruments</p>
<p class="text-lg font-semibold text-gray-900">{{ marketData().length }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">trending_up</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between"> <div>
<p class="text-sm text-gray-600">Last Update</p>
<p class="text-lg font-semibold text-gray-900">{{ currentTime() }}</p>
</div>
<mat-icon class="text-purple-600 text-3xl">access_time</mat-icon>
</div>
</mat-card>
</div>
<!-- Market Data Table -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Live Market Data</h3>
<div class="flex gap-2">
<button mat-button>
<mat-icon>filter_list</mat-icon>
Filter
</button>
<button mat-button>
<mat-icon>file_download</mat-icon>
Export
</button>
</div>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading market data...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let stock" class="font-semibold text-gray-900">{{ stock.symbol }}</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Price</th>
<td mat-cell *matCellDef="let stock" class="text-right font-medium text-gray-900">
${{ stock.price.toFixed(2) }}
</td>
</ng-container>
<!-- Change Column -->
<ng-container matColumnDef="change">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.change > 0"
[class.text-red-600]="stock.change < 0">
{{ stock.change > 0 ? '+' : '' }}${{ stock.change.toFixed(2) }}
</td>
</ng-container>
<!-- Change Percent Column -->
<ng-container matColumnDef="changePercent">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change %</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.changePercent > 0"
[class.text-red-600]="stock.changePercent < 0">
{{ stock.changePercent > 0 ? '+' : '' }}{{ stock.changePercent.toFixed(2) }}%
</td>
</ng-container>
<!-- Volume Column -->
<ng-container matColumnDef="volume">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Volume</th>
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
{{ stock.volume.toLocaleString() }}
</td>
</ng-container>
<!-- Market Cap Column -->
<ng-container matColumnDef="marketCap">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Cap</th>
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
${{ stock.marketCap }}
</td>
</ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
<!-- Market Analytics Tabs -->
<mat-tab-group>
<mat-tab label="Technical Analysis">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">bar_chart</mat-icon>
<p class="mb-4">Technical analysis charts and indicators will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Market Trends">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">timeline</mat-icon>
<p class="mb-4">Market trends and sector analysis will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="News & Events">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">article</mat-icon>
<p class="mb-4">Market news and economic events will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>
</div>

View file

@ -1,198 +0,0 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { ApiService } from '../../services/api.service';
import { WebSocketService } from '../../services/websocket.service';
import { interval, Subscription } from 'rxjs';
export interface ExtendedMarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
marketCap: string;
high52Week: number;
low52Week: number;
}
@Component({
selector: 'app-market-data',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatTabsModule,
MatProgressSpinnerModule,
MatSnackBarModule
],
templateUrl: './market-data.component.html',
styleUrl: './market-data.component.css'
})
export class MarketDataComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private webSocketService = inject(WebSocketService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected marketData = signal<ExtendedMarketData[]>([]);
protected currentTime = signal<string>(new Date().toLocaleTimeString());
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent', 'volume', 'marketCap'];
ngOnInit() {
// Update time every second
const timeSubscription = interval(1000).subscribe(() => {
this.currentTime.set(new Date().toLocaleTimeString());
});
this.subscriptions.push(timeSubscription);
// Load initial market data
this.loadMarketData();
// Subscribe to real-time market data updates
const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({
next: (update) => {
this.updateMarketData(update);
},
error: (err) => {
console.error('WebSocket market data error:', err);
}
});
this.subscriptions.push(wsSubscription);
// Fallback: Refresh market data every 30 seconds if WebSocket fails
const dataSubscription = interval(30000).subscribe(() => {
if (!this.webSocketService.isConnected()) {
this.loadMarketData();
}
});
this.subscriptions.push(dataSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadMarketData() {
this.apiService.getMarketData().subscribe({
next: (response) => {
// Convert MarketData to ExtendedMarketData with mock extended properties
const extendedData: ExtendedMarketData[] = response.data.map(item => ({
...item,
marketCap: this.getMockMarketCap(item.symbol),
high52Week: item.price * 1.3, // Mock 52-week high (30% above current)
low52Week: item.price * 0.7 // Mock 52-week low (30% below current)
}));
this.marketData.set(extendedData);
this.isLoading.set(false);
this.error.set(null);
},
error: (err) => {
console.error('Failed to load market data:', err);
this.error.set('Failed to load market data');
this.isLoading.set(false);
this.snackBar.open('Failed to load market data', 'Dismiss', { duration: 5000 });
// Use mock data as fallback
this.marketData.set(this.getMockData());
}
});
}
private getMockMarketCap(symbol: string): string {
const marketCaps: { [key: string]: string } = {
'AAPL': '2.98T',
'GOOGL': '1.78T',
'MSFT': '3.08T',
'TSLA': '789.2B',
'AMZN': '1.59T'
};
return marketCaps[symbol] || '1.00T';
}
private getMockData(): ExtendedMarketData[] {
return [
{
symbol: 'AAPL',
price: 192.53,
change: 2.41,
changePercent: 1.27,
volume: 45230000,
marketCap: '2.98T',
high52Week: 199.62,
low52Week: 164.08
},
{
symbol: 'GOOGL',
price: 2847.56,
change: -12.34,
changePercent: -0.43,
volume: 12450000,
marketCap: '1.78T',
high52Week: 3030.93,
low52Week: 2193.62
},
{
symbol: 'MSFT',
price: 415.26,
change: 8.73,
changePercent: 2.15,
volume: 23180000,
marketCap: '3.08T',
high52Week: 468.35,
low52Week: 309.45
},
{
symbol: 'TSLA',
price: 248.50,
change: -5.21,
changePercent: -2.05,
volume: 89760000,
marketCap: '789.2B',
high52Week: 299.29,
low52Week: 152.37
},
{
symbol: 'AMZN',
price: 152.74,
change: 3.18,
changePercent: 2.12,
volume: 34520000,
marketCap: '1.59T',
high52Week: 170.17,
low52Week: 118.35
}
];
}
refreshData() {
this.isLoading.set(true);
this.loadMarketData();
}
private updateMarketData(update: any) {
const currentData = this.marketData();
const updatedData = currentData.map(item => {
if (item.symbol === update.symbol) {
return {
...item,
price: update.price,
change: update.change,
changePercent: update.changePercent,
volume: update.volume
};
}
return item;
});
this.marketData.set(updatedData);
}
}

View file

@ -1 +0,0 @@
/* Portfolio specific styles */

View file

@ -1,203 +0,0 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Portfolio</h1>
<p class="text-gray-600 mt-1">Manage and monitor your investment portfolio</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Portfolio Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total Value</p>
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().totalValue.toLocaleString() }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Total P&L</p>
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().totalPnL)">
{{ portfolioSummary().totalPnL > 0 ? '+' : '' }}${{ portfolioSummary().totalPnL.toLocaleString() }}
({{ portfolioSummary().totalPnLPercent.toFixed(2) }}%)
</p>
</div>
<mat-icon class="text-green-600 text-3xl" *ngIf="portfolioSummary().totalPnL >= 0">trending_up</mat-icon>
<mat-icon class="text-red-600 text-3xl" *ngIf="portfolioSummary().totalPnL < 0">trending_down</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Day Change</p>
<p class="text-lg font-semibold" [class]="getPnLColor(portfolioSummary().dayChange)">
{{ portfolioSummary().dayChange > 0 ? '+' : '' }}${{ portfolioSummary().dayChange.toLocaleString() }}
({{ portfolioSummary().dayChangePercent.toFixed(2) }}%)
</p>
</div>
<mat-icon class="text-purple-600 text-3xl">today</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Cash Available</p>
<p class="text-lg font-semibold text-gray-900">${{ portfolioSummary().cash.toLocaleString() }}</p>
</div>
<mat-icon class="text-yellow-600 text-3xl">attach_money</mat-icon>
</div>
</mat-card>
</div>
<!-- Portfolio Tabs -->
<mat-tab-group>
<mat-tab label="Positions">
<div class="p-6">
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Current Positions</h3>
<div class="flex gap-2">
<button mat-button>
<mat-icon>add</mat-icon>
Add Position
</button>
<button mat-button>
<mat-icon>file_download</mat-icon>
Export
</button>
</div>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading portfolio...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else if (positions().length === 0) {
<div class="text-center py-8 text-gray-500">
<mat-icon class="text-4xl">account_balance_wallet</mat-icon>
<p class="mt-2">No positions found</p>
<button mat-button color="primary" class="mt-2">
<mat-icon>add</mat-icon>
Add Your First Position
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="positions()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let position" class="font-semibold text-gray-900">{{ position.symbol }}</td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Quantity</th>
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
{{ position.quantity.toLocaleString() }}
</td>
</ng-container>
<!-- Average Price Column -->
<ng-container matColumnDef="avgPrice">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Avg Price</th>
<td mat-cell *matCellDef="let position" class="text-right text-gray-600">
${{ position.avgPrice.toFixed(2) }}
</td>
</ng-container>
<!-- Current Price Column -->
<ng-container matColumnDef="currentPrice">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Current Price</th>
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
${{ position.currentPrice.toFixed(2) }}
</td>
</ng-container>
<!-- Market Value Column -->
<ng-container matColumnDef="marketValue">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Value</th>
<td mat-cell *matCellDef="let position" class="text-right font-medium text-gray-900">
${{ position.marketValue.toLocaleString() }}
</td>
</ng-container>
<!-- Unrealized P&L Column -->
<ng-container matColumnDef="unrealizedPnL">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Unrealized P&L</th>
<td mat-cell *matCellDef="let position"
class="text-right font-medium"
[class]="getPnLColor(position.unrealizedPnL)">
{{ position.unrealizedPnL > 0 ? '+' : '' }}${{ position.unrealizedPnL.toLocaleString() }}
({{ position.unrealizedPnLPercent.toFixed(2) }}%)
</td>
</ng-container>
<!-- Day Change Column -->
<ng-container matColumnDef="dayChange">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Day Change</th>
<td mat-cell *matCellDef="let position"
class="text-right font-medium"
[class]="getPnLColor(position.dayChange)">
{{ position.dayChange > 0 ? '+' : '' }}${{ position.dayChange.toFixed(2) }}
({{ position.dayChangePercent.toFixed(2) }}%)
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
</div>
</mat-tab>
<mat-tab label="Performance">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">trending_up</mat-icon>
<p class="mb-4">Performance charts and analytics will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Orders">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt</mat-icon>
<p class="mb-4">Order history and management will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>
</div>

View file

@ -1,159 +0,0 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { ApiService } from '../../services/api.service';
import { interval, Subscription } from 'rxjs';
export interface Position {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice: number;
marketValue: number;
unrealizedPnL: number;
unrealizedPnLPercent: number;
dayChange: number;
dayChangePercent: number;
}
export interface PortfolioSummary {
totalValue: number;
totalCost: number;
totalPnL: number;
totalPnLPercent: number;
dayChange: number;
dayChangePercent: number;
cash: number;
positionsCount: number;
}
@Component({
selector: 'app-portfolio',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTabsModule
],
templateUrl: './portfolio.component.html',
styleUrl: './portfolio.component.css'
})
export class PortfolioComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected portfolioSummary = signal<PortfolioSummary>({
totalValue: 0,
totalCost: 0,
totalPnL: 0,
totalPnLPercent: 0,
dayChange: 0,
dayChangePercent: 0,
cash: 0,
positionsCount: 0
});
protected positions = signal<Position[]>([]);
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns = ['symbol', 'quantity', 'avgPrice', 'currentPrice', 'marketValue', 'unrealizedPnL', 'dayChange'];
ngOnInit() {
this.loadPortfolioData();
// Refresh portfolio data every 30 seconds
const portfolioSubscription = interval(30000).subscribe(() => {
this.loadPortfolioData();
});
this.subscriptions.push(portfolioSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadPortfolioData() {
// Since we don't have a portfolio endpoint yet, let's create mock data
// In a real implementation, this would call this.apiService.getPortfolio()
setTimeout(() => {
const mockPositions: Position[] = [
{
symbol: 'AAPL',
quantity: 100,
avgPrice: 180.50,
currentPrice: 192.53,
marketValue: 19253,
unrealizedPnL: 1203,
unrealizedPnLPercent: 6.67,
dayChange: 241,
dayChangePercent: 1.27
},
{
symbol: 'MSFT',
quantity: 50,
avgPrice: 400.00,
currentPrice: 415.26,
marketValue: 20763,
unrealizedPnL: 763,
unrealizedPnLPercent: 3.82,
dayChange: 436.50,
dayChangePercent: 2.15
},
{
symbol: 'GOOGL',
quantity: 10,
avgPrice: 2900.00,
currentPrice: 2847.56,
marketValue: 28475.60,
unrealizedPnL: -524.40,
unrealizedPnLPercent: -1.81,
dayChange: -123.40,
dayChangePercent: -0.43
}
];
const summary: PortfolioSummary = {
totalValue: mockPositions.reduce((sum, pos) => sum + pos.marketValue, 0) + 25000, // + cash
totalCost: mockPositions.reduce((sum, pos) => sum + (pos.avgPrice * pos.quantity), 0),
totalPnL: mockPositions.reduce((sum, pos) => sum + pos.unrealizedPnL, 0),
totalPnLPercent: 0,
dayChange: mockPositions.reduce((sum, pos) => sum + pos.dayChange, 0),
dayChangePercent: 0,
cash: 25000,
positionsCount: mockPositions.length
};
summary.totalPnLPercent = (summary.totalPnL / summary.totalCost) * 100;
summary.dayChangePercent = (summary.dayChange / (summary.totalValue - summary.dayChange)) * 100;
this.positions.set(mockPositions);
this.portfolioSummary.set(summary);
this.isLoading.set(false);
this.error.set(null);
}, 1000);
}
refreshData() {
this.isLoading.set(true);
this.loadPortfolioData();
}
getPnLColor(value: number): string {
if (value > 0) return 'text-green-600';
if (value < 0) return 'text-red-600';
return 'text-gray-600';
}
}

View file

@ -1 +0,0 @@
/* Risk Management specific styles */

View file

@ -1,178 +0,0 @@
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Risk Management</h1>
<p class="text-gray-600 mt-1">Monitor and control trading risks and exposure</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Risk Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
@if (riskThresholds(); as thresholds) {
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Max Position Size</p>
<p class="text-lg font-semibold text-gray-900">${{ thresholds.maxPositionSize.toLocaleString() }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Max Daily Loss</p>
<p class="text-lg font-semibold text-red-600">${{ thresholds.maxDailyLoss.toLocaleString() }}</p>
</div>
<mat-icon class="text-red-600 text-3xl">trending_down</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Portfolio Risk Limit</p>
<p class="text-lg font-semibold text-yellow-600">{{ (thresholds.maxPortfolioRisk * 100).toFixed(1) }}%</p>
</div>
<mat-icon class="text-yellow-600 text-3xl">pie_chart</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Volatility Limit</p>
<p class="text-lg font-semibold text-purple-600">{{ (thresholds.volatilityLimit * 100).toFixed(1) }}%</p>
</div>
<mat-icon class="text-purple-600 text-3xl">show_chart</mat-icon>
</div>
</mat-card>
}
</div>
<!-- Risk Thresholds Configuration -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Risk Thresholds Configuration</h3>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading risk settings...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<form [formGroup]="thresholdsForm" (ngSubmit)="saveThresholds()">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<mat-form-field appearance="outline">
<mat-label>Max Position Size ($)</mat-label>
<input matInput type="number" formControlName="maxPositionSize" placeholder="100000">
<mat-icon matSuffix>attach_money</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Daily Loss ($)</mat-label>
<input matInput type="number" formControlName="maxDailyLoss" placeholder="5000">
<mat-icon matSuffix>trending_down</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Max Portfolio Risk (0-1)</mat-label>
<input matInput type="number" step="0.01" formControlName="maxPortfolioRisk" placeholder="0.1">
<mat-icon matSuffix>pie_chart</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Volatility Limit (0-1)</mat-label>
<input matInput type="number" step="0.01" formControlName="volatilityLimit" placeholder="0.3">
<mat-icon matSuffix>show_chart</mat-icon>
</mat-form-field>
</div>
<div class="flex justify-end mt-4">
<button mat-raised-button color="primary" type="submit" [disabled]="thresholdsForm.invalid || isSaving()">
@if (isSaving()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Save Thresholds
</button>
</div>
</form>
}
</mat-card>
<!-- Risk History Table -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Recent Risk Evaluations</h3>
</div>
@if (riskHistory().length === 0) {
<div class="text-center py-8 text-gray-500">
<mat-icon class="text-4xl">history</mat-icon>
<p class="mt-2">No risk evaluations found</p>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="riskHistory()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let risk" class="font-semibold text-gray-900">{{ risk.symbol }}</td>
</ng-container>
<!-- Position Value Column -->
<ng-container matColumnDef="positionValue">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Position Value</th>
<td mat-cell *matCellDef="let risk" class="text-right font-medium text-gray-900">
${{ risk.positionValue.toLocaleString() }}
</td>
</ng-container>
<!-- Risk Level Column -->
<ng-container matColumnDef="riskLevel">
<th mat-header-cell *matHeaderCellDef class="text-center font-medium text-gray-900">Risk Level</th>
<td mat-cell *matCellDef="let risk" class="text-center">
<span class="px-2 py-1 rounded-full text-sm font-medium" [class]="getRiskLevelColor(risk.riskLevel)">
{{ risk.riskLevel }}
</span>
</td>
</ng-container>
<!-- Violations Column -->
<ng-container matColumnDef="violations">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Violations</th>
<td mat-cell *matCellDef="let risk" class="text-gray-600">
@if (risk.violations.length > 0) {
<span class="text-red-600">{{ risk.violations.join(', ') }}</span>
} @else {
<span class="text-green-600">None</span>
}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
</div>

View file

@ -1,135 +0,0 @@
import { Component, signal, OnInit, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ApiService, RiskThresholds, RiskEvaluation } from '../../services/api.service';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-risk-management',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatFormFieldModule,
MatInputModule,
MatSnackBarModule,
MatProgressSpinnerModule,
ReactiveFormsModule
],
templateUrl: './risk-management.component.html',
styleUrl: './risk-management.component.css'
})
export class RiskManagementComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private snackBar = inject(MatSnackBar);
private fb = inject(FormBuilder);
private subscriptions: Subscription[] = [];
protected riskThresholds = signal<RiskThresholds | null>(null);
protected riskHistory = signal<RiskEvaluation[]>([]);
protected isLoading = signal<boolean>(true);
protected isSaving = signal<boolean>(false);
protected error = signal<string | null>(null);
protected thresholdsForm: FormGroup;
protected displayedColumns = ['symbol', 'positionValue', 'riskLevel', 'violations', 'timestamp'];
constructor() {
this.thresholdsForm = this.fb.group({
maxPositionSize: [0, [Validators.required, Validators.min(0)]],
maxDailyLoss: [0, [Validators.required, Validators.min(0)]],
maxPortfolioRisk: [0, [Validators.required, Validators.min(0), Validators.max(1)]],
volatilityLimit: [0, [Validators.required, Validators.min(0), Validators.max(1)]]
});
}
ngOnInit() {
this.loadRiskThresholds();
this.loadRiskHistory();
// Refresh risk history every 30 seconds
const historySubscription = interval(30000).subscribe(() => {
this.loadRiskHistory();
});
this.subscriptions.push(historySubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadRiskThresholds() {
this.apiService.getRiskThresholds().subscribe({
next: (response) => {
this.riskThresholds.set(response.data);
this.thresholdsForm.patchValue(response.data);
this.isLoading.set(false);
this.error.set(null);
},
error: (err) => {
console.error('Failed to load risk thresholds:', err);
this.error.set('Failed to load risk thresholds');
this.isLoading.set(false);
this.snackBar.open('Failed to load risk thresholds', 'Dismiss', { duration: 5000 });
}
});
}
private loadRiskHistory() {
this.apiService.getRiskHistory().subscribe({
next: (response) => {
this.riskHistory.set(response.data);
},
error: (err) => {
console.error('Failed to load risk history:', err);
this.snackBar.open('Failed to load risk history', 'Dismiss', { duration: 3000 });
}
});
}
saveThresholds() {
if (this.thresholdsForm.valid) {
this.isSaving.set(true);
const thresholds = this.thresholdsForm.value as RiskThresholds;
this.apiService.updateRiskThresholds(thresholds).subscribe({
next: (response) => {
this.riskThresholds.set(response.data);
this.isSaving.set(false);
this.snackBar.open('Risk thresholds updated successfully', 'Dismiss', { duration: 3000 });
},
error: (err) => {
console.error('Failed to save risk thresholds:', err);
this.isSaving.set(false);
this.snackBar.open('Failed to save risk thresholds', 'Dismiss', { duration: 5000 });
}
});
}
}
refreshData() {
this.isLoading.set(true);
this.loadRiskThresholds();
this.loadRiskHistory();
}
getRiskLevelColor(level: string): string {
switch (level) {
case 'LOW': return 'text-green-600';
case 'MEDIUM': return 'text-yellow-600';
case 'HIGH': return 'text-red-600';
default: return 'text-gray-600';
}
}
}

View file

@ -1 +0,0 @@
/* Settings specific styles */

View file

@ -1,15 +0,0 @@
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Settings</h1>
<p class="text-gray-600 mt-1">Configure application preferences and system settings</p>
</div>
</div>
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">settings</mat-icon>
<p class="mb-4">Application settings and configuration will be implemented here</p>
</div>
</mat-card>
</div>

View file

@ -1,13 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-settings',
standalone: true,
imports: [CommonModule, MatCardModule, MatIconModule],
templateUrl: './settings.component.html',
styleUrl: './settings.component.css'
})
export class SettingsComponent {}

View file

@ -1,165 +0,0 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-drawdown-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="drawdown-chart-container">
<canvas #drawdownChart></canvas>
</div>
`,
styles: `
.drawdown-chart-container {
width: 100%;
height: 300px;
margin-bottom: 20px;
}
`
})
export class DrawdownChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Calculate drawdown series from daily returns
const drawdownData = this.calculateDrawdownSeries(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: drawdownData.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Drawdown',
data: drawdownData.drawdowns,
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: true,
tension: 0.3,
borderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return (value * 100).toFixed(1) + '%';
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
},
min: -0.05, // Show at least 5% drawdown for context
suggestedMax: 0.01
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += (context.parsed.y * 100).toFixed(2) + '%';
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateDrawdownSeries(result: BacktestResult): {
dates: Date[];
drawdowns: number[];
} {
const dates: Date[] = [];
const drawdowns: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate equity curve
let equity = 1;
const equityCurve: number[] = [];
for (const daily of sortedReturns) {
equity *= (1 + daily.return);
equityCurve.push(equity);
dates.push(new Date(daily.date));
}
// Calculate running maximum (high water mark)
let hwm = equityCurve[0];
for (let i = 0; i < equityCurve.length; i++) {
// Update high water mark
hwm = Math.max(hwm, equityCurve[i]);
// Calculate drawdown as percentage from high water mark
const drawdown = (equityCurve[i] / hwm) - 1;
drawdowns.push(drawdown);
}
return { dates, drawdowns };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}

View file

@ -1,171 +0,0 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BacktestResult } from '../../../services/strategy.service';
import { Chart, ChartOptions } from 'chart.js/auto';
@Component({
selector: 'app-equity-chart',
standalone: true,
imports: [CommonModule],
template: `
<div class="equity-chart-container">
<canvas #equityChart></canvas>
</div>
`,
styles: `
.equity-chart-container {
width: 100%;
height: 400px;
margin-bottom: 20px;
}
`
})
export class EquityChartComponent implements OnChanges {
@Input() backtestResult?: BacktestResult;
private chart?: Chart;
private chartElement?: HTMLCanvasElement;
ngOnChanges(changes: SimpleChanges): void {
if (changes['backtestResult'] && this.backtestResult) {
this.renderChart();
}
}
ngAfterViewInit(): void {
this.chartElement = document.querySelector('canvas') as HTMLCanvasElement;
if (this.backtestResult) {
this.renderChart();
}
}
private renderChart(): void {
if (!this.chartElement || !this.backtestResult) return;
// Clean up previous chart if it exists
if (this.chart) {
this.chart.destroy();
}
// Prepare data
const equityCurve = this.calculateEquityCurve(this.backtestResult);
// Create chart
this.chart = new Chart(this.chartElement, {
type: 'line',
data: {
labels: equityCurve.dates.map(date => this.formatDate(date)),
datasets: [
{
label: 'Portfolio Value',
data: equityCurve.values,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.3,
borderWidth: 2,
fill: true
},
{
label: 'Benchmark',
data: equityCurve.benchmark,
borderColor: 'rgba(153, 102, 255, 0.5)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 1,
fill: false
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
ticks: {
maxTicksLimit: 12,
maxRotation: 0,
minRotation: 0
},
grid: {
display: false
}
},
y: {
ticks: {
callback: function(value) {
return '$' + value.toLocaleString();
}
},
grid: {
color: 'rgba(200, 200, 200, 0.2)'
}
}
},
plugins: {
tooltip: {
mode: 'index',
intersect: false,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
.format(context.parsed.y);
}
return label;
}
}
},
legend: {
position: 'top',
}
}
} as ChartOptions
});
}
private calculateEquityCurve(result: BacktestResult): {
dates: Date[];
values: number[];
benchmark: number[];
} {
const initialValue = result.initialCapital;
const dates: Date[] = [];
const values: number[] = [];
const benchmark: number[] = [];
// Sort daily returns by date
const sortedReturns = [...result.dailyReturns].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
// Calculate cumulative portfolio values
let portfolioValue = initialValue;
let benchmarkValue = initialValue;
for (const daily of sortedReturns) {
const date = new Date(daily.date);
portfolioValue = portfolioValue * (1 + daily.return);
// Simple benchmark (e.g., assuming 8% annualized return for a market index)
benchmarkValue = benchmarkValue * (1 + 0.08 / 365);
dates.push(date);
values.push(portfolioValue);
benchmark.push(benchmarkValue);
}
return { dates, values, benchmark };
}
private formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
}
}

View file

@ -1,258 +0,0 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatDividerModule } from '@angular/material/divider';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-performance-metrics',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatGridListModule,
MatDividerModule,
MatTooltipModule
],
template: `
<mat-card class="metrics-card">
<mat-card-header>
<mat-card-title>Performance Metrics</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="metrics-grid">
<div class="metric-group">
<h3>Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total return over the backtest period">Total Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.totalReturn || 0)">
{{formatPercent(backtestResult?.totalReturn || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized return (adjusted for the backtest duration)">Annualized Return</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.annualizedReturn || 0)">
{{formatPercent(backtestResult?.annualizedReturn || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Compound Annual Growth Rate">CAGR</div>
<div class="metric-value" [ngClass]="getReturnClass(backtestResult?.cagr || 0)">
{{formatPercent(backtestResult?.cagr || 0)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk Metrics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Maximum peak-to-valley drawdown">Max Drawdown</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.maxDrawdown || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Number of days in the worst drawdown">Max DD Duration</div>
<div class="metric-value">
{{formatDays(backtestResult?.maxDrawdownDuration || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Annualized standard deviation of returns">Volatility</div>
<div class="metric-value">
{{formatPercent(backtestResult?.volatility || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Square root of the sum of the squares of drawdowns">Ulcer Index</div>
<div class="metric-value">
{{(backtestResult?.ulcerIndex || 0).toFixed(4)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Risk-Adjusted Returns</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Excess return per unit of risk">Sharpe Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sharpeRatio || 0)">
{{(backtestResult?.sharpeRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of downside risk">Sortino Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.sortinoRatio || 0)">
{{(backtestResult?.sortinoRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Return per unit of max drawdown">Calmar Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.calmarRatio || 0)">
{{(backtestResult?.calmarRatio || 0).toFixed(2)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Probability-weighted ratio of gains vs. losses">Omega Ratio</div>
<div class="metric-value" [ngClass]="getRatioClass(backtestResult?.omegaRatio || 0)">
{{(backtestResult?.omegaRatio || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="metric-group">
<h3>Trade Statistics</h3>
<div class="metrics-row">
<div class="metric">
<div class="metric-name" matTooltip="Total number of trades">Total Trades</div>
<div class="metric-value">
{{backtestResult?.totalTrades || 0}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Percentage of winning trades">Win Rate</div>
<div class="metric-value" [ngClass]="getWinRateClass(backtestResult?.winRate || 0)">
{{formatPercent(backtestResult?.winRate || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average profit of winning trades">Avg Win</div>
<div class="metric-value positive">
{{formatPercent(backtestResult?.averageWinningTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Average loss of losing trades">Avg Loss</div>
<div class="metric-value negative">
{{formatPercent(backtestResult?.averageLosingTrade || 0)}}
</div>
</div>
<div class="metric">
<div class="metric-name" matTooltip="Ratio of total gains to total losses">Profit Factor</div>
<div class="metric-value" [ngClass]="getProfitFactorClass(backtestResult?.profitFactor || 0)">
{{(backtestResult?.profitFactor || 0).toFixed(2)}}
</div>
</div>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
`,
styles: `
.metrics-card {
margin-bottom: 20px;
}
.metrics-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.metric-group {
padding: 10px 0;
}
.metric-group h3 {
margin-top: 0;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
color: #555;
}
.metrics-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.metric {
min-width: 120px;
margin-bottom: 16px;
}
.metric-name {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
.metric-value {
font-size: 16px;
font-weight: 500;
}
.positive {
color: #4CAF50;
}
.negative {
color: #F44336;
}
.neutral {
color: #FFA000;
}
mat-divider {
margin: 8px 0;
}
`
})
export class PerformanceMetricsComponent {
@Input() backtestResult?: BacktestResult;
// Formatting helpers
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
formatDays(days: number): string {
return `${days} days`;
}
// Conditional classes
getReturnClass(value: number): string {
if (value > 0) return 'positive';
if (value < 0) return 'negative';
return '';
}
getRatioClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
if (value < 0) return 'negative';
return '';
}
getWinRateClass(value: number): string {
if (value >= 0.55) return 'positive';
if (value >= 0.45) return 'neutral';
return 'negative';
}
getProfitFactorClass(value: number): string {
if (value >= 1.5) return 'positive';
if (value >= 1) return 'neutral';
return 'negative';
}
}

View file

@ -1,221 +0,0 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { BacktestResult } from '../../../services/strategy.service';
@Component({
selector: 'app-trades-table',
standalone: true,
imports: [
CommonModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatCardModule,
MatIconModule
],
template: `
<mat-card class="trades-card">
<mat-card-header>
<mat-card-title>Trades</mat-card-title>
</mat-card-header>
<mat-card-content>
<table mat-table [dataSource]="displayedTrades" matSort (matSortChange)="sortData($event)" class="trades-table">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </th>
<td mat-cell *matCellDef="let trade"> {{trade.symbol}} </td>
</ng-container>
<!-- Entry Date Column -->
<ng-container matColumnDef="entryTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Entry Time </th>
<td mat-cell *matCellDef="let trade"> {{formatDate(trade.entryTime)}} </td>
</ng-container>
<!-- Entry Price Column -->
<ng-container matColumnDef="entryPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Entry Price </th>
<td mat-cell *matCellDef="let trade"> {{formatCurrency(trade.entryPrice)}} </td>
</ng-container>
<!-- Exit Date Column -->
<ng-container matColumnDef="exitTime">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Exit Time </th>
<td mat-cell *matCellDef="let trade"> {{formatDate(trade.exitTime)}} </td>
</ng-container>
<!-- Exit Price Column -->
<ng-container matColumnDef="exitPrice">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Exit Price </th>
<td mat-cell *matCellDef="let trade"> {{formatCurrency(trade.exitPrice)}} </td>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Quantity </th>
<td mat-cell *matCellDef="let trade"> {{trade.quantity}} </td>
</ng-container>
<!-- P&L Column -->
<ng-container matColumnDef="pnl">
<th mat-header-cell *matHeaderCellDef mat-sort-header> P&L </th>
<td mat-cell *matCellDef="let trade"
[ngClass]="{'positive': trade.pnl > 0, 'negative': trade.pnl < 0}">
{{formatCurrency(trade.pnl)}}
</td>
</ng-container>
<!-- P&L Percent Column -->
<ng-container matColumnDef="pnlPercent">
<th mat-header-cell *matHeaderCellDef mat-sort-header> P&L % </th>
<td mat-cell *matCellDef="let trade"
[ngClass]="{'positive': trade.pnlPercent > 0, 'negative': trade.pnlPercent < 0}">
{{formatPercent(trade.pnlPercent)}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator
[length]="totalTrades"
[pageSize]="pageSize"
[pageSizeOptions]="[5, 10, 25, 50]"
(page)="pageChange($event)"
aria-label="Select page">
</mat-paginator>
</mat-card-content>
</mat-card>
`,
styles: `
.trades-card {
margin-bottom: 20px;
}
.trades-table {
width: 100%;
border-collapse: collapse;
}
.mat-column-pnl, .mat-column-pnlPercent {
text-align: right;
font-weight: 500;
}
.positive {
color: #4CAF50;
}
.negative {
color: #F44336;
}
.mat-mdc-row:hover {
background-color: rgba(0, 0, 0, 0.04);
}
`
})
export class TradesTableComponent {
@Input() set backtestResult(value: BacktestResult | undefined) {
if (value) {
this._backtestResult = value;
this.updateDisplayedTrades();
}
}
get backtestResult(): BacktestResult | undefined {
return this._backtestResult;
}
private _backtestResult?: BacktestResult;
// Table configuration
displayedColumns: string[] = [
'symbol', 'entryTime', 'entryPrice', 'exitTime',
'exitPrice', 'quantity', 'pnl', 'pnlPercent'
];
// Pagination
pageSize = 10;
currentPage = 0;
displayedTrades: any[] = [];
get totalTrades(): number {
return this._backtestResult?.trades.length || 0;
}
// Sort the trades
sortData(sort: Sort): void {
if (!sort.active || sort.direction === '') {
this.updateDisplayedTrades();
return;
}
const data = this._backtestResult?.trades.slice() || [];
this.displayedTrades = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'symbol': return this.compare(a.symbol, b.symbol, isAsc);
case 'entryTime': return this.compare(new Date(a.entryTime).getTime(), new Date(b.entryTime).getTime(), isAsc);
case 'entryPrice': return this.compare(a.entryPrice, b.entryPrice, isAsc);
case 'exitTime': return this.compare(new Date(a.exitTime).getTime(), new Date(b.exitTime).getTime(), isAsc);
case 'exitPrice': return this.compare(a.exitPrice, b.exitPrice, isAsc);
case 'quantity': return this.compare(a.quantity, b.quantity, isAsc);
case 'pnl': return this.compare(a.pnl, b.pnl, isAsc);
case 'pnlPercent': return this.compare(a.pnlPercent, b.pnlPercent, isAsc);
default: return 0;
}
}).slice(this.currentPage * this.pageSize, (this.currentPage + 1) * this.pageSize);
}
// Handle page changes
pageChange(event: PageEvent): void {
this.pageSize = event.pageSize;
this.currentPage = event.pageIndex;
this.updateDisplayedTrades();
}
// Update displayed trades based on current page and page size
updateDisplayedTrades(): void {
if (this._backtestResult) {
this.displayedTrades = this._backtestResult.trades.slice(
this.currentPage * this.pageSize,
(this.currentPage + 1) * this.pageSize
);
} else {
this.displayedTrades = [];
}
}
// Helper methods for formatting
formatDate(date: Date | string): string {
return new Date(date).toLocaleString();
}
formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
formatPercent(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(value);
}
private compare(a: number | string, b: number | string, isAsc: boolean): number {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
}

View file

@ -1,185 +0,0 @@
import { Component, Inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import {
BacktestRequest,
BacktestResult,
StrategyService,
TradingStrategy
} from '../../../services/strategy.service';
@Component({
selector: 'app-backtest-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
MatNativeDateModule,
MatProgressBarModule,
MatTabsModule,
MatChipsModule,
MatIconModule,
MatSlideToggleModule
],
templateUrl: './backtest-dialog.component.html',
styleUrl: './backtest-dialog.component.css'
})
export class BacktestDialogComponent implements OnInit {
backtestForm: FormGroup;
strategyTypes: string[] = [];
availableSymbols: string[] = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'SPY', 'QQQ'];
selectedSymbols: string[] = [];
parameters: Record<string, any> = {};
isRunning: boolean = false;
backtestResult: BacktestResult | null = null;
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<BacktestDialogComponent>
) {
// Initialize form with defaults
this.backtestForm = this.fb.group({
strategyType: ['', [Validators.required]],
startDate: [new Date(new Date().setFullYear(new Date().getFullYear() - 1)), [Validators.required]],
endDate: [new Date(), [Validators.required]],
initialCapital: [100000, [Validators.required, Validators.min(1000)]],
dataResolution: ['1d', [Validators.required]],
commission: [0.001, [Validators.required, Validators.min(0), Validators.max(0.1)]],
slippage: [0.0005, [Validators.required, Validators.min(0), Validators.max(0.1)]],
mode: ['event', [Validators.required]]
});
// If strategy is provided, pre-populate the form
if (data) {
this.selectedSymbols = [...data.symbols];
this.backtestForm.patchValue({
strategyType: data.type
});
this.parameters = {...data.parameters};
}
}
ngOnInit(): void {
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
this.strategyService.getStrategyTypes().subscribe({
next: (response) => {
if (response.success) {
this.strategyTypes = response.data;
// If strategy is provided, load its parameters
if (this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: (error) => {
console.error('Error loading strategy types:', error);
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
}
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: (response) => {
if (response.success) {
// If strategy is provided, merge default with existing
if (this.data) {
this.parameters = {
...response.data,
...this.data.parameters
};
} else {
this.parameters = response.data;
}
}
},
error: (error) => {
console.error('Error loading parameters:', error);
this.parameters = {};
}
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) return;
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
onSubmit(): void {
if (this.backtestForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.backtestForm.value;
const backtestRequest: BacktestRequest = {
strategyType: formValue.strategyType,
strategyParams: this.parameters,
symbols: this.selectedSymbols,
startDate: formValue.startDate,
endDate: formValue.endDate,
initialCapital: formValue.initialCapital,
dataResolution: formValue.dataResolution,
commission: formValue.commission,
slippage: formValue.slippage,
mode: formValue.mode
};
this.isRunning = true;
this.strategyService.runBacktest(backtestRequest).subscribe({
next: (response) => {
this.isRunning = false;
if (response.success) {
this.backtestResult = response.data;
}
},
error: (error) => {
this.isRunning = false;
console.error('Backtest error:', error);
}
});
}
close(): void {
this.dialogRef.close(this.backtestResult);
}
}

View file

@ -1,84 +0,0 @@
<h2 mat-dialog-title>{{isEditMode ? 'Edit Strategy' : 'Create Strategy'}}</h2>
<form [formGroup]="strategyForm" (ngSubmit)="onSubmit()">
<mat-dialog-content class="mat-typography">
<div class="grid grid-cols-1 gap-4">
<!-- Basic Strategy Information -->
<mat-form-field appearance="outline" class="w-full">
<mat-label>Strategy Name</mat-label>
<input matInput formControlName="name" placeholder="e.g., My Moving Average Crossover">
<mat-error *ngIf="strategyForm.get('name')?.invalid">Name is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" rows="3"
placeholder="Describe what this strategy does..."></textarea>
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Strategy Type</mat-label>
<mat-select formControlName="type" (selectionChange)="onStrategyTypeChange($event.value)">
<mat-option *ngFor="let type of strategyTypes" [value]="type">
{{type}}
</mat-option>
</mat-select>
<mat-error *ngIf="strategyForm.get('type')?.invalid">Strategy type is required</mat-error>
</mat-form-field>
<!-- Symbol Selection -->
<div class="w-full">
<label class="text-sm">Trading Symbols</label>
<div class="flex flex-wrap gap-2 mb-2">
<mat-chip *ngFor="let symbol of selectedSymbols" [removable]="true"
(removed)="removeSymbol(symbol)">
{{symbol}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</div>
<div class="flex gap-2">
<mat-form-field appearance="outline" class="flex-1">
<mat-label>Add Symbol</mat-label>
<input matInput #symbolInput placeholder="e.g., AAPL">
</mat-form-field>
<button type="button" mat-raised-button color="primary"
(click)="addSymbol(symbolInput.value); symbolInput.value = ''">
Add
</button>
</div>
<div class="mt-2">
<p class="text-sm text-gray-500 mb-1">Suggested symbols:</p>
<div class="flex flex-wrap gap-2">
<button type="button" *ngFor="let symbol of availableSymbols"
mat-stroked-button (click)="addSymbol(symbol)"
[disabled]="selectedSymbols.includes(symbol)">
{{symbol}}
</button>
</div>
</div>
</div>
<!-- Dynamic Strategy Parameters -->
<div *ngIf="strategyForm.get('type')?.value && Object.keys(parameters).length > 0">
<h3 class="text-lg font-semibold mb-2">Strategy Parameters</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<mat-form-field *ngFor="let param of parameters | keyvalue" appearance="outline">
<mat-label>{{param.key}}</mat-label>
<input matInput [value]="param.value"
(input)="updateParameter(param.key, $any($event.target).value)">
</mat-form-field>
</div>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-raised-button color="primary" type="submit"
[disabled]="strategyForm.invalid || selectedSymbols.length === 0">
{{isEditMode ? 'Update' : 'Create'}}
</button>
</mat-dialog-actions>
</form>

View file

@ -1,178 +0,0 @@
import { Component, Inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import {
StrategyService,
TradingStrategy
} from '../../../services/strategy.service';
@Component({
selector: 'app-strategy-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatChipsModule,
MatIconModule,
MatAutocompleteModule
],
templateUrl: './strategy-dialog.component.html',
styleUrl: './strategy-dialog.component.css'
})
export class StrategyDialogComponent implements OnInit {
strategyForm: FormGroup;
isEditMode: boolean = false;
strategyTypes: string[] = [];
availableSymbols: string[] = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'SPY', 'QQQ'];
selectedSymbols: string[] = [];
separatorKeysCodes: number[] = [ENTER, COMMA];
parameters: Record<string, any> = {};
constructor(
private fb: FormBuilder,
private strategyService: StrategyService,
@Inject(MAT_DIALOG_DATA) public data: TradingStrategy | null,
private dialogRef: MatDialogRef<StrategyDialogComponent>
) {
this.isEditMode = !!data;
this.strategyForm = this.fb.group({
name: ['', [Validators.required]],
description: [''],
type: ['', [Validators.required]],
// Dynamic parameters will be added based on strategy type
});
if (this.isEditMode && data) {
this.selectedSymbols = [...data.symbols];
this.strategyForm.patchValue({
name: data.name,
description: data.description,
type: data.type
});
this.parameters = {...data.parameters};
}
}
ngOnInit(): void {
// In a real implementation, fetch available strategy types from the API
this.loadStrategyTypes();
}
loadStrategyTypes(): void {
// In a real implementation, this would call the API
this.strategyService.getStrategyTypes().subscribe({
next: (response) => {
if (response.success) {
this.strategyTypes = response.data;
// If editing, load parameters
if (this.isEditMode && this.data) {
this.onStrategyTypeChange(this.data.type);
}
}
},
error: (error) => {
console.error('Error loading strategy types:', error);
// Fallback to hardcoded types
this.strategyTypes = ['MOVING_AVERAGE_CROSSOVER', 'MEAN_REVERSION', 'CUSTOM'];
}
});
}
onStrategyTypeChange(type: string): void {
// Get default parameters for this strategy type
this.strategyService.getStrategyParameters(type).subscribe({
next: (response) => {
if (response.success) {
// If editing, merge default with existing
if (this.isEditMode && this.data) {
this.parameters = {
...response.data,
...this.data.parameters
};
} else {
this.parameters = response.data;
}
}
},
error: (error) => {
console.error('Error loading parameters:', error);
// Fallback to empty parameters
this.parameters = {};
}
});
}
addSymbol(symbol: string): void {
if (!symbol || this.selectedSymbols.includes(symbol)) return;
this.selectedSymbols.push(symbol);
}
removeSymbol(symbol: string): void {
this.selectedSymbols = this.selectedSymbols.filter(s => s !== symbol);
}
onSubmit(): void {
if (this.strategyForm.invalid || this.selectedSymbols.length === 0) {
return;
}
const formValue = this.strategyForm.value;
const strategy: Partial<TradingStrategy> = {
name: formValue.name,
description: formValue.description,
type: formValue.type,
symbols: this.selectedSymbols,
parameters: this.parameters,
};
if (this.isEditMode && this.data) {
this.strategyService.updateStrategy(this.data.id, strategy).subscribe({
next: (response) => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: (error) => {
console.error('Error updating strategy:', error);
}
});
} else {
this.strategyService.createStrategy(strategy).subscribe({
next: (response) => {
if (response.success) {
this.dialogRef.close(true);
}
},
error: (error) => {
console.error('Error creating strategy:', error);
}
});
}
}
updateParameter(key: string, value: any): void {
this.parameters[key] = value;
}
}

View file

@ -1 +0,0 @@
/* Strategies specific styles */

View file

@ -1,142 +0,0 @@
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Trading Strategies</h1>
<p class="text-gray-600 mt-1">Configure and monitor your automated trading strategies</p>
</div>
<div class="flex gap-2">
<button mat-raised-button color="primary" (click)="openStrategyDialog()">
<mat-icon>add</mat-icon> New Strategy
</button>
<button mat-raised-button color="accent" (click)="openBacktestDialog()">
<mat-icon>science</mat-icon> New Backtest
</button>
</div>
</div>
<mat-card *ngIf="isLoading" class="p-4">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card>
<div *ngIf="!selectedStrategy; else strategyDetails">
<mat-card *ngIf="strategies.length > 0; else noStrategies" class="p-4">
<table mat-table [dataSource]="strategies" class="w-full">
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Strategy</th>
<td mat-cell *matCellDef="let strategy">
<div class="font-semibold">{{strategy.name}}</div>
<div class="text-xs text-gray-500">{{strategy.description}}</div>
</td>
</ng-container>
<!-- Type Column -->
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>Type</th>
<td mat-cell *matCellDef="let strategy">{{strategy.type}}</td>
</ng-container>
<!-- Symbols Column -->
<ng-container matColumnDef="symbols">
<th mat-header-cell *matHeaderCellDef>Symbols</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex flex-wrap gap-1 max-w-xs">
<mat-chip *ngFor="let symbol of strategy.symbols.slice(0, 3)">
{{symbol}}
</mat-chip>
<span *ngIf="strategy.symbols.length > 3" class="text-gray-500">
+{{strategy.symbols.length - 3}} more
</span>
</div>
</td>
</ng-container>
<!-- Status Column -->
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>Status</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full"
[style.background-color]="getStatusColor(strategy.status)"></span>
{{strategy.status}}
</div>
</td>
</ng-container>
<!-- Performance Column -->
<ng-container matColumnDef="performance">
<th mat-header-cell *matHeaderCellDef>Performance</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex flex-col">
<div class="flex justify-between">
<span class="text-xs text-gray-500">Return:</span>
<span [ngClass]="{'text-green-600': strategy.performance.totalReturn > 0,
'text-red-600': strategy.performance.totalReturn < 0}">
{{strategy.performance.totalReturn | percent:'1.2-2'}}
</span>
</div>
<div class="flex justify-between">
<span class="text-xs text-gray-500">Win Rate:</span>
<span>{{strategy.performance.winRate | percent:'1.0-0'}}</span>
</div>
</div>
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let strategy">
<div class="flex gap-2">
<button mat-icon-button color="primary" (click)="viewStrategyDetails(strategy)">
<mat-icon>visibility</mat-icon>
</button>
<button mat-icon-button [color]="strategy.status === 'ACTIVE' ? 'warn' : 'primary'"
(click)="toggleStrategyStatus(strategy)">
<mat-icon>{{strategy.status === 'ACTIVE' ? 'pause' : 'play_arrow'}}</mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="openStrategyDialog(strategy)">
<mat-icon>edit</mat-icon>
<span>Edit</span>
</button>
<button mat-menu-item (click)="openBacktestDialog(strategy)">
<mat-icon>science</mat-icon>
<span>Backtest</span>
</button>
</mat-menu>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-card>
<ng-template #noStrategies>
<mat-card class="p-6 flex flex-col items-center justify-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem; margin: 0 auto;">psychology</mat-icon>
<h3 class="text-xl font-semibold mt-4">No Strategies Yet</h3>
<p class="mb-4">Create your first trading strategy to get started</p>
<button mat-raised-button color="primary" (click)="openStrategyDialog()">
<mat-icon>add</mat-icon> Create Strategy
</button>
</div>
</mat-card>
</ng-template>
</div>
<ng-template #strategyDetails>
<div class="flex justify-between items-center mb-4">
<button mat-button (click)="selectedStrategy = null">
<mat-icon>arrow_back</mat-icon> Back to Strategies
</button>
</div>
<app-strategy-details [strategy]="selectedStrategy"></app-strategy-details>
</ng-template>
</div>

View file

@ -1,148 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { MatChipsModule } from '@angular/material/chips';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { StrategyService, TradingStrategy } from '../../services/strategy.service';
import { WebSocketService } from '../../services/websocket.service';
import { StrategyDialogComponent } from './dialogs/strategy-dialog.component';
import { BacktestDialogComponent } from './dialogs/backtest-dialog.component';
import { StrategyDetailsComponent } from './strategy-details/strategy-details.component';
@Component({
selector: 'app-strategies',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatIconModule,
MatButtonModule,
MatTabsModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatDialogModule,
MatMenuModule,
MatChipsModule,
MatProgressBarModule,
FormsModule,
ReactiveFormsModule,
StrategyDetailsComponent
],
templateUrl: './strategies.component.html',
styleUrl: './strategies.component.css'
})
export class StrategiesComponent implements OnInit {
strategies: TradingStrategy[] = [];
displayedColumns: string[] = ['name', 'type', 'symbols', 'status', 'performance', 'actions'];
selectedStrategy: TradingStrategy | null = null;
isLoading = false;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.loadStrategies();
this.listenForStrategyUpdates();
}
loadStrategies(): void {
this.isLoading = true;
this.strategyService.getStrategies().subscribe({
next: (response) => {
if (response.success) {
this.strategies = response.data;
}
this.isLoading = false;
},
error: (error) => {
console.error('Error loading strategies:', error);
this.isLoading = false;
}
});
}
listenForStrategyUpdates(): void {
this.webSocketService.messages.subscribe(message => {
if (message.type === 'STRATEGY_CREATED' ||
message.type === 'STRATEGY_UPDATED' ||
message.type === 'STRATEGY_STATUS_CHANGED') {
// Refresh the strategy list when changes occur
this.loadStrategies();
}
});
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE': return 'green';
case 'PAUSED': return 'orange';
case 'ERROR': return 'red';
default: return 'gray';
}
}
openStrategyDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: strategy || null
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadStrategies();
}
});
}
openBacktestDialog(strategy?: TradingStrategy): void {
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: strategy || null
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Handle backtest result if needed
}
});
}
toggleStrategyStatus(strategy: TradingStrategy): void {
this.isLoading = true;
if (strategy.status === 'ACTIVE') {
this.strategyService.pauseStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: (error) => {
console.error('Error pausing strategy:', error);
this.isLoading = false;
}
});
} else {
this.strategyService.startStrategy(strategy.id).subscribe({
next: () => this.loadStrategies(),
error: (error) => {
console.error('Error starting strategy:', error);
this.isLoading = false;
}
});
}
}
viewStrategyDetails(strategy: TradingStrategy): void {
this.selectedStrategy = strategy;
}
}

View file

@ -1,16 +0,0 @@
/* Strategy details specific styles */
table {
width: 100%;
border-collapse: collapse;
}
th {
font-weight: 600;
color: #4b5563;
font-size: 0.875rem;
border-bottom: 1px solid #e5e7eb;
}
td {
border-bottom: 1px solid #e5e7eb;
}

View file

@ -1,214 +0,0 @@
<div class="space-y-6" *ngIf="strategy">
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<!-- Strategy Overview Card -->
<mat-card class="flex-1 p-4">
<div class="flex items-start justify-between">
<div>
<h2 class="text-xl font-bold">{{strategy.name}}</h2>
<p class="text-gray-600 text-sm">{{strategy.description}}</p>
</div>
<div class="flex items-center gap-2">
<button mat-raised-button color="primary" class="mr-2" (click)="openBacktestDialog()">
Run Backtest
</button>
<span class="px-3 py-1 rounded-full text-xs font-semibold"
[style.background-color]="getStatusColor(strategy.status)"
style="color: white;">
{{strategy.status}}
</span>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 class="font-semibold text-sm text-gray-600">Type</h3>
<p>{{strategy.type}}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Created</h3>
<p>{{strategy.createdAt | date:'medium'}}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Last Updated</h3>
<p>{{strategy.updatedAt | date:'medium'}}</p>
</div>
<div>
<h3 class="font-semibold text-sm text-gray-600">Symbols</h3>
<div class="flex flex-wrap gap-1 mt-1">
<mat-chip *ngFor="let symbol of strategy.symbols">{{symbol}}</mat-chip>
</div>
</div>
</div>
</mat-card>
<!-- Performance Summary Card -->
<mat-card class="md:w-1/3 p-4">
<h3 class="text-lg font-bold mb-3">Performance</h3>
<div class="grid grid-cols-2 gap-3">
<div>
<p class="text-sm text-gray-600">Return</p>
<p class="text-xl font-semibold"
[ngClass]="{'text-green-600': performance.totalReturn >= 0, 'text-red-600': performance.totalReturn < 0}">
{{performance.totalReturn | percent:'1.2-2'}}
</p>
</div>
<div>
<p class="text-sm text-gray-600">Win Rate</p>
<p class="text-xl font-semibold">{{performance.winRate | percent:'1.0-0'}}</p>
</div>
<div>
<p class="text-sm text-gray-600">Sharpe Ratio</p>
<p class="text-xl font-semibold">{{performance.sharpeRatio | number:'1.2-2'}}</p>
</div>
<div>
<p class="text-sm text-gray-600">Max Drawdown</p>
<p class="text-xl font-semibold text-red-600">{{performance.maxDrawdown | percent:'1.2-2'}}</p>
</div>
<div>
<p class="text-sm text-gray-600">Total Trades</p>
<p class="text-xl font-semibold">{{performance.totalTrades}}</p>
</div>
<div>
<p class="text-sm text-gray-600">Sortino Ratio</p>
<p class="text-xl font-semibold">{{performance.sortinoRatio | number:'1.2-2'}}</p>
</div>
</div>
<mat-divider class="my-4"></mat-divider>
<div class="flex justify-between mt-2">
<button mat-button color="primary" *ngIf="strategy.status !== 'ACTIVE'" (click)="activateStrategy()">
<mat-icon>play_arrow</mat-icon> Start
</button>
<button mat-button color="accent" *ngIf="strategy.status === 'ACTIVE'" (click)="pauseStrategy()">
<mat-icon>pause</mat-icon> Pause
</button>
<button mat-button color="warn" *ngIf="strategy.status === 'ACTIVE'" (click)="stopStrategy()">
<mat-icon>stop</mat-icon> Stop
</button>
<button mat-button (click)="openEditDialog()">
<mat-icon>edit</mat-icon> Edit
</button>
</div>
</mat-card>
</div>
<!-- Parameters Card -->
<mat-card class="p-4">
<h3 class="text-lg font-bold mb-3">Strategy Parameters</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div *ngFor="let param of strategy.parameters | keyvalue">
<p class="text-sm text-gray-600">{{param.key}}</p>
<p class="font-semibold">{{param.value}}</p>
</div>
</div>
</mat-card>
<!-- Backtest Results Section (only shown when a backtest has been run) -->
<div *ngIf="backtestResult" class="backtest-results space-y-6">
<h2 class="text-xl font-bold">Backtest Results</h2>
<!-- Performance Metrics Component -->
<app-performance-metrics [backtestResult]="backtestResult"></app-performance-metrics>
<!-- Equity Chart Component -->
<app-equity-chart [backtestResult]="backtestResult"></app-equity-chart>
<!-- Drawdown Chart Component -->
<app-drawdown-chart [backtestResult]="backtestResult"></app-drawdown-chart>
<!-- Trades Table Component -->
<app-trades-table [backtestResult]="backtestResult"></app-trades-table>
</div>
<!-- Tabs for Signals/Trades -->
<mat-card class="p-0">
<mat-tab-group>
<!-- Signals Tab -->
<mat-tab label="Recent Signals">
<div class="p-4">
<ng-container *ngIf="!isLoadingSignals; else loadingSignals">
<table class="min-w-full">
<thead>
<tr>
<th class="py-2 text-left">Time</th>
<th class="py-2 text-left">Symbol</th>
<th class="py-2 text-left">Action</th>
<th class="py-2 text-left">Price</th>
<th class="py-2 text-left">Quantity</th>
<th class="py-2 text-left">Confidence</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let signal of signals">
<td class="py-2">{{signal.timestamp | date:'short'}}</td>
<td class="py-2">{{signal.symbol}}</td>
<td class="py-2">
<span class="px-2 py-1 rounded text-xs font-semibold"
[style.background-color]="getSignalColor(signal.action)"
style="color: white;">
{{signal.action}}
</span>
</td>
<td class="py-2">${{signal.price | number:'1.2-2'}}</td>
<td class="py-2">{{signal.quantity}}</td>
<td class="py-2">{{signal.confidence | percent:'1.0-0'}}</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-template #loadingSignals>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</div>
</mat-tab>
<!-- Trades Tab -->
<mat-tab label="Recent Trades">
<div class="p-4">
<ng-container *ngIf="!isLoadingTrades; else loadingTrades">
<table class="min-w-full">
<thead>
<tr>
<th class="py-2 text-left">Symbol</th>
<th class="py-2 text-left">Entry</th>
<th class="py-2 text-left">Exit</th>
<th class="py-2 text-left">Quantity</th>
<th class="py-2 text-left">P&L</th>
<th class="py-2 text-left">P&L %</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let trade of trades">
<td class="py-2">{{trade.symbol}}</td>
<td class="py-2">
${{trade.entryPrice | number:'1.2-2'}} @ {{trade.entryTime | date:'short'}}
</td>
<td class="py-2">
${{trade.exitPrice | number:'1.2-2'}} @ {{trade.exitTime | date:'short'}}
</td>
<td class="py-2">{{trade.quantity}}</td>
<td class="py-2" [ngClass]="{'text-green-600': trade.pnl >= 0, 'text-red-600': trade.pnl < 0}">
${{trade.pnl | number:'1.2-2'}}
</td>
<td class="py-2" [ngClass]="{'text-green-600': trade.pnlPercent >= 0, 'text-red-600': trade.pnlPercent < 0}">
{{trade.pnlPercent | number:'1.2-2'}}%
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-template #loadingTrades>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
</div>
</mat-tab>
</mat-tab-group>
</mat-card>
</div>
<mat-card class="p-6 flex items-center" *ngIf="!strategy">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">psychology</mat-icon>
<p class="mb-4">No strategy selected</p>
</div>
</mat-card>

View file

@ -1,381 +0,0 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatChipsModule } from '@angular/material/chips';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatDialog } from '@angular/material/dialog';
import { BacktestResult, TradingStrategy, StrategyService } from '../../../services/strategy.service';
import { WebSocketService } from '../../../services/websocket.service';
import { EquityChartComponent } from '../components/equity-chart.component';
import { DrawdownChartComponent } from '../components/drawdown-chart.component';
import { TradesTableComponent } from '../components/trades-table.component';
import { PerformanceMetricsComponent } from '../components/performance-metrics.component';
import { StrategyDialogComponent } from '../dialogs/strategy-dialog.component';
import { BacktestDialogComponent } from '../dialogs/backtest-dialog.component';
@Component({
selector: 'app-strategy-details',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatChipsModule,
MatProgressBarModule,
MatDividerModule,
EquityChartComponent,
DrawdownChartComponent,
TradesTableComponent,
PerformanceMetricsComponent
],
templateUrl: './strategy-details.component.html',
styleUrl: './strategy-details.component.css'
})
export class StrategyDetailsComponent implements OnChanges {
@Input() strategy: TradingStrategy | null = null;
signals: any[] = [];
trades: any[] = [];
performance: any = {};
isLoadingSignals = false;
isLoadingTrades = false;
backtestResult: BacktestResult | undefined;
constructor(
private strategyService: StrategyService,
private webSocketService: WebSocketService,
private dialog: MatDialog
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['strategy'] && this.strategy) {
this.loadStrategyData();
this.listenForUpdates();
}
}
loadStrategyData(): void {
if (!this.strategy) return;
// In a real implementation, these would call API methods to fetch the data
this.loadSignals();
this.loadTrades();
this.loadPerformance();
}
loadSignals(): void {
if (!this.strategy) return;
this.isLoadingSignals = true;
// First check if we can get real signals from the API
this.strategyService.getStrategySignals(this.strategy.id)
.subscribe({
next: (response) => {
if (response.success && response.data && response.data.length > 0) {
this.signals = response.data;
} else {
// Fallback to mock data if no real signals available
this.signals = this.generateMockSignals();
}
this.isLoadingSignals = false;
},
error: (error) => {
console.error('Error loading signals', error);
// Fallback to mock data on error
this.signals = this.generateMockSignals();
this.isLoadingSignals = false;
}
});
}
loadTrades(): void {
if (!this.strategy) return;
this.isLoadingTrades = true;
// First check if we can get real trades from the API
this.strategyService.getStrategyTrades(this.strategy.id)
.subscribe({
next: (response) => {
if (response.success && response.data && response.data.length > 0) {
this.trades = response.data;
} else {
// Fallback to mock data if no real trades available
this.trades = this.generateMockTrades();
}
this.isLoadingTrades = false;
},
error: (error) => {
console.error('Error loading trades', error);
// Fallback to mock data on error
this.trades = this.generateMockTrades();
this.isLoadingTrades = false;
}
});
}
loadPerformance(): void {
// This would be an API call in a real implementation
this.performance = {
totalReturn: this.strategy?.performance.totalReturn || 0,
winRate: this.strategy?.performance.winRate || 0,
sharpeRatio: this.strategy?.performance.sharpeRatio || 0,
maxDrawdown: this.strategy?.performance.maxDrawdown || 0,
totalTrades: this.strategy?.performance.totalTrades || 0,
// Additional metrics that would come from the API
dailyReturn: 0.0012,
volatility: 0.008,
sortinoRatio: 1.2,
calmarRatio: 0.7
};
}
listenForUpdates(): void {
if (!this.strategy) return;
// Subscribe to strategy signals
this.webSocketService.getStrategySignals(this.strategy.id)
.subscribe((signal: any) => {
// Add the new signal to the top of the list
this.signals = [signal, ...this.signals.slice(0, 9)]; // Keep only the latest 10 signals
});
// Subscribe to strategy trades
this.webSocketService.getStrategyTrades(this.strategy.id)
.subscribe((trade: any) => {
// Add the new trade to the top of the list
this.trades = [trade, ...this.trades.slice(0, 9)]; // Keep only the latest 10 trades
// Update performance metrics
this.updatePerformanceMetrics();
});
// Subscribe to strategy status updates
this.webSocketService.getStrategyUpdates()
.subscribe((update: any) => {
if (update.strategyId === this.strategy?.id) {
// Update strategy status if changed
if (update.status && this.strategy && this.strategy.status !== update.status) {
this.strategy.status = update.status;
}
// Update other fields if present
if (update.performance && this.strategy) {
this.strategy.performance = {
...this.strategy.performance,
...update.performance
};
this.performance = {
...this.performance,
...update.performance
};
}
}
});
console.log('WebSocket listeners for strategy updates initialized');
}
/**
* Update performance metrics when new trades come in
*/
private updatePerformanceMetrics(): void {
if (!this.strategy || this.trades.length === 0) return;
// Calculate basic metrics
const winningTrades = this.trades.filter(t => t.pnl > 0);
const losingTrades = this.trades.filter(t => t.pnl < 0);
const totalPnl = this.trades.reduce((sum, trade) => sum + trade.pnl, 0);
const winRate = winningTrades.length / this.trades.length;
// Update performance data
const currentPerformance = this.performance || {};
this.performance = {
...currentPerformance,
totalTrades: this.trades.length,
winRate: winRate,
totalReturn: (currentPerformance.totalReturn || 0) + (totalPnl / 10000) // Approximate
};
// Update strategy performance as well
if (this.strategy && this.strategy.performance) {
this.strategy.performance = {
...this.strategy.performance,
totalTrades: this.trades.length,
winRate: winRate
};
}
}
getStatusColor(status: string): string {
switch (status) {
case 'ACTIVE': return 'green';
case 'PAUSED': return 'orange';
case 'ERROR': return 'red';
default: return 'gray';
}
}
getSignalColor(action: string): string {
switch (action) {
case 'BUY': return 'green';
case 'SELL': return 'red';
default: return 'gray';
}
}
/**
* Open the backtest dialog to run a backtest for this strategy
*/
openBacktestDialog(): void {
if (!this.strategy) return;
const dialogRef = this.dialog.open(BacktestDialogComponent, {
width: '800px',
data: this.strategy
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Store the backtest result for visualization
this.backtestResult = result;
}
});
}
/**
* Open the strategy edit dialog
*/
openEditDialog(): void {
if (!this.strategy) return;
const dialogRef = this.dialog.open(StrategyDialogComponent, {
width: '600px',
data: this.strategy
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// Refresh strategy data after edit
this.loadStrategyData();
}
});
}
/**
* Start the strategy
*/
activateStrategy(): void {
if (!this.strategy) return;
this.strategyService.startStrategy(this.strategy.id).subscribe({
next: (response) => {
if (response.success) {
this.strategy!.status = 'ACTIVE';
}
},
error: (error) => {
console.error('Error starting strategy:', error);
}
});
}
/**
* Pause the strategy
*/
pauseStrategy(): void {
if (!this.strategy) return;
this.strategyService.pauseStrategy(this.strategy.id).subscribe({
next: (response) => {
if (response.success) {
this.strategy!.status = 'PAUSED';
}
},
error: (error) => {
console.error('Error pausing strategy:', error);
}
});
}
/**
* Stop the strategy
*/
stopStrategy(): void {
if (!this.strategy) return;
this.strategyService.stopStrategy(this.strategy.id).subscribe({
next: (response) => {
if (response.success) {
this.strategy!.status = 'INACTIVE';
}
},
error: (error) => {
console.error('Error stopping strategy:', error);
}
});
}
// Methods to generate mock data
private generateMockSignals(): any[] {
if (!this.strategy) return [];
const signals = [];
const actions = ['BUY', 'SELL', 'HOLD'];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol = this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const action = actions[Math.floor(Math.random() * actions.length)];
signals.push({
id: `sig_${i}`,
symbol,
action,
confidence: 0.7 + Math.random() * 0.3,
price: 100 + Math.random() * 50,
timestamp: new Date(now.getTime() - i * 1000 * 60 * 30), // 30 min intervals
quantity: Math.floor(10 + Math.random() * 90)
});
}
return signals;
}
private generateMockTrades(): any[] {
if (!this.strategy) return [];
const trades = [];
const now = new Date();
for (let i = 0; i < 10; i++) {
const symbol = this.strategy.symbols[Math.floor(Math.random() * this.strategy.symbols.length)];
const entryPrice = 100 + Math.random() * 50;
const exitPrice = entryPrice * (1 + (Math.random() * 0.1 - 0.05)); // -5% to +5%
const quantity = Math.floor(10 + Math.random() * 90);
const pnl = (exitPrice - entryPrice) * quantity;
trades.push({
id: `trade_${i}`,
symbol,
entryPrice,
entryTime: new Date(now.getTime() - (i + 5) * 1000 * 60 * 60), // Hourly intervals
exitPrice,
exitTime: new Date(now.getTime() - i * 1000 * 60 * 60),
quantity,
pnl,
pnlPercent: ((exitPrice - entryPrice) / entryPrice) * 100
});
}
return trades;
}
}

View file

@ -1,98 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface RiskThresholds {
maxPositionSize: number;
maxDailyLoss: number;
maxPortfolioRisk: number;
volatilityLimit: number;
}
export interface RiskEvaluation {
symbol: string;
positionValue: number;
positionRisk: number;
violations: string[];
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export interface MarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private readonly baseUrls = {
riskGuardian: 'http://localhost:3002',
strategyOrchestrator: 'http://localhost:3003',
marketDataGateway: 'http://localhost:3001'
};
constructor(private http: HttpClient) {}
// Risk Guardian API
getRiskThresholds(): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.get<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`
);
}
updateRiskThresholds(thresholds: RiskThresholds): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.put<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`,
thresholds
);
}
evaluateRisk(params: {
symbol: string;
quantity: number;
price: number;
portfolioValue: number;
}): Observable<{ success: boolean; data: RiskEvaluation }> {
return this.http.post<{ success: boolean; data: RiskEvaluation }>(
`${this.baseUrls.riskGuardian}/api/risk/evaluate`,
params
);
}
getRiskHistory(): Observable<{ success: boolean; data: RiskEvaluation[] }> {
return this.http.get<{ success: boolean; data: RiskEvaluation[] }>(
`${this.baseUrls.riskGuardian}/api/risk/history`
);
}
// Strategy Orchestrator API
getStrategies(): Observable<{ success: boolean; data: any[] }> {
return this.http.get<{ success: boolean; data: any[] }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`
);
}
createStrategy(strategy: any): Observable<{ success: boolean; data: any }> {
return this.http.post<{ success: boolean; data: any }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`,
strategy
);
}
// Market Data Gateway API
getMarketData(symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']): Observable<{ success: boolean; data: MarketData[] }> {
const symbolsParam = symbols.join(',');
return this.http.get<{ success: boolean; data: MarketData[] }>(
`${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}`
);
}
// Health checks
checkServiceHealth(service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'): Observable<any> {
return this.http.get(`${this.baseUrls[service]}/health`);
}
}

View file

@ -1,193 +0,0 @@
import { Injectable, signal, inject } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { WebSocketService, RiskAlert } from './websocket.service';
import { Subscription } from 'rxjs';
export interface Notification {
id: string;
type: 'info' | 'warning' | 'error' | 'success';
title: string;
message: string;
timestamp: Date;
read: boolean;
}
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private snackBar = inject(MatSnackBar);
private webSocketService = inject(WebSocketService);
private riskAlertsSubscription?: Subscription;
// Reactive state
public notifications = signal<Notification[]>([]);
public unreadCount = signal<number>(0);
constructor() {
this.initializeRiskAlerts();
}
private initializeRiskAlerts() {
// Subscribe to risk alerts from WebSocket
this.riskAlertsSubscription = this.webSocketService.getRiskAlerts().subscribe({
next: (alert: RiskAlert) => {
this.handleRiskAlert(alert);
},
error: (err) => {
console.error('Risk alert subscription error:', err);
}
});
}
private handleRiskAlert(alert: RiskAlert) {
const notification: Notification = {
id: alert.id,
type: this.mapSeverityToType(alert.severity),
title: `Risk Alert: ${alert.symbol}`,
message: alert.message,
timestamp: new Date(alert.timestamp),
read: false
};
this.addNotification(notification);
this.showSnackBarAlert(notification);
}
private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' {
switch (severity) {
case 'HIGH': return 'error';
case 'MEDIUM': return 'warning';
case 'LOW': return 'info';
default: return 'info';
}
}
private showSnackBarAlert(notification: Notification) {
const actionText = notification.type === 'error' ? 'Review' : 'Dismiss';
const duration = notification.type === 'error' ? 10000 : 5000;
this.snackBar.open(
`${notification.title}: ${notification.message}`,
actionText,
{
duration,
panelClass: [`snack-${notification.type}`]
}
);
}
// Public methods
addNotification(notification: Notification) {
const current = this.notifications();
const updated = [notification, ...current].slice(0, 50); // Keep only latest 50
this.notifications.set(updated);
this.updateUnreadCount();
}
markAsRead(notificationId: string) {
const current = this.notifications();
const updated = current.map(n =>
n.id === notificationId ? { ...n, read: true } : n
);
this.notifications.set(updated);
this.updateUnreadCount();
}
markAllAsRead() {
const current = this.notifications();
const updated = current.map(n => ({ ...n, read: true }));
this.notifications.set(updated);
this.updateUnreadCount();
}
clearNotification(notificationId: string) {
const current = this.notifications();
const updated = current.filter(n => n.id !== notificationId);
this.notifications.set(updated);
this.updateUnreadCount();
}
clearAllNotifications() {
this.notifications.set([]);
this.unreadCount.set(0);
}
private updateUnreadCount() {
const unread = this.notifications().filter(n => !n.read).length;
this.unreadCount.set(unread);
}
// Manual notification methods
showSuccess(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'success',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 3000,
panelClass: ['snack-success']
});
}
showError(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'error',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 8000,
panelClass: ['snack-error']
});
}
showWarning(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'warning',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 5000,
panelClass: ['snack-warning']
});
}
showInfo(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'info',
title,
message,
timestamp: new Date(),
read: false
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 4000,
panelClass: ['snack-info']
});
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
ngOnDestroy() {
this.riskAlertsSubscription?.unsubscribe();
}
}

View file

@ -1,209 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface TradingStrategy {
id: string;
name: string;
description: string;
status: 'ACTIVE' | 'INACTIVE' | 'PAUSED' | 'ERROR';
type: string;
symbols: string[];
parameters: Record<string, any>;
performance: {
totalTrades: number;
winRate: number;
totalReturn: number;
sharpeRatio: number;
maxDrawdown: number;
};
createdAt: Date;
updatedAt: Date;
}
export interface BacktestRequest {
strategyType: string;
strategyParams: Record<string, any>;
symbols: string[];
startDate: Date | string;
endDate: Date | string;
initialCapital: number;
dataResolution: '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
commission: number;
slippage: number;
mode: 'event' | 'vector';
}
export interface BacktestResult {
strategyId: string;
startDate: Date;
endDate: Date;
duration: number;
initialCapital: number;
finalCapital: number;
totalReturn: number;
annualizedReturn: number;
sharpeRatio: number;
maxDrawdown: number;
maxDrawdownDuration: number;
winRate: number;
totalTrades: number;
winningTrades: number;
losingTrades: number;
averageWinningTrade: number;
averageLosingTrade: number;
profitFactor: number;
dailyReturns: Array<{ date: Date; return: number }>;
trades: Array<{
symbol: string;
entryTime: Date;
entryPrice: number;
exitTime: Date;
exitPrice: number;
quantity: number;
pnl: number;
pnlPercent: number;
}>;
// Advanced metrics
sortinoRatio?: number;
calmarRatio?: number;
omegaRatio?: number;
cagr?: number;
volatility?: number;
ulcerIndex?: number;
}
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
@Injectable({
providedIn: 'root'
})
export class StrategyService {
private apiBaseUrl = '/api'; // Will be proxied to the correct backend endpoint
constructor(private http: HttpClient) { }
// Strategy Management
getStrategies(): Observable<ApiResponse<TradingStrategy[]>> {
return this.http.get<ApiResponse<TradingStrategy[]>>(`${this.apiBaseUrl}/strategies`);
}
getStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.get<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`);
}
createStrategy(strategy: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies`, strategy);
}
updateStrategy(id: string, updates: Partial<TradingStrategy>): Observable<ApiResponse<TradingStrategy>> {
return this.http.put<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}`, updates);
}
startStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/start`, {});
}
stopStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/stop`, {});
}
pauseStrategy(id: string): Observable<ApiResponse<TradingStrategy>> {
return this.http.post<ApiResponse<TradingStrategy>>(`${this.apiBaseUrl}/strategies/${id}/pause`, {});
}
// Backtest Management
getStrategyTypes(): Observable<ApiResponse<string[]>> {
return this.http.get<ApiResponse<string[]>>(`${this.apiBaseUrl}/strategy-types`);
}
getStrategyParameters(type: string): Observable<ApiResponse<Record<string, any>>> {
return this.http.get<ApiResponse<Record<string, any>>>(`${this.apiBaseUrl}/strategy-parameters/${type}`);
}
runBacktest(request: BacktestRequest): Observable<ApiResponse<BacktestResult>> {
return this.http.post<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest`, request);
}
getBacktestResult(id: string): Observable<ApiResponse<BacktestResult>> {
return this.http.get<ApiResponse<BacktestResult>>(`${this.apiBaseUrl}/backtest/${id}`);
}
optimizeStrategy(
baseRequest: BacktestRequest,
parameterGrid: Record<string, any[]>
): Observable<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>> {
return this.http.post<ApiResponse<Array<BacktestResult & { parameters: Record<string, any> }>>>(
`${this.apiBaseUrl}/backtest/optimize`,
{ baseRequest, parameterGrid }
);
}
// Strategy Signals and Trades
getStrategySignals(strategyId: string): Observable<ApiResponse<Array<{
id: string;
strategyId: string;
symbol: string;
action: string;
price: number;
quantity: number;
timestamp: Date;
confidence: number;
metadata?: any;
}>>> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/signals`);
}
getStrategyTrades(strategyId: string): Observable<ApiResponse<Array<{
id: string;
strategyId: string;
symbol: string;
entryPrice: number;
entryTime: Date;
exitPrice: number;
exitTime: Date;
quantity: number;
pnl: number;
pnlPercent: number;
}>>> {
return this.http.get<ApiResponse<any[]>>(`${this.apiBaseUrl}/strategies/${strategyId}/trades`);
}
// Helper methods for common transformations
formatBacktestRequest(formData: any): BacktestRequest {
// Handle date formatting and parameter conversion
return {
...formData,
startDate: formData.startDate instanceof Date ? formData.startDate.toISOString() : formData.startDate,
endDate: formData.endDate instanceof Date ? formData.endDate.toISOString() : formData.endDate,
strategyParams: this.convertParameterTypes(formData.strategyType, formData.strategyParams)
};
}
private convertParameterTypes(strategyType: string, params: Record<string, any>): Record<string, any> {
// Convert string parameters to correct types based on strategy requirements
const result: Record<string, any> = {};
for (const [key, value] of Object.entries(params)) {
if (typeof value === 'string') {
// Try to convert to number if it looks like a number
if (!isNaN(Number(value))) {
result[key] = Number(value);
} else if (value.toLowerCase() === 'true') {
result[key] = true;
} else if (value.toLowerCase() === 'false') {
result[key] = false;
} else {
result[key] = value;
}
} else {
result[key] = value;
}
}
return result;
}
}

View file

@ -1,218 +0,0 @@
import { Injectable, signal } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
export interface WebSocketMessage {
type: string;
data: any;
timestamp: string;
}
export interface MarketDataUpdate {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
export interface RiskAlert {
id: string;
symbol: string;
alertType: 'POSITION_LIMIT' | 'DAILY_LOSS' | 'VOLATILITY' | 'PORTFOLIO_RISK';
message: string;
severity: 'LOW' | 'MEDIUM' | 'HIGH';
timestamp: string;
}
@Injectable({
providedIn: 'root'
})
export class WebSocketService {
private readonly WS_ENDPOINTS = {
marketData: 'ws://localhost:3001/ws',
riskGuardian: 'ws://localhost:3002/ws',
strategyOrchestrator: 'ws://localhost:3003/ws'
};
private connections = new Map<string, WebSocket>();
private messageSubjects = new Map<string, Subject<WebSocketMessage>>();
// Connection status signals
public isConnected = signal<boolean>(false);
public connectionStatus = signal<{ [key: string]: boolean }>({
marketData: false,
riskGuardian: false,
strategyOrchestrator: false
});
constructor() {
this.initializeConnections();
}
private initializeConnections() {
// Initialize WebSocket connections for all services
Object.entries(this.WS_ENDPOINTS).forEach(([service, url]) => {
this.connect(service, url);
});
}
private connect(serviceName: string, url: string) {
try {
const ws = new WebSocket(url);
const messageSubject = new Subject<WebSocketMessage>();
ws.onopen = () => {
console.log(`Connected to ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, true);
};
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
messageSubject.next(message);
} catch (error) {
console.error(`Failed to parse WebSocket message from ${serviceName}:`, error);
}
};
ws.onclose = () => {
console.log(`Disconnected from ${serviceName} WebSocket`);
this.updateConnectionStatus(serviceName, false);
// Attempt to reconnect after 5 seconds
setTimeout(() => {
this.connect(serviceName, url);
}, 5000);
};
ws.onerror = (error) => {
console.error(`WebSocket error for ${serviceName}:`, error);
this.updateConnectionStatus(serviceName, false);
};
this.connections.set(serviceName, ws);
this.messageSubjects.set(serviceName, messageSubject);
} catch (error) {
console.error(`Failed to connect to ${serviceName} WebSocket:`, error);
this.updateConnectionStatus(serviceName, false);
}
}
private updateConnectionStatus(serviceName: string, isConnected: boolean) {
const currentStatus = this.connectionStatus();
const newStatus = { ...currentStatus, [serviceName]: isConnected };
this.connectionStatus.set(newStatus);
// Update overall connection status
const overallConnected = Object.values(newStatus).some(status => status);
this.isConnected.set(overallConnected);
}
// Market Data Updates
getMarketDataUpdates(): Observable<MarketDataUpdate> {
const subject = this.messageSubjects.get('marketData');
if (!subject) {
throw new Error('Market data WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'market_data_update'),
map(message => message.data as MarketDataUpdate)
);
}
// Risk Alerts
getRiskAlerts(): Observable<RiskAlert> {
const subject = this.messageSubjects.get('riskGuardian');
if (!subject) {
throw new Error('Risk Guardian WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'risk_alert'),
map(message => message.data as RiskAlert)
);
}
// Strategy Updates
getStrategyUpdates(): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message => message.type === 'strategy_update'),
map(message => message.data)
);
}
// Strategy Signals
getStrategySignals(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message =>
message.type === 'strategy_signal' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// Strategy Trades
getStrategyTrades(strategyId?: string): Observable<any> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message =>
message.type === 'strategy_trade' &&
(!strategyId || message.data.strategyId === strategyId)
),
map(message => message.data)
);
}
// All strategy-related messages, useful for components that need all types
getAllStrategyMessages(): Observable<WebSocketMessage> {
const subject = this.messageSubjects.get('strategyOrchestrator');
if (!subject) {
throw new Error('Strategy Orchestrator WebSocket not initialized');
}
return subject.asObservable().pipe(
filter(message =>
message.type.startsWith('strategy_')
)
);
}
// Send messages
sendMessage(serviceName: string, message: any) {
const ws = this.connections.get(serviceName);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
} else {
console.warn(`Cannot send message to ${serviceName}: WebSocket not connected`);
}
}
// Cleanup
disconnect() {
this.connections.forEach((ws, serviceName) => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
this.connections.clear();
this.messageSubjects.clear();
}
}

View file

@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Trading Dashboard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View file

@ -1,6 +0,0 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig)
.catch((err) => console.error(err));

View file

@ -1,89 +0,0 @@
@import "tailwindcss";
/* Custom base styles */
html, body {
height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f9fafb;
}
/* Angular Material integration styles */
.mat-sidenav-container {
background-color: transparent;
}
.mat-sidenav {
border-radius: 0;
}
.mat-toolbar {
background-color: white;
color: #374151;
}
.mat-mdc-button.w-full {
width: 100%;
text-align: left;
justify-content: flex-start;
}
.mat-mdc-card {
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #e5e7eb;
}
.mat-mdc-chip.chip-green {
background-color: #dcfce7 !important;
color: #166534 !important;
}
.mat-mdc-chip.chip-blue {
background-color: #dbeafe !important;
color: #1e40af !important;
}
.mat-mdc-table {
border-radius: 8px;
overflow: hidden;
}
.mat-mdc-header-row {
background-color: #f9fafb;
}
.mat-mdc-row:hover {
background-color: #f9fafb;
}
/* Dark mode overrides */
.dark .mat-toolbar {
background-color: #1f2937;
color: #f9fafb;
}
.dark .mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #4b5563;
}
.dark .mat-mdc-header-row {
background-color: #1f2937;
}
.dark .mat-mdc-row:hover {
background-color: #374151;
}
.dark .mat-mdc-card {
background-color: #1f2937;
color: #f9fafb;
}
.dark .mat-mdc-table {
background-color: #1f2937;
color: #f9fafb;
}

View file

@ -1,52 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
danger: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
},
},
},
plugins: [],
}

View file

@ -1,15 +0,0 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}

View file

@ -1,32 +0,0 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View file

@ -1,14 +0,0 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}

View file

@ -1,244 +0,0 @@
/**
* Data Service - Combined live and historical data ingestion with queue-based architecture
*/
import { getLogger } from '@stock-bot/logger';
import { loadEnvVariables } from '@stock-bot/config';
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { queueManager } from './services/queue.service';
// Load environment variables
loadEnvVariables();
const app = new Hono();
const logger = getLogger('data-service');
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');
// Health check endpoint
app.get('/health', (c) => {
return c.json({
service: 'data-service',
status: 'healthy',
timestamp: new Date().toISOString(),
queue: {
status: 'running',
workers: queueManager.getWorkerCount()
}
});
});
// Queue management endpoints
app.get('/api/queue/status', async (c) => {
try {
const status = await queueManager.getQueueStatus();
return c.json({ status: 'success', data: status });
} catch (error) {
logger.error('Failed to get queue status', { error });
return c.json({ status: 'error', message: 'Failed to get queue status' }, 500);
}
});
app.post('/api/queue/job', async (c) => {
try {
const jobData = await c.req.json();
const job = await queueManager.addJob(jobData);
return c.json({ status: 'success', jobId: job.id });
} catch (error) {
logger.error('Failed to add job', { error });
return c.json({ status: 'error', message: 'Failed to add job' }, 500);
}
});
// Market data endpoints
app.get('/api/live/:symbol', async (c) => {
const symbol = c.req.param('symbol');
logger.info('Live data request', { symbol });
try { // Queue job for live data using Yahoo provider
const job = await queueManager.addJob({
type: 'market-data-live',
service: 'market-data',
provider: 'yahoo-finance',
operation: 'live-data',
payload: { symbol }
});
return c.json({
status: 'success',
message: 'Live data job queued',
jobId: job.id,
symbol
});
} catch (error) {
logger.error('Failed to queue live data job', { symbol, error });
return c.json({ status: 'error', message: 'Failed to queue live data job' }, 500);
}
});
app.get('/api/historical/:symbol', async (c) => {
const symbol = c.req.param('symbol');
const from = c.req.query('from');
const to = c.req.query('to');
logger.info('Historical data request', { symbol, from, to });
try {
const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 days ago
const toDate = to ? new Date(to) : new Date(); // Now
// Queue job for historical data using Yahoo provider
const job = await queueManager.addJob({
type: 'market-data-historical',
service: 'market-data',
provider: 'yahoo-finance',
operation: 'historical-data',
payload: {
symbol,
from: fromDate.toISOString(),
to: toDate.toISOString()
}
}); return c.json({
status: 'success',
message: 'Historical data job queued',
jobId: job.id,
symbol,
from: fromDate,
to: toDate
});
} catch (error) {
logger.error('Failed to queue historical data job', { symbol, from, to, error });
return c.json({ status: 'error', message: 'Failed to queue historical data job' }, 500); }
});
// Proxy management endpoints
app.post('/api/proxy/fetch', async (c) => {
try {
const job = await queueManager.addJob({
type: 'proxy-fetch',
service: 'proxy',
provider: 'proxy-service',
operation: 'fetch-and-check',
payload: {},
priority: 5
});
return c.json({
status: 'success',
jobId: job.id,
message: 'Proxy fetch job queued'
});
} catch (error) {
logger.error('Failed to queue proxy fetch', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy fetch' }, 500);
}
});
app.post('/api/proxy/check', async (c) => {
try {
const { proxies } = await c.req.json();
const job = await queueManager.addJob({
type: 'proxy-check',
service: 'proxy',
provider: 'proxy-service',
operation: 'check-specific',
payload: { proxies },
priority: 8
});
return c.json({
status: 'success',
jobId: job.id,
message: `Proxy check job queued for ${proxies.length} proxies`
});
} catch (error) {
logger.error('Failed to queue proxy check', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy check' }, 500);
}
});
// Get proxy stats via queue
app.get('/api/proxy/stats', async (c) => {
try {
const job = await queueManager.addJob({
type: 'proxy-stats',
service: 'proxy',
provider: 'proxy-service',
operation: 'get-stats',
payload: {},
priority: 3
});
return c.json({
status: 'success',
jobId: job.id,
message: 'Proxy stats job queued'
});
} catch (error) {
logger.error('Failed to queue proxy stats', { error });
return c.json({ status: 'error', message: 'Failed to queue proxy stats' }, 500);
}
});
// Provider registry endpoints
app.get('/api/providers', async (c) => {
try {
const providers = queueManager.getRegisteredProviders();
return c.json({ status: 'success', providers });
} catch (error) {
logger.error('Failed to get providers', { error });
return c.json({ status: 'error', message: 'Failed to get providers' }, 500);
}
});
// Add new endpoint to see scheduled jobs
app.get('/api/scheduled-jobs', async (c) => {
try {
const jobs = queueManager.getScheduledJobsInfo();
return c.json({
status: 'success',
count: jobs.length,
jobs
});
} catch (error) {
logger.error('Failed to get scheduled jobs info', { error });
return c.json({ status: 'error', message: 'Failed to get scheduled jobs' }, 500);
}
});
// Initialize services
async function initializeServices() {
logger.info('Initializing data service...');
try {
// Initialize queue service (Redis connections should be ready now)
logger.info('Starting queue service initialization...');
await queueManager.initialize();
logger.info('Queue service initialized');
logger.info('All services initialized successfully');
} catch (error) {
logger.error('Failed to initialize services', { error });
throw error;
}
}
// Start server
async function startServer() {
await initializeServices();
}
// Graceful shutdown
process.on('SIGINT', async () => {
logger.info('Received SIGINT, shutting down gracefully...');
await queueManager.shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('Received SIGTERM, shutting down gracefully...');
await queueManager.shutdown();
process.exit(0);
});
startServer().catch(error => {
logger.error('Failed to start server', { error });
process.exit(1);
});

View file

@ -1,140 +0,0 @@
import { ProxyInfo } from 'libs/http/src/types';
import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger';
import { BatchProcessor } from '../utils/batch-processor';
// Create logger for this provider
const logger = getLogger('proxy-provider');
// This will run at the same time each day as when the app started
const getEvery24HourCron = (): string => {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
return `${minutes} ${hours} * * *`; // Every day at startup time
};
export const proxyProvider: ProviderConfig = {
name: 'proxy-service',
service: 'proxy',
operations: {
'fetch-and-check': async (payload: { sources?: string[] }) => {
const { proxyService } = await import('./proxy.tasks');
const { queueManager } = await import('../services/queue.service');
const proxies = await proxyService.fetchProxiesFromSources();
if (proxies.length === 0) {
return { proxiesFetched: 0, jobsCreated: 0 };
}
const batchProcessor = new BatchProcessor(queueManager);
// Simplified configuration
const result = await batchProcessor.processItems({
items: proxies,
batchSize: parseInt(process.env.PROXY_BATCH_SIZE || '200'),
totalDelayMs: parseInt(process.env.PROXY_VALIDATION_HOURS || '4') * 60 * 60 * 1000 ,
jobNamePrefix: 'proxy',
operation: 'check-proxy',
service: 'proxy',
provider: 'proxy-service',
priority: 2,
useBatching: process.env.PROXY_DIRECT_MODE !== 'true', // Simple boolean flag
createJobData: (proxy: ProxyInfo) => ({
proxy,
source: 'fetch-and-check'
}),
removeOnComplete: 5,
removeOnFail: 3
});
return {
proxiesFetched: result.totalItems,
...result
};
},
'process-proxy-batch': async (payload: any) => {
// Process a batch of proxies - uses the fetch-and-check JobNamePrefix process-(proxy)-batch
const { queueManager } = await import('../services/queue.service');
const batchProcessor = new BatchProcessor(queueManager);
return await batchProcessor.processBatch(
payload,
(proxy: ProxyInfo) => ({
proxy,
source: payload.config?.source || 'batch-processing'
})
);
},
'check-proxy': async (payload: {
proxy: ProxyInfo,
source?: string,
batchIndex?: number,
itemIndex?: number,
total?: number
}) => {
const { checkProxy } = await import('./proxy.tasks');
try {
const result = await checkProxy(payload.proxy);
logger.debug('Proxy validated', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`,
isWorking: result.isWorking,
responseTime: result.responseTime,
batchIndex: payload.batchIndex
});
return {
result,
proxy: payload.proxy,
// Only include batch info if it exists (for batch mode)
...(payload.batchIndex !== undefined && {
batchInfo: {
batchIndex: payload.batchIndex,
itemIndex: payload.itemIndex,
total: payload.total,
source: payload.source
}
})
};
} catch (error) {
logger.warn('Proxy validation failed', {
proxy: `${payload.proxy.host}:${payload.proxy.port}`,
error: error instanceof Error ? error.message : String(error),
batchIndex: payload.batchIndex
});
return {
result: { isWorking: false, error: String(error) },
proxy: payload.proxy,
// Only include batch info if it exists (for batch mode)
...(payload.batchIndex !== undefined && {
batchInfo: {
batchIndex: payload.batchIndex,
itemIndex: payload.itemIndex,
total: payload.total,
source: payload.source
}
})
};
}
}
},
scheduledJobs: [
{
type: 'proxy-maintenance',
operation: 'fetch-and-check',
payload: {},
// should remove and just run at the same time so app restarts dont keeping adding same jobs
cronPattern: getEvery24HourCron(),
priority: 5,
immediately: true, // Don't run immediately during startup to avoid conflicts
description: 'Fetch and validate proxy list from sources'
}
]
};

View file

@ -1,365 +0,0 @@
import { getLogger } from '@stock-bot/logger';
import { createCache, type CacheProvider } from '@stock-bot/cache';
import { HttpClient, ProxyInfo } from '@stock-bot/http';
import pLimit from 'p-limit';
// Type definitions
export interface ProxySource {
id: string;
url: string;
protocol: string;
working?: number; // Optional, used for stats
total?: number; // Optional, used for stats
percentWorking?: number; // Optional, used for stats
lastChecked?: Date; // Optional, used for stats
}
// Shared configuration and utilities
const PROXY_CONFIG = {
CACHE_KEY: 'active',
CACHE_STATS_KEY: 'stats',
CACHE_TTL: 86400, // 24 hours
CHECK_TIMEOUT: 7000,
CHECK_IP: '99.246.102.205',
CHECK_URL: 'https://proxy-detection.stare.gg/?api_key=bd406bf53ddc6abe1d9de5907830a955',
CONCURRENCY_LIMIT: 100,
PROXY_SOURCES: [
{id: 'prxchk', url: 'https://raw.githubusercontent.com/prxchk/proxy-list/main/http.txt', protocol: 'http'},
{id: 'casals', url: 'https://raw.githubusercontent.com/casals-ar/proxy-list/main/http', protocol: 'http'},
{id: 'murong', url: 'https://raw.githubusercontent.com/MuRongPIG/Proxy-Master/main/http.txt', protocol: 'http'},
{id: 'vakhov-fresh', url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/master/http.txt', protocol: 'http'},
{id: 'sunny9577', url: 'https://raw.githubusercontent.com/sunny9577/proxy-scraper/master/proxies.txt', protocol: 'http'},
{id: 'kangproxy', url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/http/http.txt', protocol: 'http'},
{id: 'gfpcom', url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/http.txt', protocol: 'http'},
{id: 'dpangestuw', url: 'https://raw.githubusercontent.com/dpangestuw/Free-Proxy/refs/heads/main/http_proxies.txt', protocol: 'http'},
{id: 'gitrecon', url: 'https://raw.githubusercontent.com/gitrecon1455/fresh-proxy-list/refs/heads/main/proxylist.txt', protocol: 'http'},
{id: 'themiralay', url: 'https://raw.githubusercontent.com/themiralay/Proxy-List-World/refs/heads/master/data.txt', protocol: 'http'},
{id: 'vakhov-master', url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/http.txt', protocol: 'http'},
{id: 'casa-ls', url: 'https://raw.githubusercontent.com/casa-ls/proxy-list/refs/heads/main/http', protocol: 'http'},
{id: 'databay', url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/http.txt', protocol: 'http'},
{id: 'breaking-tech', url: 'https://raw.githubusercontent.com/BreakingTechFr/Proxy_Free/refs/heads/main/proxies/http.txt', protocol: 'http'},
{id: 'speedx', url: 'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt', protocol: 'http'},
{id: 'ercindedeoglu', url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/http.txt', protocol: 'http'},
{id: 'monosans', url: 'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt', protocol: 'http'},
{id: 'tuanminpay', url: 'https://raw.githubusercontent.com/TuanMinPay/live-proxy/master/http.txt', protocol: 'http'},
// {url: 'https://raw.githubusercontent.com/r00tee/Proxy-List/refs/heads/main/Https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/ErcinDedeoglu/proxies/main/proxies/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/vakhov/fresh-proxy-list/refs/heads/master/https.txt', protocol: 'https' },
// {url: 'https://raw.githubusercontent.com/databay-labs/free-proxy-list/refs/heads/master/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/officialputuid/KangProxy/refs/heads/KangProxy/https/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/zloi-user/hideip.me/refs/heads/master/https.txt',protocol: 'https', },
// {url: 'https://raw.githubusercontent.com/gfpcom/free-proxy-list/refs/heads/main/list/https.txt',protocol: 'https', },
]
};
// Shared instances (module-scoped, not global)
let logger: ReturnType<typeof getLogger>;
let cache: CacheProvider;
let httpClient: HttpClient;
let concurrencyLimit: ReturnType<typeof pLimit>;
let proxyStats: ProxySource[] = PROXY_CONFIG.PROXY_SOURCES.map(source => ({
id: source.id,
total: 0,
working: 0,
lastChecked: new Date(),
protocol: source.protocol,
url: source.url,
}));
// make a function that takes in source id and a boolean success and updates the proxyStats array
async function updateProxyStats(sourceId: string, success: boolean) {
const source = proxyStats.find(s => s.id === sourceId);
if (source !== undefined) {
if(typeof source.working !== 'number')
source.working = 0;
if(typeof source.total !== 'number')
source.total = 0;
source.total += 1;
if (success) {
source.working += 1;
}
source.percentWorking = source.working / source.total * 100;
source.lastChecked = new Date();
await cache.set(`${PROXY_CONFIG.CACHE_STATS_KEY}:${source.id}`, source, PROXY_CONFIG.CACHE_TTL);
return source;
} else {
logger.warn(`Unknown proxy source: ${sourceId}`);
}
}
// make a function that resets proxyStats
async function resetProxyStats(): Promise<void> {
proxyStats = PROXY_CONFIG.PROXY_SOURCES.map(source => ({
id: source.id,
total: 0,
working: 0,
lastChecked: new Date(),
protocol: source.protocol,
url: source.url,
}));
for (const source of proxyStats) {
await cache.set(`${PROXY_CONFIG.CACHE_STATS_KEY}:${source.id}`, source, PROXY_CONFIG.CACHE_TTL);
}
return Promise.resolve();
}
// Initialize shared resources
async function initializeSharedResources() {
if (!logger) {
logger = getLogger('proxy-tasks');
cache = createCache({
keyPrefix: 'proxy:',
ttl: PROXY_CONFIG.CACHE_TTL,
enableMetrics: true
});
// Always initialize httpClient and concurrencyLimit first
httpClient = new HttpClient({ timeout: 10000 }, logger);
concurrencyLimit = pLimit(PROXY_CONFIG.CONCURRENCY_LIMIT);
// Check if cache is ready, but don't block initialization
if (cache.isReady()) {
logger.info('Cache already ready');
} else {
logger.info('Cache not ready yet, tasks will use fallback mode');
// Try to wait briefly for cache to be ready, but don't block
cache.waitForReady(5000).then(() => {
logger.info('Cache became ready after initialization');
}).catch(error => {
logger.warn('Cache connection timeout, continuing with fallback mode:', {error: error.message});
});
}
logger.info('Proxy tasks initialized');
}
}
// Individual task functions
export async function queueProxyFetch(): Promise<string> {
await initializeSharedResources();
const { queueManager } = await import('../services/queue.service');
const job = await queueManager.addJob({
type: 'proxy-fetch',
service: 'proxy',
provider: 'proxy-service',
operation: 'fetch-and-check',
payload: {},
priority: 5
});
const jobId = job.id || 'unknown';
logger.info('Proxy fetch job queued', { jobId });
return jobId;
}
export async function queueProxyCheck(proxies: ProxyInfo[]): Promise<string> {
await initializeSharedResources();
const { queueManager } = await import('../services/queue.service');
const job = await queueManager.addJob({
type: 'proxy-check',
service: 'proxy',
provider: 'proxy-service',
operation: 'check-specific',
payload: { proxies },
priority: 3
});
const jobId = job.id || 'unknown';
logger.info('Proxy check job queued', { jobId, count: proxies.length });
return jobId;
}
export async function fetchProxiesFromSources(): Promise<ProxyInfo[]> {
await initializeSharedResources();
await resetProxyStats();
// Ensure concurrencyLimit is available before using it
if (!concurrencyLimit) {
logger.error('concurrencyLimit not initialized, using sequential processing');
const result = [];
for (const source of PROXY_CONFIG.PROXY_SOURCES) {
const proxies = await fetchProxiesFromSource(source);
result.push(...proxies);
}
let allProxies: ProxyInfo[] = result;
allProxies = removeDuplicateProxies(allProxies);
return allProxies;
}
const sources = PROXY_CONFIG.PROXY_SOURCES.map(source =>
concurrencyLimit(() => fetchProxiesFromSource(source))
);
const result = await Promise.all(sources);
let allProxies: ProxyInfo[] = result.flat();
allProxies = removeDuplicateProxies(allProxies);
// await checkProxies(allProxies);
return allProxies;
}
export async function fetchProxiesFromSource(source: ProxySource): Promise<ProxyInfo[]> {
await initializeSharedResources();
const allProxies: ProxyInfo[] = [];
try {
logger.info(`Fetching proxies from ${source.url}`);
const response = await httpClient.get(source.url, {
timeout: 10000
});
if (response.status !== 200) {
logger.warn(`Failed to fetch from ${source.url}: ${response.status}`);
return [];
}
const text = response.data;
const lines = text.split('\n').filter((line: string) => line.trim());
for (const line of lines) {
let trimmed = line.trim();
trimmed = cleanProxyUrl(trimmed);
if (!trimmed || trimmed.startsWith('#')) continue;
// Parse formats like "host:port" or "host:port:user:pass"
const parts = trimmed.split(':');
if (parts.length >= 2) {
const proxy: ProxyInfo = {
source: source.id,
protocol: source.protocol as 'http' | 'https' | 'socks4' | 'socks5',
host: parts[0],
port: parseInt(parts[1])
};
if (!isNaN(proxy.port) && proxy.host) {
allProxies.push(proxy);
}
}
}
logger.info(`Parsed ${allProxies.length} proxies from ${source.url}`);
} catch (error) {
logger.error(`Error fetching proxies from ${source.url}`, error);
return [];
}
return allProxies;
}
/**
* Check if a proxy is working
*/
export async function checkProxy(proxy: ProxyInfo): Promise<ProxyInfo> {
await initializeSharedResources();
let success = false;
logger.debug(`Checking Proxy:`, {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
});
try {
// Test the proxy
const response = await httpClient.get(PROXY_CONFIG.CHECK_URL, {
proxy,
timeout: PROXY_CONFIG.CHECK_TIMEOUT
});
const isWorking = response.status >= 200 && response.status < 300;
const result: ProxyInfo = {
...proxy,
isWorking,
checkedAt: new Date(),
responseTime: response.responseTime,
};
if (isWorking && !JSON.stringify(response.data).includes(PROXY_CONFIG.CHECK_IP)) {
success = true;
await cache.set(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`, result, PROXY_CONFIG.CACHE_TTL);
} else {
await cache.del(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`);
}
if( proxy.source ){
await updateProxyStats(proxy.source, success);
}
logger.debug('Proxy check completed', {
host: proxy.host,
port: proxy.port,
isWorking,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const result: ProxyInfo = {
...proxy,
isWorking: false,
error: errorMessage,
checkedAt: new Date()
};
// If the proxy check failed, remove it from cache - success is here cause i think abort signal fails sometimes
// if (!success) {
// await cache.set(`${PROXY_CONFIG.CACHE_KEY}:${proxy.protocol}://${proxy.host}:${proxy.port}`, result);
// }
if( proxy.source ){
await updateProxyStats(proxy.source, success);
}
logger.debug('Proxy check failed', {
host: proxy.host,
port: proxy.port,
error: errorMessage
});
return result;
}
}
// Utility functions
function cleanProxyUrl(url: string): string {
return url
.replace(/^https?:\/\//, '')
.replace(/^0+/, '')
.replace(/:0+(\d)/g, ':$1');
}
function removeDuplicateProxies(proxies: ProxyInfo[]): ProxyInfo[] {
const seen = new Set<string>();
const unique: ProxyInfo[] = [];
for (const proxy of proxies) {
const key = `${proxy.protocol}://${proxy.host}:${proxy.port}`;
if (!seen.has(key)) {
seen.add(key);
unique.push(proxy);
}
}
return unique;
}
// Optional: Export a convenience object that groups related tasks
export const proxyTasks = {
queueProxyFetch,
queueProxyCheck,
fetchProxiesFromSources,
fetchProxiesFromSource,
checkProxy,
};
// Export singleton instance for backward compatibility (optional)
// Remove this if you want to fully move to the task-based approach
export const proxyService = proxyTasks;

View file

@ -1,175 +0,0 @@
import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('quotemedia-provider');
export const quotemediaProvider: ProviderConfig = {
name: 'quotemedia',
service: 'market-data',
operations: { 'live-data': async (payload: { symbol: string; fields?: string[] }) => {
logger.info('Fetching live data from QuoteMedia', { symbol: payload.symbol });
// Simulate QuoteMedia API call
const mockData = {
symbol: payload.symbol,
price: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
change: (Math.random() - 0.5) * 20,
changePercent: (Math.random() - 0.5) * 5,
timestamp: new Date().toISOString(),
source: 'quotemedia',
fields: payload.fields || ['price', 'volume', 'change']
};
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200));
return mockData;
},
'historical-data': async (payload: {
symbol: string;
from: Date;
to: Date;
interval?: string;
fields?: string[]; }) => {
logger.info('Fetching historical data from QuoteMedia', {
symbol: payload.symbol,
from: payload.from,
to: payload.to,
interval: payload.interval || '1d'
});
// Generate mock historical data
const days = Math.ceil((payload.to.getTime() - payload.from.getTime()) / (1000 * 60 * 60 * 24));
const data = [];
for (let i = 0; i < Math.min(days, 100); i++) {
const date = new Date(payload.from.getTime() + i * 24 * 60 * 60 * 1000);
data.push({
date: date.toISOString().split('T')[0],
open: Math.random() * 1000 + 100,
high: Math.random() * 1000 + 100,
low: Math.random() * 1000 + 100,
close: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
source: 'quotemedia'
});
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
return {
symbol: payload.symbol,
interval: payload.interval || '1d',
data,
source: 'quotemedia',
totalRecords: data.length
};
},
'batch-quotes': async (payload: { symbols: string[]; fields?: string[] }) => {
logger.info('Fetching batch quotes from QuoteMedia', {
symbols: payload.symbols,
count: payload.symbols.length
});
const quotes = payload.symbols.map(symbol => ({
symbol,
price: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
change: (Math.random() - 0.5) * 20,
timestamp: new Date().toISOString(),
source: 'quotemedia'
}));
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200));
return {
quotes,
source: 'quotemedia',
timestamp: new Date().toISOString(),
totalSymbols: payload.symbols.length
};
}, 'company-profile': async (payload: { symbol: string }) => {
logger.info('Fetching company profile from QuoteMedia', { symbol: payload.symbol });
// Simulate company profile data
const profile = {
symbol: payload.symbol,
companyName: `${payload.symbol} Corporation`,
sector: 'Technology',
industry: 'Software',
description: `${payload.symbol} is a leading technology company.`,
marketCap: Math.floor(Math.random() * 1000000000000),
employees: Math.floor(Math.random() * 100000),
website: `https://www.${payload.symbol.toLowerCase()}.com`,
source: 'quotemedia'
};
await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 100));
return profile;
}, 'options-chain': async (payload: { symbol: string; expiration?: string }) => {
logger.info('Fetching options chain from QuoteMedia', {
symbol: payload.symbol,
expiration: payload.expiration
});
// Generate mock options data
const strikes = Array.from({ length: 20 }, (_, i) => 100 + i * 5);
const calls = strikes.map(strike => ({
strike,
bid: Math.random() * 10,
ask: Math.random() * 10 + 0.5,
volume: Math.floor(Math.random() * 1000),
openInterest: Math.floor(Math.random() * 5000)
}));
const puts = strikes.map(strike => ({
strike,
bid: Math.random() * 10,
ask: Math.random() * 10 + 0.5,
volume: Math.floor(Math.random() * 1000),
openInterest: Math.floor(Math.random() * 5000)
}));
await new Promise(resolve => setTimeout(resolve, 400 + Math.random() * 300));
return {
symbol: payload.symbol,
expiration: payload.expiration || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
calls,
puts,
source: 'quotemedia'
};
}
},
scheduledJobs: [
// {
// type: 'quotemedia-premium-refresh',
// operation: 'batch-quotes',
// payload: { symbols: ['AAPL', 'GOOGL', 'MSFT'] },
// cronPattern: '*/2 * * * *', // Every 2 minutes
// priority: 7,
// description: 'Refresh premium quotes with detailed market data'
// },
// {
// type: 'quotemedia-options-update',
// operation: 'options-chain',
// payload: { symbol: 'SPY' },
// cronPattern: '*/10 * * * *', // Every 10 minutes
// priority: 5,
// description: 'Update options chain data for SPY ETF'
// },
// {
// type: 'quotemedia-profiles',
// operation: 'company-profile',
// payload: { symbol: 'AAPL' },
// cronPattern: '0 9 * * 1-5', // Weekdays at 9 AM
// priority: 3,
// description: 'Update company profile data'
// }
]
};

View file

@ -1,249 +0,0 @@
import { ProviderConfig } from '../services/provider-registry.service';
import { getLogger } from '@stock-bot/logger';
const logger = getLogger('yahoo-provider');
export const yahooProvider: ProviderConfig = {
name: 'yahoo-finance',
service: 'market-data',
operations: {
'live-data': async (payload: { symbol: string; modules?: string[] }) => {
logger.info('Fetching live data from Yahoo Finance', { symbol: payload.symbol });
// Simulate Yahoo Finance API call
const mockData = {
symbol: payload.symbol,
regularMarketPrice: Math.random() * 1000 + 100,
regularMarketVolume: Math.floor(Math.random() * 1000000),
regularMarketChange: (Math.random() - 0.5) * 20,
regularMarketChangePercent: (Math.random() - 0.5) * 5,
preMarketPrice: Math.random() * 1000 + 100,
postMarketPrice: Math.random() * 1000 + 100,
marketCap: Math.floor(Math.random() * 1000000000000),
peRatio: Math.random() * 50 + 5,
dividendYield: Math.random() * 0.1,
fiftyTwoWeekHigh: Math.random() * 1200 + 100,
fiftyTwoWeekLow: Math.random() * 800 + 50,
timestamp: Date.now() / 1000,
source: 'yahoo-finance',
modules: payload.modules || ['price', 'summaryDetail']
};
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 150 + Math.random() * 250));
return mockData;
},
'historical-data': async (payload: {
symbol: string;
period1: number;
period2: number;
interval?: string;
events?: string; }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching historical data from Yahoo Finance', {
symbol: payload.symbol,
period1: payload.period1,
period2: payload.period2,
interval: payload.interval || '1d'
});
// Generate mock historical data
const days = Math.ceil((payload.period2 - payload.period1) / (24 * 60 * 60));
const data = [];
for (let i = 0; i < Math.min(days, 100); i++) {
const timestamp = payload.period1 + i * 24 * 60 * 60;
data.push({
timestamp,
date: new Date(timestamp * 1000).toISOString().split('T')[0],
open: Math.random() * 1000 + 100,
high: Math.random() * 1000 + 100,
low: Math.random() * 1000 + 100,
close: Math.random() * 1000 + 100,
adjClose: Math.random() * 1000 + 100,
volume: Math.floor(Math.random() * 1000000),
source: 'yahoo-finance'
});
}
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 250 + Math.random() * 350));
return {
symbol: payload.symbol,
interval: payload.interval || '1d',
timestamps: data.map(d => d.timestamp),
indicators: {
quote: [{
open: data.map(d => d.open),
high: data.map(d => d.high),
low: data.map(d => d.low),
close: data.map(d => d.close),
volume: data.map(d => d.volume)
}],
adjclose: [{
adjclose: data.map(d => d.adjClose)
}]
},
source: 'yahoo-finance',
totalRecords: data.length
};
},
'search': async (payload: { query: string; quotesCount?: number; newsCount?: number }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Searching Yahoo Finance', { query: payload.query });
// Generate mock search results
const quotes = Array.from({ length: payload.quotesCount || 5 }, (_, i) => ({
symbol: `${payload.query.toUpperCase()}${i}`,
shortname: `${payload.query} Company ${i}`,
longname: `${payload.query} Corporation ${i}`,
exchDisp: 'NASDAQ',
typeDisp: 'Equity',
source: 'yahoo-finance'
}));
const news = Array.from({ length: payload.newsCount || 3 }, (_, i) => ({
uuid: `news-${i}-${Date.now()}`,
title: `${payload.query} News Article ${i}`,
publisher: 'Financial News',
providerPublishTime: Date.now() - i * 3600000,
type: 'STORY',
source: 'yahoo-finance'
}));
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 200));
return {
quotes,
news,
totalQuotes: quotes.length,
totalNews: news.length,
source: 'yahoo-finance'
};
}, 'financials': async (payload: { symbol: string; type?: 'income' | 'balance' | 'cash' }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching financials from Yahoo Finance', {
symbol: payload.symbol,
type: payload.type || 'income'
});
// Generate mock financial data
const financials = {
symbol: payload.symbol,
type: payload.type || 'income',
currency: 'USD',
annual: Array.from({ length: 4 }, (_, i) => ({
fiscalYear: 2024 - i,
revenue: Math.floor(Math.random() * 100000000000),
netIncome: Math.floor(Math.random() * 10000000000),
totalAssets: Math.floor(Math.random() * 500000000000),
totalDebt: Math.floor(Math.random() * 50000000000)
})),
quarterly: Array.from({ length: 4 }, (_, i) => ({
fiscalQuarter: `Q${4-i} 2024`,
revenue: Math.floor(Math.random() * 25000000000),
netIncome: Math.floor(Math.random() * 2500000000)
})),
source: 'yahoo-finance'
};
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 200));
return financials;
}, 'earnings': async (payload: { symbol: string; period?: 'annual' | 'quarterly' }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching earnings from Yahoo Finance', {
symbol: payload.symbol,
period: payload.period || 'quarterly'
});
// Generate mock earnings data
const earnings = {
symbol: payload.symbol,
period: payload.period || 'quarterly',
earnings: Array.from({ length: 8 }, (_, i) => ({
quarter: `Q${(i % 4) + 1} ${2024 - Math.floor(i/4)}`,
epsEstimate: Math.random() * 5,
epsActual: Math.random() * 5,
revenueEstimate: Math.floor(Math.random() * 50000000000),
revenueActual: Math.floor(Math.random() * 50000000000),
surprise: (Math.random() - 0.5) * 2
})),
source: 'yahoo-finance'
};
await new Promise(resolve => setTimeout(resolve, 250 + Math.random() * 150));
return earnings;
}, 'recommendations': async (payload: { symbol: string }) => {
const { getLogger } = await import('@stock-bot/logger');
const logger = getLogger('yahoo-provider');
logger.info('Fetching recommendations from Yahoo Finance', { symbol: payload.symbol });
// Generate mock recommendations
const recommendations = {
symbol: payload.symbol,
current: {
strongBuy: Math.floor(Math.random() * 10),
buy: Math.floor(Math.random() * 15),
hold: Math.floor(Math.random() * 20),
sell: Math.floor(Math.random() * 5),
strongSell: Math.floor(Math.random() * 3)
},
trend: Array.from({ length: 4 }, (_, i) => ({
period: `${i}m`,
strongBuy: Math.floor(Math.random() * 10),
buy: Math.floor(Math.random() * 15),
hold: Math.floor(Math.random() * 20),
sell: Math.floor(Math.random() * 5),
strongSell: Math.floor(Math.random() * 3)
})),
source: 'yahoo-finance'
};
await new Promise(resolve => setTimeout(resolve, 180 + Math.random() * 120));
return recommendations;
}
},
scheduledJobs: [
// {
// type: 'yahoo-market-refresh',
// operation: 'live-data',
// payload: { symbol: 'AAPL' },
// cronPattern: '*/1 * * * *', // Every minute
// priority: 8,
// description: 'Refresh Apple stock price from Yahoo Finance'
// },
// {
// type: 'yahoo-sp500-update',
// operation: 'live-data',
// payload: { symbol: 'SPY' },
// cronPattern: '*/2 * * * *', // Every 2 minutes
// priority: 9,
// description: 'Update S&P 500 ETF price'
// },
// {
// type: 'yahoo-earnings-check',
// operation: 'earnings',
// payload: { symbol: 'AAPL' },
// cronPattern: '0 16 * * 1-5', // Weekdays at 4 PM (market close)
// priority: 6,
// description: 'Check earnings data for Apple'
// }
]
};

View file

@ -1,115 +0,0 @@
import { getLogger } from '@stock-bot/logger';
export interface JobHandler {
(payload: any): Promise<any>;
}
export interface ScheduledJob {
type: string;
operation: string;
payload: any;
cronPattern: string;
priority?: number;
description?: string;
immediately?: boolean;
}
export interface ProviderConfig {
name: string;
service: string;
operations: Record<string, JobHandler>;
scheduledJobs?: ScheduledJob[];
}
export class ProviderRegistry {
private logger = getLogger('provider-registry');
private providers = new Map<string, ProviderConfig>();
/**
* Register a provider with its operations
*/ registerProvider(config: ProviderConfig): void {
const key = `${config.service}:${config.name}`;
this.providers.set(key, config);
this.logger.info(`Registered provider: ${key}`, {
operations: Object.keys(config.operations),
scheduledJobs: config.scheduledJobs?.length || 0
});
}
/**
* Get a job handler for a specific provider and operation
*/
getHandler(service: string, provider: string, operation: string): JobHandler | null {
const key = `${service}:${provider}`;
const providerConfig = this.providers.get(key);
if (!providerConfig) {
this.logger.warn(`Provider not found: ${key}`);
return null;
}
const handler = providerConfig.operations[operation];
if (!handler) {
this.logger.warn(`Operation not found: ${operation} in provider ${key}`);
return null;
}
return handler;
}
/**
* Get all registered providers
*/
getAllScheduledJobs(): Array<{
service: string;
provider: string;
job: ScheduledJob;
}> {
const allJobs: Array<{ service: string; provider: string; job: ScheduledJob }> = [];
for (const [key, config] of this.providers) {
if (config.scheduledJobs) {
for (const job of config.scheduledJobs) {
allJobs.push({
service: config.service,
provider: config.name,
job
});
}
}
}
return allJobs;
}
getProviders(): Array<{ key: string; config: ProviderConfig }> {
return Array.from(this.providers.entries()).map(([key, config]) => ({
key,
config
}));
}
/**
* Check if a provider exists
*/
hasProvider(service: string, provider: string): boolean {
return this.providers.has(`${service}:${provider}`);
}
/**
* Get providers by service type
*/
getProvidersByService(service: string): ProviderConfig[] {
return Array.from(this.providers.values()).filter(provider => provider.service === service);
}
/**
* Clear all providers (useful for testing)
*/
clear(): void {
this.providers.clear();
this.logger.info('All providers cleared');
}
}
export const providerRegistry = new ProviderRegistry();

View file

@ -1,478 +0,0 @@
import { Queue, Worker, QueueEvents } from 'bullmq';
import { getLogger } from '@stock-bot/logger';
import { providerRegistry } from './provider-registry.service';
export interface JobData {
type: string;
service: string;
provider: string;
operation: string;
payload: any;
priority?: number;
immediately?: boolean;
}
export class QueueService {
private logger = getLogger('queue-service');
private queue!: Queue;
private workers: Worker[] = [];
private queueEvents!: QueueEvents;
private isInitialized = false;
constructor() {
// Don't initialize in constructor to allow for proper async initialization
}
async initialize() {
if (this.isInitialized) {
this.logger.warn('Queue service already initialized');
return;
}
this.logger.info('Initializing queue service...');
// Register all providers first
await this.registerProviders();
const connection = {
host: process.env.DRAGONFLY_HOST || 'localhost',
port: parseInt(process.env.DRAGONFLY_PORT || '6379'),
// Add these Redis-specific options to fix the undeclared key issue
maxRetriesPerRequest: null,
retryDelayOnFailover: 100,
enableReadyCheck: false,
lazyConnect: false,
// Disable Redis Cluster mode if you're using standalone Redis/Dragonfly
enableOfflineQueue: true
};
// Worker configuration
const workerCount = parseInt(process.env.WORKER_COUNT || '5');
const concurrencyPerWorker = parseInt(process.env.WORKER_CONCURRENCY || '20');
this.logger.info('Connecting to Redis/Dragonfly', connection);
try {
this.queue = new Queue('{data-service-queue}', {
connection,
defaultJobOptions: {
removeOnComplete: 10,
removeOnFail: 5,
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
}
}
});
// Create multiple workers
for (let i = 0; i < workerCount; i++) {
const worker = new Worker(
'{data-service-queue}',
this.processJob.bind(this),
{
connection: { ...connection }, // Each worker gets its own connection
concurrency: concurrencyPerWorker,
maxStalledCount: 1,
stalledInterval: 30000,
}
);
// Add worker-specific logging
worker.on('ready', () => {
this.logger.info(`Worker ${i + 1} ready`, { workerId: i + 1 });
});
worker.on('error', (error) => {
this.logger.error(`Worker ${i + 1} error`, { workerId: i + 1, error });
});
this.workers.push(worker);
}
this.queueEvents = new QueueEvents('{data-service-queue}', { connection }); // Test connection
// Wait for all workers to be ready
await this.queue.waitUntilReady();
await Promise.all(this.workers.map(worker => worker.waitUntilReady()));
await this.queueEvents.waitUntilReady();
this.setupEventListeners();
this.isInitialized = true;
this.logger.info('Queue service initialized successfully');
await this.setupScheduledTasks();
} catch (error) {
this.logger.error('Failed to initialize queue service', { error });
throw error;
}
}
// Update getTotalConcurrency method
getTotalConcurrency() {
if (!this.isInitialized) {
return 0;
}
return this.workers.reduce((total, worker) => {
return total + (worker.opts.concurrency || 1);
}, 0);
}
private async registerProviders() {
this.logger.info('Registering providers...');
try {
// Import and register all providers
const { proxyProvider } = await import('../providers/proxy.provider');
const { quotemediaProvider } = await import('../providers/quotemedia.provider');
const { yahooProvider } = await import('../providers/yahoo.provider');
providerRegistry.registerProvider(proxyProvider);
providerRegistry.registerProvider(quotemediaProvider);
providerRegistry.registerProvider(yahooProvider);
this.logger.info('All providers registered successfully');
} catch (error) {
this.logger.error('Failed to register providers', { error });
throw error;
}
}
private async processJob(job: any) {
const { service, provider, operation, payload }: JobData = job.data;
this.logger.info('Processing job', {
id: job.id,
service,
provider,
operation,
payloadKeys: Object.keys(payload || {})
});
try {
// Get handler from registry
const handler = providerRegistry.getHandler(service, provider, operation);
if (!handler) {
throw new Error(`No handler found for ${service}:${provider}:${operation}`);
}
// Execute the handler
const result = await handler(payload);
this.logger.info('Job completed successfully', {
id: job.id,
service,
provider,
operation
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error('Job failed', {
id: job.id,
service,
provider,
operation,
error: errorMessage
});
throw error;
}
}
async addBulk(jobs: any[]) : Promise<any[]> {
return await this.queue.addBulk(jobs)
}
private setupEventListeners() {
this.queueEvents.on('completed', (job) => {
this.logger.info('Job completed', { id: job.jobId });
});
this.queueEvents.on('failed', (job) => {
this.logger.error('Job failed', { id: job.jobId, error: job.failedReason });
});
// Note: Worker-specific events are already set up during worker creation
// No need for additional progress events since we handle them per-worker
}
private async setupScheduledTasks() {
try {
this.logger.info('Setting up scheduled tasks from providers...');
// Get all scheduled jobs from all providers
const allScheduledJobs = providerRegistry.getAllScheduledJobs();
if (allScheduledJobs.length === 0) {
this.logger.warn('No scheduled jobs found in providers');
return;
}
// Get existing repeatable jobs for comparison
const existingJobs = await this.queue.getRepeatableJobs();
this.logger.info(`Found ${existingJobs.length} existing repeatable jobs`);
let successCount = 0;
let failureCount = 0;
let updatedCount = 0;
let newCount = 0;
// Process each scheduled job
for (const { service, provider, job } of allScheduledJobs) {
try {
const jobKey = `${service}-${provider}-${job.operation}`;
// Check if this job already exists
const existingJob = existingJobs.find(existing =>
existing.key?.includes(jobKey) || existing.name === job.type
);
if (existingJob) {
// Check if the job needs updating (different cron pattern or config)
const needsUpdate = existingJob.pattern !== job.cronPattern;
if (needsUpdate) {
this.logger.info('Job configuration changed, updating', {
jobKey,
oldPattern: existingJob.pattern,
newPattern: job.cronPattern
});
updatedCount++;
} else {
this.logger.debug('Job unchanged, skipping', { jobKey });
successCount++;
continue;
}
} else {
newCount++;
}
// Add delay between job registrations
await new Promise(resolve => setTimeout(resolve, 100));
await this.addRecurringJob({
type: job.type,
service: service,
provider: provider,
operation: job.operation,
payload: job.payload,
priority: job.priority,
immediately: job.immediately || false
}, job.cronPattern);
this.logger.info('Scheduled job registered', {
type: job.type,
service,
provider,
operation: job.operation,
cronPattern: job.cronPattern,
description: job.description,
immediately: job.immediately || false
});
successCount++;
} catch (error) {
this.logger.error('Failed to register scheduled job', {
type: job.type,
service,
provider,
error: error instanceof Error ? error.message : String(error)
});
failureCount++;
}
}
this.logger.info(`Scheduled tasks setup complete`, {
total: allScheduledJobs.length,
successful: successCount,
failed: failureCount,
updated: updatedCount,
new: newCount
});
} catch (error) {
this.logger.error('Failed to setup scheduled tasks', error);
}
}
async addJob(jobData: JobData, options?: any) {
if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.');
}
return this.queue.add(jobData.type, jobData, {
priority: jobData.priority || 0,
removeOnComplete: 10,
removeOnFail: 5,
...options
});
}
async addRecurringJob(jobData: JobData, cronPattern: string, options?: any) {
if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.');
}
try {
// Create a unique job key for this specific job
const jobKey = `${jobData.service}-${jobData.provider}-${jobData.operation}`;
// Get all existing repeatable jobs
const existingJobs = await this.queue.getRepeatableJobs();
// Find and remove the existing job with the same key if it exists
const existingJob = existingJobs.find(job => {
// Check if this is the same job by comparing key components
return job.key?.includes(jobKey) || job.name === jobData.type;
});
if (existingJob) {
this.logger.info('Updating existing recurring job', {
jobKey,
existingPattern: existingJob.pattern,
newPattern: cronPattern
});
// Remove the existing job
await this.queue.removeRepeatableByKey(existingJob.key);
// Small delay to ensure cleanup is complete
await new Promise(resolve => setTimeout(resolve, 100));
} else {
this.logger.info('Creating new recurring job', { jobKey, cronPattern });
}
// Add the new/updated recurring job
const job = await this.queue.add(jobData.type, jobData, {
repeat: {
pattern: cronPattern,
tz: 'UTC',
immediately: jobData.immediately || false,
},
// Use a consistent jobId for this specific recurring job
jobId: `recurring-${jobKey}`,
removeOnComplete: 1,
removeOnFail: 1,
attempts: 2,
backoff: {
type: 'fixed',
delay: 5000
},
...options
});
this.logger.info('Recurring job added/updated successfully', {
jobKey,
type: jobData.type,
cronPattern,
immediately: jobData.immediately || false
});
return job;
} catch (error) {
this.logger.error('Failed to add/update recurring job', {
jobData,
cronPattern,
error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
async getJobStats() {
if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.');
}
const [waiting, active, completed, failed, delayed] = await Promise.all([
this.queue.getWaiting(),
this.queue.getActive(),
this.queue.getCompleted(),
this.queue.getFailed(),
this.queue.getDelayed()
]);
return {
waiting: waiting.length,
active: active.length,
completed: completed.length,
failed: failed.length,
delayed: delayed.length
};
}
async drainQueue() {
if (!this.isInitialized) {
await this.queue.drain()
}
}
async getQueueStatus() {
if (!this.isInitialized) {
throw new Error('Queue service not initialized. Call initialize() first.');
}
const stats = await this.getJobStats();
return {
...stats,
workers: this.getWorkerCount(),
totalConcurrency: this.getTotalConcurrency(),
queue: this.queue.name,
connection: {
host: process.env.DRAGONFLY_HOST || 'localhost',
port: parseInt(process.env.DRAGONFLY_PORT || '6379')
}
};
}
getWorkerCount() {
if (!this.isInitialized) {
return 0;
}
return this.workers.length;
}
getRegisteredProviders() {
return providerRegistry.getProviders().map(({ key, config }) => ({
key,
name: config.name,
service: config.service,
operations: Object.keys(config.operations),
scheduledJobs: config.scheduledJobs?.length || 0
}));
}
getScheduledJobsInfo() {
return providerRegistry.getAllScheduledJobs().map(({ service, provider, job }) => ({
id: `${service}-${provider}-${job.type}`,
service,
provider,
type: job.type,
operation: job.operation,
cronPattern: job.cronPattern,
priority: job.priority,
description: job.description,
immediately: job.immediately || false
}));
}
async shutdown() {
if (!this.isInitialized) {
this.logger.warn('Queue service not initialized, nothing to shutdown');
return;
}
this.logger.info('Shutting down queue service');
// Close all workers
this.logger.info(`Closing ${this.workers.length} workers...`);
await Promise.all(this.workers.map((worker, index) => {
this.logger.debug(`Closing worker ${index + 1}`);
return worker.close();
}));
await this.queue.close();
await this.queueEvents.close();
this.isInitialized = false;
this.logger.info('Queue service shutdown complete');
}
}
export const queueManager = new QueueService();

View file

@ -1,545 +0,0 @@
import { getLogger } from '@stock-bot/logger';
import { createCache, CacheProvider } from '@stock-bot/cache';
export interface BatchConfig<T> {
items: T[];
batchSize?: number; // Optional - only used for batch mode
totalDelayMs: number;
jobNamePrefix: string;
operation: string;
service: string;
provider: string;
priority?: number;
createJobData: (item: T, index: number) => any;
removeOnComplete?: number;
removeOnFail?: number;
useBatching?: boolean; // Simple flag to choose mode
payloadTtlHours?: number; // TTL for stored payloads (default 24 hours)
}
const logger = getLogger('batch-processor');
export class BatchProcessor {
private cacheProvider: CacheProvider;
private isReady = false;
private keyPrefix: string = 'batch:'; // Default key prefix for batch payloads
constructor(
private queueManager: any,
private cacheOptions?: { keyPrefix?: string; ttl?: number } // Optional cache configuration
) {
this.keyPrefix = cacheOptions?.keyPrefix || 'batch:'; // Initialize cache provider with batch-specific settings
this.cacheProvider = createCache({
keyPrefix: this.keyPrefix,
ttl: cacheOptions?.ttl || 86400 * 2, // 48 hours default
enableMetrics: true
});
this.initialize();
}
/**
* Initialize the batch processor and wait for cache to be ready
*/
async initialize(timeout: number = 10000): Promise<void> {
if (this.isReady) {
logger.warn('BatchProcessor already initialized');
return;
}
logger.info('Initializing BatchProcessor, waiting for cache to be ready...');
try {
await this.cacheProvider.waitForReady(timeout);
this.isReady = true;
logger.info('BatchProcessor initialized successfully', {
cacheReady: this.cacheProvider.isReady(),
keyPrefix: this.keyPrefix,
ttlHours: ((this.cacheOptions?.ttl || 86400 * 2) / 3600).toFixed(1)
});
} catch (error) {
logger.warn('BatchProcessor cache not ready within timeout, continuing with fallback mode', {
error: error instanceof Error ? error.message : String(error),
timeout
});
// Don't throw - mark as ready anyway and let cache operations use their fallback mechanisms
this.isReady = true;
}
}
/**
* Check if the batch processor is ready
*/
getReadyStatus(): boolean {
return this.isReady; // Don't require cache to be ready, let individual operations handle fallbacks
}
/**
* Generate a unique key for storing batch payload in Redis
* Note: The cache provider will add its keyPrefix ('batch:') automatically
*/
private generatePayloadKey(jobNamePrefix: string, batchIndex: number): string {
return `payload:${jobNamePrefix}:${batchIndex}:${Date.now()}`;
}/**
* Store batch payload in Redis and return the key
*/ private async storeBatchPayload<T>(
items: T[],
config: BatchConfig<T>,
batchIndex: number
): Promise<string> {
const payloadKey = this.generatePayloadKey(config.jobNamePrefix, batchIndex);
const payload = {
items,
batchIndex,
config: {
...config,
items: undefined // Don't store items twice
},
createdAt: new Date().toISOString()
};
const ttlSeconds = (config.payloadTtlHours || 24) * 60 * 60;
try {
await this.cacheProvider.set(
payloadKey,
JSON.stringify(payload),
ttlSeconds
);
logger.info('Stored batch payload in Redis', {
payloadKey,
itemCount: items.length,
batchIndex,
ttlHours: config.payloadTtlHours || 24
});
} catch (error) {
logger.error('Failed to store batch payload, job will run without caching', {
payloadKey,
error: error instanceof Error ? error.message : String(error)
});
// Don't throw - the job can still run, just without the cached payload
}
return payloadKey;
}/**
* Load batch payload from Redis
*/
private async loadBatchPayload<T>(payloadKey: string): Promise<{
items: T[];
batchIndex: number;
config: BatchConfig<T>;
} | null> {
// Auto-initialize if not ready
if (!this.cacheProvider.isReady() || !this.isReady) {
logger.info('Cache provider not ready, initializing...', { payloadKey });
try {
await this.initialize();
} catch (error) {
logger.error('Failed to initialize cache provider for loading', {
payloadKey,
error: error instanceof Error ? error.message : String(error)
});
throw new Error('Cache provider initialization failed - cannot load batch payload');
}
}
try {
const payloadData = await this.cacheProvider.get<any>(payloadKey);
if (!payloadData) {
logger.error('Batch payload not found in Redis', { payloadKey });
throw new Error('Batch payload not found in Redis');
}
// Handle both string and already-parsed object
let payload;
if (typeof payloadData === 'string') {
payload = JSON.parse(payloadData);
} else {
// Already parsed by cache provider
payload = payloadData;
}
logger.info('Loaded batch payload from Redis', {
payloadKey,
itemCount: payload.items?.length || 0,
batchIndex: payload.batchIndex
});
return payload;
} catch (error) {
logger.error('Failed to load batch payload from Redis', {
payloadKey,
error: error instanceof Error ? error.message : String(error)
});
throw new Error('Failed to load batch payload from Redis');
}
}
/**
* Unified method that handles both direct and batch approaches
*/
async processItems<T>(config: BatchConfig<T>) {
// Check if BatchProcessor is ready
if (!this.getReadyStatus()) {
logger.warn('BatchProcessor not ready, attempting to initialize...');
await this.initialize();
}
const { items, useBatching = false } = config;
if (items.length === 0) {
return { totalItems: 0, jobsCreated: 0 };
} // Final readiness check - wait briefly for cache to be ready
if (!this.cacheProvider.isReady()) {
logger.warn('Cache provider not ready, waiting briefly...');
try {
await this.cacheProvider.waitForReady(10000); // Wait up to 10 seconds
logger.info('Cache provider became ready');
} catch (error) {
logger.warn('Cache provider still not ready, continuing with fallback mode');
// Don't throw error - let the cache operations use their fallback mechanisms
}
}
logger.info('Starting item processing', {
totalItems: items.length,
mode: useBatching ? 'batch' : 'direct',
cacheReady: this.cacheProvider.isReady()
});
if (useBatching) {
return await this.createBatchJobs(config);
} else {
return await this.createDirectJobs(config);
}
}
private async createDirectJobs<T>(config: BatchConfig<T>) {
const {
items,
totalDelayMs,
jobNamePrefix,
operation,
service,
provider,
priority = 2,
createJobData,
removeOnComplete = 5,
removeOnFail = 3
} = config;
const delayPerItem = Math.floor(totalDelayMs / items.length);
const chunkSize = 100;
let totalJobsCreated = 0;
logger.info('Creating direct jobs', {
totalItems: items.length,
delayPerItem: `${(delayPerItem / 1000).toFixed(1)}s`,
estimatedDuration: `${(totalDelayMs / 1000 / 60 / 60).toFixed(1)} hours`
});
// Process in chunks to avoid overwhelming Redis
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
const jobs = chunk.map((item, chunkIndex) => {
const globalIndex = i + chunkIndex;
return {
name: `${jobNamePrefix}-processing`,
data: {
type: `${jobNamePrefix}-processing`,
service,
provider,
operation,
payload: createJobData(item, globalIndex),
priority
},
opts: {
delay: globalIndex * delayPerItem,
jobId: `${jobNamePrefix}:${globalIndex}:${Date.now()}`,
removeOnComplete,
removeOnFail
}
};
});
try {
const createdJobs = await this.queueManager.queue.addBulk(jobs);
totalJobsCreated += createdJobs.length;
// Log progress every 500 jobs
if (totalJobsCreated % 500 === 0 || i + chunkSize >= items.length) {
logger.info('Direct job creation progress', {
created: totalJobsCreated,
total: items.length,
percentage: `${((totalJobsCreated / items.length) * 100).toFixed(1)}%`
});
}
} catch (error) {
logger.error('Failed to create job chunk', {
startIndex: i,
chunkSize: chunk.length,
error: error instanceof Error ? error.message : String(error)
});
}
}
return {
totalItems: items.length,
jobsCreated: totalJobsCreated,
mode: 'direct'
};
}
private async createBatchJobs<T>(config: BatchConfig<T>) {
const {
items,
batchSize = 200,
totalDelayMs,
jobNamePrefix,
operation,
service,
provider,
priority = 3
} = config;
const totalBatches = Math.ceil(items.length / batchSize);
const delayPerBatch = Math.floor(totalDelayMs / totalBatches);
const chunkSize = 50; // Create batch jobs in chunks
let batchJobsCreated = 0;
logger.info('Creating optimized batch jobs with Redis payload storage', {
totalItems: items.length,
batchSize,
totalBatches,
delayPerBatch: `${(delayPerBatch / 1000 / 60).toFixed(2)} minutes`,
payloadTtlHours: config.payloadTtlHours || 24
});
// Create batch jobs in chunks
for (let chunkStart = 0; chunkStart < totalBatches; chunkStart += chunkSize) {
const chunkEnd = Math.min(chunkStart + chunkSize, totalBatches);
const batchJobs = [];
for (let batchIndex = chunkStart; batchIndex < chunkEnd; batchIndex++) {
const startIndex = batchIndex * batchSize;
const endIndex = Math.min(startIndex + batchSize, items.length);
const batchItems = items.slice(startIndex, endIndex);
// Store batch payload in Redis and get reference key
const payloadKey = await this.storeBatchPayload(batchItems, config, batchIndex);
batchJobs.push({
name: `${jobNamePrefix}-batch-processing`,
data: {
type: `${jobNamePrefix}-batch-processing`,
service,
provider,
operation: `process-${jobNamePrefix}-batch`,
payload: {
// Optimized: only store reference and metadata
payloadKey: payloadKey,
batchIndex,
total: totalBatches,
itemCount: batchItems.length,
configSnapshot: {
jobNamePrefix: config.jobNamePrefix,
operation: config.operation,
service: config.service,
provider: config.provider,
priority: config.priority,
removeOnComplete: config.removeOnComplete,
removeOnFail: config.removeOnFail,
totalDelayMs: config.totalDelayMs
}
},
priority
},
opts: {
delay: batchIndex * delayPerBatch,
jobId: `${jobNamePrefix}-batch:${batchIndex}:${Date.now()}`
}
});
}
try {
const createdJobs = await this.queueManager.queue.addBulk(batchJobs);
batchJobsCreated += createdJobs.length;
logger.info('Optimized batch chunk created', {
chunkStart: chunkStart + 1,
chunkEnd,
created: createdJobs.length,
totalCreated: batchJobsCreated,
progress: `${((chunkEnd / totalBatches) * 100).toFixed(1)}%`,
usingRedisStorage: true
});
} catch (error) {
logger.error('Failed to create batch chunk', {
chunkStart,
chunkEnd,
error: error instanceof Error ? error.message : String(error)
});
}
// Small delay between chunks
if (chunkEnd < totalBatches) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} return {
totalItems: items.length,
batchJobsCreated,
totalBatches,
estimatedDurationHours: totalDelayMs / 1000 / 60 / 60,
mode: 'batch',
optimized: true
};
}
/**
* Process a batch (called by batch jobs)
* Supports both optimized (Redis payload storage) and fallback modes
*/
async processBatch<T>(
jobPayload: any,
createJobData?: (item: T, index: number) => any
) {
let batchData: {
items: T[];
batchIndex: number;
config: BatchConfig<T>;
};
let total: number;
// Check if this is an optimized batch with Redis payload storage
if (jobPayload.payloadKey) {
logger.info('Processing optimized batch with Redis payload storage', {
payloadKey: jobPayload.payloadKey,
batchIndex: jobPayload.batchIndex,
itemCount: jobPayload.itemCount
});
// Load actual payload from Redis
const loadedPayload = await this.loadBatchPayload<T>(jobPayload.payloadKey);
if (!loadedPayload) {
throw new Error(`Failed to load batch payload from Redis: ${jobPayload.payloadKey}`);
}
batchData = loadedPayload;
total = jobPayload.total;
// Clean up Redis payload after loading (optional - you might want to keep it for retry scenarios)
// await this.redisClient?.del(jobPayload.payloadKey);
} else {
// Fallback: payload stored directly in job data
logger.info('Processing batch with inline payload storage', {
batchIndex: jobPayload.batchIndex,
itemCount: jobPayload.items?.length || 0
});
batchData = {
items: jobPayload.items,
batchIndex: jobPayload.batchIndex,
config: jobPayload.config
};
total = jobPayload.total;
}
const { items, batchIndex, config } = batchData;
logger.info('Processing batch', {
batchIndex,
batchSize: items.length,
total,
progress: `${((batchIndex + 1) / total * 100).toFixed(2)}%`,
isOptimized: !!jobPayload.payloadKey
});
const totalBatchDelayMs = config.totalDelayMs / total;
const delayPerItem = Math.floor(totalBatchDelayMs / items.length);
const jobs = items.map((item, itemIndex) => {
// Use the provided createJobData function or fall back to config
const jobDataFn = createJobData || config.createJobData;
if (!jobDataFn) {
throw new Error('createJobData function is required');
}
const userData = jobDataFn(item, itemIndex);
return {
name: `${config.jobNamePrefix}-processing`,
data: {
type: `${config.jobNamePrefix}-processing`,
service: config.service,
provider: config.provider,
operation: config.operation,
payload: {
...userData,
batchIndex,
itemIndex,
total,
source: userData.source || 'batch-processing'
},
priority: config.priority || 2
},
opts: {
delay: itemIndex * delayPerItem,
jobId: `${config.jobNamePrefix}:${batchIndex}:${itemIndex}:${Date.now()}`,
removeOnComplete: config.removeOnComplete || 5,
removeOnFail: config.removeOnFail || 3
}
};
});
try {
const createdJobs = await this.queueManager.queue.addBulk(jobs);
logger.info('Batch processing completed', {
batchIndex,
totalItems: items.length,
jobsCreated: createdJobs.length,
progress: `${((batchIndex + 1) / total * 100).toFixed(2)}%`,
memoryOptimized: !!jobPayload.payloadKey
});
return {
batchIndex,
totalItems: items.length,
jobsCreated: createdJobs.length,
jobsFailed: 0,
payloadKey: jobPayload.payloadKey || null
};
} catch (error) {
logger.error('Failed to process batch', {
batchIndex,
error: error instanceof Error ? error.message : String(error)
});
return {
batchIndex,
totalItems: items.length,
jobsCreated: 0,
jobsFailed: items.length,
payloadKey: jobPayload.payloadKey || null
};
}
} /**
* Clean up Redis payload after successful processing (optional)
*/
async cleanupBatchPayload(payloadKey: string): Promise<void> {
if (!payloadKey) {
return;
}
if (!this.cacheProvider.isReady()) {
logger.warn('Cache provider not ready - skipping cleanup', { payloadKey });
return;
}
try {
await this.cacheProvider.del(payloadKey);
logger.info('Cleaned up batch payload from Redis', { payloadKey });
} catch (error) {
logger.warn('Failed to cleanup batch payload', {
payloadKey,
error: error instanceof Error ? error.message : String(error)
});
}
}
}

View file

@ -1,20 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/__tests__/**"],
"references": [
{ "path": "../../libs/types" },
{ "path": "../../libs/config" },
{ "path": "../../libs/logger" },
{ "path": "../../libs/http" },
{ "path": "../../libs/cache" },
{ "path": "../../libs/questdb-client" },
{ "path": "../../libs/mongodb-client" },
{ "path": "../../libs/event-bus" },
{ "path": "../../libs/shutdown" }
]
}

View file

@ -1,19 +0,0 @@
{
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": [
"@stock-bot/cache#build",
"@stock-bot/config#build",
"@stock-bot/event-bus#build",
"@stock-bot/http#build",
"@stock-bot/logger#build",
"@stock-bot/mongodb-client#build",
"@stock-bot/questdb-client#build",
"@stock-bot/shutdown#build"
],
"outputs": ["dist/**"],
"inputs": ["src/**", "package.json", "tsconfig.json", "!**/*.test.ts", "!**/*.spec.ts", "!**/test/**", "!**/tests/**", "!**/__tests__/**"]
}
}
}

View file

@ -1,37 +0,0 @@
{
"name": "@stock-bot/execution-service",
"version": "1.0.0",
"description": "Execution service for stock trading bot - handles order execution and broker integration",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"devvvvv": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"test": "bun test",
"lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@hono/node-server": "^1.12.0",
"hono": "^4.6.1",
"@stock-bot/config": "*",
"@stock-bot/logger": "*",
"@stock-bot/types": "*",
"@stock-bot/event-bus": "*",
"@stock-bot/utils": "*"
},
"devDependencies": {
"@types/node": "^22.5.0",
"typescript": "^5.5.4"
},
"keywords": [
"trading",
"execution",
"broker",
"orders",
"stock-bot"
],
"author": "Stock Bot Team",
"license": "MIT"
}

View file

@ -1,94 +0,0 @@
import { Order, OrderResult, OrderStatus } from '@stock-bot/types';
export interface BrokerInterface {
/**
* Execute an order with the broker
*/
executeOrder(order: Order): Promise<OrderResult>;
/**
* Get order status from broker
*/
getOrderStatus(orderId: string): Promise<OrderStatus>;
/**
* Cancel an order
*/
cancelOrder(orderId: string): Promise<boolean>;
/**
* Get current positions
*/
getPositions(): Promise<Position[]>;
/**
* Get account balance
*/
getAccountBalance(): Promise<AccountBalance>;
}
export interface Position {
symbol: string;
quantity: number;
averagePrice: number;
currentPrice: number;
unrealizedPnL: number;
side: 'long' | 'short';
}
export interface AccountBalance {
totalValue: number;
availableCash: number;
buyingPower: number;
marginUsed: number;
}
export class MockBroker implements BrokerInterface {
private orders: Map<string, OrderResult> = new Map();
private positions: Position[] = [];
async executeOrder(order: Order): Promise<OrderResult> {
const orderId = `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const result: OrderResult = {
orderId,
symbol: order.symbol,
quantity: order.quantity,
side: order.side,
status: 'filled',
executedPrice: order.price || 100, // Mock price
executedAt: new Date(),
commission: 1.0
};
this.orders.set(orderId, result);
return result;
}
async getOrderStatus(orderId: string): Promise<OrderStatus> {
const order = this.orders.get(orderId);
return order?.status || 'unknown';
}
async cancelOrder(orderId: string): Promise<boolean> {
const order = this.orders.get(orderId);
if (order && order.status === 'pending') {
order.status = 'cancelled';
return true;
}
return false;
}
async getPositions(): Promise<Position[]> {
return this.positions;
}
async getAccountBalance(): Promise<AccountBalance> {
return {
totalValue: 100000,
availableCash: 50000,
buyingPower: 200000,
marginUsed: 0
};
}
}

View file

@ -1,57 +0,0 @@
import { Order, OrderResult } from '@stock-bot/types';
import { logger } from '@stock-bot/logger';
import { BrokerInterface } from '../broker/interface.ts';
export class OrderManager {
private broker: BrokerInterface;
private pendingOrders: Map<string, Order> = new Map();
constructor(broker: BrokerInterface) {
this.broker = broker;
}
async executeOrder(order: Order): Promise<OrderResult> {
try {
logger.info(`Executing order: ${order.symbol} ${order.side} ${order.quantity} @ ${order.price}`);
// Add to pending orders
const orderId = `order_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.pendingOrders.set(orderId, order);
// Execute with broker
const result = await this.broker.executeOrder(order);
// Remove from pending
this.pendingOrders.delete(orderId);
logger.info(`Order executed successfully: ${result.orderId}`);
return result;
} catch (error) {
logger.error('Order execution failed', error);
throw error;
}
}
async cancelOrder(orderId: string): Promise<boolean> {
try {
const success = await this.broker.cancelOrder(orderId);
if (success) {
this.pendingOrders.delete(orderId);
logger.info(`Order cancelled: ${orderId}`);
}
return success;
} catch (error) {
logger.error('Order cancellation failed', error);
throw error;
}
}
async getOrderStatus(orderId: string) {
return await this.broker.getOrderStatus(orderId);
}
getPendingOrders(): Order[] {
return Array.from(this.pendingOrders.values());
}
}

View file

@ -1,111 +0,0 @@
import { Order } from '@stock-bot/types';
import { getLogger } from '@stock-bot/logger';
export interface RiskRule {
name: string;
validate(order: Order, context: RiskContext): Promise<RiskValidationResult>;
}
export interface RiskContext {
currentPositions: Map<string, number>;
accountBalance: number;
totalExposure: number;
maxPositionSize: number;
maxDailyLoss: number;
}
export interface RiskValidationResult {
isValid: boolean;
reason?: string;
severity: 'info' | 'warning' | 'error';
}
export class RiskManager {
private logger = getLogger('risk-manager');
private rules: RiskRule[] = [];
constructor() {
this.initializeDefaultRules();
}
addRule(rule: RiskRule): void {
this.rules.push(rule);
}
async validateOrder(order: Order, context: RiskContext): Promise<RiskValidationResult> {
for (const rule of this.rules) {
const result = await rule.validate(order, context);
if (!result.isValid) {
logger.warn(`Risk rule violation: ${rule.name}`, {
order,
reason: result.reason
});
return result;
}
}
return { isValid: true, severity: 'info' };
}
private initializeDefaultRules(): void {
// Position size rule
this.addRule({
name: 'MaxPositionSize',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const orderValue = order.quantity * (order.price || 0);
if (orderValue > context.maxPositionSize) {
return {
isValid: false,
reason: `Order size ${orderValue} exceeds maximum position size ${context.maxPositionSize}`,
severity: 'error'
};
}
return { isValid: true, severity: 'info' };
}
});
// Balance check rule
this.addRule({
name: 'SufficientBalance',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const orderValue = order.quantity * (order.price || 0);
if (order.side === 'buy' && orderValue > context.accountBalance) {
return {
isValid: false,
reason: `Insufficient balance: need ${orderValue}, have ${context.accountBalance}`,
severity: 'error'
};
}
return { isValid: true, severity: 'info' };
}
});
// Concentration risk rule
this.addRule({
name: 'ConcentrationLimit',
async validate(order: Order, context: RiskContext): Promise<RiskValidationResult> {
const currentPosition = context.currentPositions.get(order.symbol) || 0;
const newPosition = order.side === 'buy' ?
currentPosition + order.quantity :
currentPosition - order.quantity;
const positionValue = Math.abs(newPosition) * (order.price || 0);
const concentrationRatio = positionValue / context.accountBalance;
if (concentrationRatio > 0.25) { // 25% max concentration
return {
isValid: false,
reason: `Position concentration ${(concentrationRatio * 100).toFixed(2)}% exceeds 25% limit`,
severity: 'warning'
};
}
return { isValid: true, severity: 'info' };
}
});
}
}

View file

@ -1,97 +0,0 @@
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { getLogger } from '@stock-bot/logger';
import { config } from '@stock-bot/config';
// import { BrokerInterface } from './broker/interface.ts';
// import { OrderManager } from './execution/order-manager.ts';
// import { RiskManager } from './execution/risk-manager.ts';
const app = new Hono();
const logger = getLogger('execution-service');
// Health check endpoint
app.get('/health', (c) => {
return c.json({
status: 'healthy',
service: 'execution-service',
timestamp: new Date().toISOString()
});
});
// Order execution endpoints
app.post('/orders/execute', async (c) => {
try {
const orderRequest = await c.req.json();
logger.info('Received order execution request', orderRequest);
// TODO: Validate order and execute
return c.json({
orderId: `order_${Date.now()}`,
status: 'pending',
message: 'Order submitted for execution'
});
} catch (error) {
logger.error('Order execution failed', error);
return c.json({ error: 'Order execution failed' }, 500);
}
});
app.get('/orders/:orderId/status', async (c) => {
const orderId = c.req.param('orderId');
try {
// TODO: Get order status from broker
return c.json({
orderId,
status: 'filled',
executedAt: new Date().toISOString()
});
} catch (error) {
logger.error('Failed to get order status', error);
return c.json({ error: 'Failed to get order status' }, 500);
}
});
app.post('/orders/:orderId/cancel', async (c) => {
const orderId = c.req.param('orderId');
try {
// TODO: Cancel order with broker
return c.json({
orderId,
status: 'cancelled',
cancelledAt: new Date().toISOString()
});
} catch (error) {
logger.error('Failed to cancel order', error);
return c.json({ error: 'Failed to cancel order' }, 500);
}
});
// Risk management endpoints
app.get('/risk/position/:symbol', async (c) => {
const symbol = c.req.param('symbol');
try {
// TODO: Get position risk metrics
return c.json({
symbol,
position: 100,
exposure: 10000,
risk: 'low'
});
} catch (error) {
logger.error('Failed to get position risk', error);
return c.json({ error: 'Failed to get position risk' }, 500);
}
});
const port = config.EXECUTION_SERVICE_PORT || 3004;
logger.info(`Starting execution service on port ${port}`);
serve({
fetch: app.fetch,
port
}, (info) => {
logger.info(`Execution service is running on port ${info.port}`);
});

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