├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── build
└── icons
│ ├── icon.icns
│ ├── icon.png
│ ├── icon_128x128.png
│ ├── icon_128x128@2x.png
│ ├── icon_16x16.png
│ ├── icon_16x16@2x.png
│ ├── icon_256x256.png
│ ├── icon_256x256@2x.png
│ ├── icon_32x32.png
│ ├── icon_32x32@2x.png
│ ├── icon_512x512.png
│ └── icon_512x512@2x.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── iconTemplate.png
├── index.html
└── worker.html
├── src
├── App.vue
├── assets
│ ├── .gitkeep
│ └── logo.svg
├── background.js
├── components
│ ├── HelloWorld.vue
│ ├── Portfolio.vue
│ └── StrategySelector.vue
├── lib
│ ├── api
│ │ ├── index.js
│ │ └── twitter.js
│ ├── brokers
│ │ ├── index.js
│ │ └── robinhood
│ │ │ └── index.js
│ ├── db.js
│ ├── initDb.js
│ ├── login.js
│ ├── strategies
│ │ ├── ReverseSplitArbitrage.js
│ │ └── Strategy.js
│ └── trader.js
├── logger
│ └── index.js
├── main.js
├── plugins
│ └── vuetify.js
├── router
│ └── index.js
├── store
│ └── index.js
├── views
│ ├── Home.vue
│ ├── Login.vue
│ ├── Logout.vue
│ ├── Logs.vue
│ └── Strategies.vue
└── worker
│ ├── Worker.vue
│ └── worker.js
├── tests
└── unit
│ ├── electron.spec.js
│ └── strategy-selector.spec.js
└── vue.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | end_of_line = lf
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | max_line_length = 100
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist_electron
2 | *.spec.js
3 | strategies/*
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: [
7 | 'plugin:vue/essential',
8 | '@vue/airbnb',
9 | ],
10 | parserOptions: {
11 | parser: 'babel-eslint',
12 | },
13 | rules: {
14 | 'linebreak-style': ["off", "unix"],
15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
17 | 'import/no-extraneous-dependencies': ['error', {'devDependencies': true}],
18 | 'max-len': ["error", {
19 | "code": 150,
20 | "ignoreTemplateLiterals": true,
21 | "ignoreStrings": true
22 | }],
23 | },
24 | overrides: [
25 | {
26 | files: [
27 | '**/__tests__/*.{j,t}s?(x)',
28 | '**/tests/unit/**/*.spec.{j,t}s?(x)',
29 | ],
30 | env: {
31 | mocha: true,
32 | },
33 | },
34 | ],
35 | };
36 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [mafischer]
4 | patreon: trader_bot
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build/release
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | runs-on: ${{ matrix.os }}
8 |
9 | strategy:
10 | matrix:
11 | os: [macos-latest, ubuntu-latest, windows-latest]
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Use Node.js ${{ matrix.node-version }}
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: ${{ matrix.node-version }}
19 | - name: Get npm cache directory
20 | id: npm-cache-dir
21 | run: |
22 | echo "::set-output name=dir::$(npm config get cache)"
23 | - uses: actions/cache@v2
24 | id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
25 | with:
26 | path: ${{ steps.npm-cache-dir.outputs.dir }}
27 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
28 | restore-keys: |
29 | ${{ runner.os }}-node-
30 | - name: Build/release Electron app
31 | uses: samuelmeuli/action-electron-builder@v1
32 | with:
33 | # GitHub token, automatically provided to the action
34 | # (No need to define this secret in the repo settings)
35 | github_token: ${{ secrets.github_token }}
36 |
37 | # If the commit is tagged with a version (e.g. "v1.0.0"),
38 | # release the app after building
39 | release: ${{ startsWith(github.ref, 'refs/tags/v') }}
40 |
41 | use_vue_cli: true
42 |
43 | mac_certs: ${{ secrets.mac_certs }}
44 | mac_certs_password: ${{ secrets.mac_certs_password }}
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Tests
5 |
6 | on:
7 | push:
8 | branches: '*'
9 | pull_request:
10 | branches: '*'
11 |
12 | jobs:
13 | test:
14 |
15 | runs-on: ${{ matrix.os }}
16 |
17 | strategy:
18 | matrix:
19 | os: [macos-latest, ubuntu-latest, windows-latest]
20 | node-version: [12.x]
21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22 |
23 | steps:
24 | - uses: actions/checkout@v2
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | - name: Get npm cache directory
30 | id: npm-cache-dir
31 | run: |
32 | echo "::set-output name=dir::$(npm config get cache)"
33 | - uses: actions/cache@v2
34 | id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
35 | with:
36 | path: ${{ steps.npm-cache-dir.outputs.dir }}
37 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
38 | restore-keys: |
39 | ${{ runner.os }}-node-
40 | - run: npm ci
41 | - name: tests
42 | run: npm run test:unit
43 | if: matrix.os != 'ubuntu-latest'
44 | - name: ubuntu tests
45 | run: "xvfb-run --auto-servernum npm run test:unit"
46 | if: matrix.os == 'ubuntu-latest'
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | .npmrc
4 | /dist
5 | *.sql
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | pnpm-debug.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | #Electron-builder output
27 | /dist_electron
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2021 Michael Fischer
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Trader Bot
2 | A stock trading bot that can automate trades based on elected trade strategies. Trade strategies are plugins that are provided out of the box, created by the end user, or provided by the community.
3 |
4 | ## TL;DR
5 | Install from [here](#Installation) and enjoy!
6 |
7 | ## Acknowledgements
8 | - This bot and the reverse-spit strategy were initially inspired by the work of [@ReverseSplitArb](https://twitter.com/ReverseSplitArb), send some appreciation that way.
9 | - This project has been built on top of the work of other open source initiatives; for more information, please see the dependencies listed in the [package.json](package.json).
10 |
11 | ## Warnings
12 | - This software should be considered experimental!
13 | - You are giving this experimental software access to your stock broker(s). **THERE IS POTENTIAL FOR FINANCIAL LOSS!!**
14 | - All data is stored on your local file system in SQLite databases.
15 | - Your credentials will be encrypted and stored in SQLite.
16 | - Anonymous usage statistics may be sent over the network if you chose to allow it.
17 |
18 | ## License
19 | This software is licensed under the ISC license. See [LICENSE](LICENSE) for full details.
20 |
21 | ## Usage
22 |
23 | ### Installation
24 | - Download the installer from [TO DO](#)
25 |
26 | ### Setup
27 |
28 | #### Pre Requisites
29 | - For access to twitter data, [apply](https://developer.twitter.com/en/apply-for-access) for a twitter developer account
30 | - create an account with a supported broker (support further development by using below links to open an account):
31 | - **robinhood**: https://join.robinhood.com/michaef30
32 | - Please review Robinhood's [TOS](https://cdn.robinhood.com/assets/robinhood/legal/Customer%20Agreement.pdf) before using this software.
33 | - **webull**: *coming next, soon..*
34 | - **more to come**
35 |
36 | #### First Run
37 | - Start the application.
38 | - You will be prompted for your broker and twitter developer credentials.
39 | - You will be prompted for a password to encrypt and decrypt your credentials.
40 | - Elect one or more trading strategies.
41 | - The bot will now make trades according to your elected strategitesl
42 |
43 | #### Subsequent Runs
44 | - Start the application.
45 | - You will be prompted for a password to encrypt and decrypt your credentials.
46 | - All of your settings are persited in a local database. The application will run as previously configured.
47 | - Leave running, Trader Bot must be running in order to make trades.
48 |
49 | ## Development
50 |
51 | ### Tech Stack & Docs
52 | - [NodeJS](https://nodejs.org/en/docs/)
53 | - [Electron](https://www.electronjs.org/)
54 | - [Vue](https://vuejs.org/)
55 | - [Vue CLI](https://cli.vuejs.org/)
56 | - [vue-cli-plugin-electron-builder](https://nklayman.github.io/vue-cli-plugin-electron-builder/)
57 | - [electron-builder](https://www.electron.build/)
58 |
59 | ### Environment Setup
60 | #### Windows
61 | For easy setup, you may use chocolatey as outlined below. Otherwise, if you know what you are doing, feel free to install the below dependencies however you see fit.
62 | ``` powershell
63 | # run powershell as Administrator
64 | # install chocloatey - https://chocolatey.org/install
65 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
66 | # install NodeJS - https://nodejs.org/
67 | choco install nodejs-lts
68 | # install SQLite
69 | choco install sqlite
70 | # install git
71 | choco install git
72 | # install windows build tools - needed for node-gyp
73 | choco install microsoft-build-tools
74 | # install python 3 - needed for node-gyp
75 | choco install python
76 | # install python 2 - needed for node-sass
77 | choco install python2
78 | # install node-gyp
79 | npm i -g node-gyp
80 | ```
81 | #### Mac
82 | For easy setup, you may use brew as outlined below. Otherwise, if you know what you are doing, feel free to install the below dependencies however you see fit.
83 | ``` bash
84 | # install brew - https://brew.sh/
85 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
86 | # install NodeJS -https://nodejs.org/
87 | brew install node
88 | # install SQLite https://www.sqlite.org/index.html
89 | brew install sqlite
90 | ```
91 | #### Other
92 | It is presumed that you know what you are doing.
93 | - Install node and sqlite
94 | #### All
95 | ``` bash
96 | # clone project
97 | git clone https://github.com/mafischer/trader-bot.git
98 | # change directory into project
99 | cd trader-bot
100 | # install dependencies
101 | npm ci
102 | # serve application
103 | npm run electron:serve
104 | ```
105 |
106 | ## Further Development
107 |
108 | ### Long term vision
109 | - This project aims to provide a consistent broker api to simplify building a trading strategy. In order for a new broker to be added, a wrapper api must be developed to normalize the broker api with what is currently available
110 | - Tensorflow will be added to support strategies that want to use Machine Learning.
111 | - A repository of plugins will allow the community to share strategies.
112 |
113 | ### Contributing
114 | - Have a look at the [issues](https://github.com/mafischer/trader-bot/issues) and feel free to submit a PR
115 | - Submit an issue if you find a bug or have an idea to make the project better.
116 | - Throw me some spare change if you found this software useful.
117 |
118 | #### Donations
119 | Become a patron by clicking below:
120 |
121 | [](https://www.patreon.com/trader_bot)
122 |
123 | Send a one-time donation via paypal:
124 |
125 | [](https://www.paypal.me/michaelmab88/5)
126 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset',
4 | ],
5 | };
6 |
--------------------------------------------------------------------------------
/build/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon.icns
--------------------------------------------------------------------------------
/build/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon.png
--------------------------------------------------------------------------------
/build/icons/icon_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_128x128.png
--------------------------------------------------------------------------------
/build/icons/icon_128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_128x128@2x.png
--------------------------------------------------------------------------------
/build/icons/icon_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_16x16.png
--------------------------------------------------------------------------------
/build/icons/icon_16x16@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_16x16@2x.png
--------------------------------------------------------------------------------
/build/icons/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_256x256.png
--------------------------------------------------------------------------------
/build/icons/icon_256x256@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_256x256@2x.png
--------------------------------------------------------------------------------
/build/icons/icon_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_32x32.png
--------------------------------------------------------------------------------
/build/icons/icon_32x32@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_32x32@2x.png
--------------------------------------------------------------------------------
/build/icons/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_512x512.png
--------------------------------------------------------------------------------
/build/icons/icon_512x512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/build/icons/icon_512x512@2x.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "trader-bot",
3 | "version": "1.0.0-alpha.1",
4 | "private": true,
5 | "description": "Stock trader bot",
6 | "author": {
7 | "name": "Michael Fischer"
8 | },
9 | "scripts": {
10 | "build": "npm ci",
11 | "test:unit": "vue-cli-service test:unit",
12 | "lint": "vue-cli-service lint",
13 | "electron:build": "vue-cli-service electron:build",
14 | "electron:serve": "vue-cli-service electron:serve",
15 | "postinstall": "electron-builder install-app-deps",
16 | "postuninstall": "electron-builder install-app-deps"
17 | },
18 | "main": "background.js",
19 | "dependencies": {
20 | "@mdi/font": "^5.9.55",
21 | "async-es": "^3.2.0",
22 | "chart.js": "^2.9.4",
23 | "chartjs-adapter-luxon": "^0.2.2",
24 | "core-js": "^3.9.1",
25 | "cryptr": "^6.0.2",
26 | "deep-equal": "^2.0.5",
27 | "electron-prompt": "^1.6.2",
28 | "luxon": "^1.26.0",
29 | "memoizee": "^0.4.15",
30 | "robinhood": "github:mafischer/robinhood-node#_login",
31 | "roboto-fontface": "^0.10.0",
32 | "sqlite": "^4.0.19",
33 | "sqlite3": "^5.0.2",
34 | "twitter-v2": "^1.0.7",
35 | "vex-js": "^4.1.0",
36 | "vue": "^2.6.11",
37 | "vue-chartjs": "^3.5.1",
38 | "vue-router": "^3.2.0",
39 | "vuetify": "^2.4.8",
40 | "vuex": "^3.4.0",
41 | "winston": "^3.3.3",
42 | "winston-daily-rotate-file": "^4.5.1"
43 | },
44 | "devDependencies": {
45 | "@vue/cli-plugin-babel": "~4.5.12",
46 | "@vue/cli-plugin-eslint": "~4.5.12",
47 | "@vue/cli-plugin-router": "~4.5.0",
48 | "@vue/cli-plugin-unit-mocha": "~4.5.0",
49 | "@vue/cli-plugin-vuex": "~4.5.12",
50 | "@vue/cli-service": "~4.5.15",
51 | "@vue/eslint-config-airbnb": "^5.0.2",
52 | "@vue/test-utils": "^1.1.3",
53 | "babel-eslint": "^10.1.0",
54 | "chai": "^4.3.4",
55 | "chai-as-promised": "^7.1.1",
56 | "electron": "^11.2.3",
57 | "electron-builder": "^22.10.5",
58 | "electron-devtools-installer": "^3.1.0",
59 | "eslint": "^6.7.2",
60 | "eslint-plugin-import": "^2.20.2",
61 | "eslint-plugin-vue": "^6.2.2",
62 | "lint-staged": "^10.5.4",
63 | "node-sass": "^4.12.0",
64 | "sass": "^1.32.8",
65 | "sass-loader": "^8.0.2",
66 | "spectron": "^13.0.0",
67 | "vue-cli-plugin-electron-builder": "~2.0.0-rc.6",
68 | "vue-cli-plugin-vuetify": "^2.3.1",
69 | "vue-template-compiler": "^2.6.11",
70 | "vuetify-loader": "^1.7.2"
71 | },
72 | "_id": "trader-bot@1.0.0",
73 | "gitHooks": {
74 | "pre-commit": "lint-staged"
75 | },
76 | "keywords": [
77 | "bot",
78 | "stock",
79 | "nyse",
80 | "nasdaq",
81 | "robinhood"
82 | ],
83 | "license": "ISC",
84 | "lint-staged": {
85 | "*.{js,jsx,vue}": [
86 | "vue-cli-service lint",
87 | "git add"
88 | ]
89 | },
90 | "readme": "ERROR: No README data found!"
91 | }
92 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/public/favicon.ico
--------------------------------------------------------------------------------
/public/iconTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/public/iconTemplate.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/worker.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hidden Worker
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 | mdi-login
12 |
13 |
14 |
15 | Login
16 |
17 |
18 |
19 |
20 |
26 |
27 | {{ icon }}
28 |
29 |
30 |
31 | {{ text }}
32 |
33 |
34 |
35 |
36 | mdi-logout
37 |
38 |
39 |
40 | Logout
41 |
42 |
43 |
44 |
45 |
46 |
51 |
52 |
53 | {{ $route.name }}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
122 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mafischer/trader-bot/168955a7b0987594c23a226e5d58ba4b7ce81aeb/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import {
3 | app,
4 | protocol,
5 | BrowserWindow,
6 | ipcMain,
7 | Tray,
8 | Menu,
9 | } from 'electron';
10 | import path from 'path';
11 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
12 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
13 | import { version, name, author } from '../package.json';
14 | import initDb from './lib/initDb';
15 |
16 | const isDevelopment = process.env.NODE_ENV !== 'production';
17 | let tray = null;
18 |
19 | // set app about
20 | app.setAboutPanelOptions({
21 | applicationName: name,
22 | applicationVersion: version,
23 | version,
24 | copyright: 'Michael Fischer 2021',
25 | authors: [author],
26 | });
27 |
28 | // get the app root directory
29 | const home = app.getPath('userData');
30 |
31 | // Scheme must be registered before the app is ready
32 | protocol.registerSchemesAsPrivileged([
33 | { scheme: 'app', privileges: { secure: true, standard: true } },
34 | ]);
35 |
36 | async function createWindow(options, html) {
37 | // Create the browser window.
38 | const win = new BrowserWindow(options);
39 |
40 | if (process.env.WEBPACK_DEV_SERVER_URL) {
41 | // Load the url of the dev server if in development mode
42 | await win.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}${html}`);
43 | if (!process.env.IS_TEST) win.webContents.openDevTools();
44 | } else {
45 | createProtocol('app');
46 | // Load the index.html when not in development
47 | win.loadURL(`app://.${html}`);
48 | }
49 |
50 | win.on('minimize', (event) => {
51 | event.preventDefault();
52 | win.hide();
53 | });
54 |
55 | win.on('close', (event) => {
56 | // TODO: store quit state in variable
57 | if (!app.isQuiting) {
58 | event.preventDefault();
59 | win.hide();
60 | }
61 |
62 | return false;
63 | });
64 |
65 | return win;
66 | }
67 |
68 | // This method will be called when Electron has finished
69 | // initialization and is ready to create browser windows.
70 | // Some APIs can only be used after this event occurs.
71 | app.on('ready', async () => {
72 | if (isDevelopment && !process.env.IS_TEST) {
73 | // Install Vue Devtools
74 | try {
75 | await installExtension(VUEJS_DEVTOOLS);
76 | } catch (e) {
77 | console.error('Vue Devtools failed to install:', e.toString());
78 | }
79 | }
80 |
81 | // initialize local db's
82 | try {
83 | await initDb(home);
84 | } catch (e) {
85 | console.error(e.message);
86 | }
87 |
88 | tray = new Tray(path.resolve(__static, 'iconTemplate.png'));
89 | tray.setToolTip('Trader Bot');
90 |
91 | const ui = await createWindow({
92 | title: name,
93 | width: 800,
94 | height: 600,
95 | webPreferences: {
96 | // Use pluginOptions.nodeIntegration, leave this alone
97 | // eslint-disable-next-line max-len
98 | // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
99 | nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
100 | enableRemoteModule: true,
101 | webSecurity: false,
102 | },
103 | }, '/index.html');
104 | ui.maximize();
105 |
106 | // create hidden window for main applicaiton code
107 | const main = await createWindow({
108 | show: false,
109 | title: 'hidden window',
110 | webPreferences: {
111 | // Use pluginOptions.nodeIntegration, leave this alone
112 | // eslint-disable-next-line max-len
113 | // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
114 | nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
115 | enableRemoteModule: true,
116 | webSecurity: false,
117 | },
118 | }, '/worker.html');
119 |
120 | const contextMenu = Menu.buildFromTemplate([
121 | {
122 | label: 'Configure',
123 | click: () => {
124 | ui.show();
125 | },
126 | },
127 | {
128 | label: 'Quit',
129 | click: () => {
130 | app.isQuiting = true;
131 | main.webContents.send('quit');
132 | // force quite after 5 seconds
133 | setTimeout(() => {
134 | app.quit();
135 | }, 5000);
136 | },
137 | },
138 | ]);
139 |
140 | tray.setContextMenu(contextMenu);
141 |
142 | // relay strategy event to main process
143 | ipcMain.on('strategy', (event, payload) => {
144 | main.webContents.send('strategy', payload);
145 | });
146 |
147 | // send login event to hidden window (data )
148 | ipcMain.on('login', (event, payload) => {
149 | main.webContents.send('login', payload);
150 | });
151 | // send log event to main window
152 | ipcMain.on('worker-log', (event, log) => {
153 | ui.webContents.send('worker-log', log);
154 | });
155 |
156 | // quit on exit signal
157 | ipcMain.on('exit', () => {
158 | app.quit();
159 | });
160 | });
161 |
162 | // Exit cleanly on request from parent process in development mode.
163 | if (isDevelopment) {
164 | if (process.platform === 'win32') {
165 | process.on('message', (data) => {
166 | if (data === 'graceful-exit') {
167 | app.quit();
168 | }
169 | });
170 | } else {
171 | process.on('SIGTERM', () => {
172 | app.quit();
173 | });
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/components/HelloWorld.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 | Welcome to Vuetify
16 |
17 |
18 |
19 | For help and collaboration with other Vuetify developers,
20 |
please join our online
21 | Discord Community
25 |
26 |
27 |
28 |
32 |
33 | What's next?
34 |
35 |
36 |
37 |
44 | {{ next.text }}
45 |
46 |
47 |
48 |
49 |
53 |
54 | Important Links
55 |
56 |
57 |
58 |
65 | {{ link.text }}
66 |
67 |
68 |
69 |
70 |
74 |
75 | Ecosystem
76 |
77 |
78 |
79 |
86 | {{ eco.text }}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
152 |
--------------------------------------------------------------------------------
/src/components/Portfolio.vue:
--------------------------------------------------------------------------------
1 |
27 |
--------------------------------------------------------------------------------
/src/components/StrategySelector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Pick an Available Strategy
4 |
5 |
13 |
14 |
15 | {{strategy.name}}
16 | About
17 | {{strategy.description}}
18 |
19 | Allocated Assets - ${{allocatedCapital}}
20 |
21 |
30 | {{`${account.broker}: ${account.id} - $${account.allocation}`}}
31 |
32 |
33 |
34 | Available Accounts
35 |
36 |
37 |
41 |
42 |
51 |
52 |
53 |
54 |
55 | Add
56 |
57 |
58 |
59 |
60 |
Active Strategies:
61 |
62 |
63 | {{chosen.name}}
64 | About
65 | {{chosen.description}}
66 |
67 | Allocated Assets - ${{chosen.allocation}}
68 |
69 |
76 | {{`${account.broker}: ${account.id} - $${account.allocation}`}}
77 |
78 |
79 |
80 |
81 | Pause
82 | Remove
83 |
84 |
85 |
86 |
Paused Strategies:
87 |
88 |
89 | {{chosen.name}}
90 | About
91 | {{chosen.description}}
92 |
93 | Allocated Assets - ${{chosen.allocation}}
94 |
95 |
102 | {{`${account.broker}: ${account.id} - $${account.allocation}`}}
103 |
104 |
105 |
106 |
107 | Resume
108 | Remove
109 |
110 |
111 |
112 |
113 |
114 |
115 |
201 |
--------------------------------------------------------------------------------
/src/lib/api/index.js:
--------------------------------------------------------------------------------
1 | import Twitter from './twitter';
2 |
3 | export default async (settings) => ({
4 | twitter: await Twitter(settings.twitter),
5 | });
6 |
--------------------------------------------------------------------------------
/src/lib/api/twitter.js:
--------------------------------------------------------------------------------
1 | import Twitter from 'twitter-v2';
2 |
3 | // initialize the twitter client
4 | export default (config) => (new Twitter(config));
5 |
--------------------------------------------------------------------------------
/src/lib/brokers/index.js:
--------------------------------------------------------------------------------
1 | import robinhood from './robinhood/index';
2 |
3 | export default async (log, settings) => ({
4 | robinhood: await robinhood(log, settings.robinhood),
5 | });
6 |
--------------------------------------------------------------------------------
/src/lib/brokers/robinhood/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /* eslint-disable no-await-in-loop */
3 | import { promisify } from 'util';
4 | import { eachLimit, each } from 'async-es';
5 | import Robinhood from 'robinhood';
6 | import { DateTime } from 'luxon';
7 | import memoize from 'memoizee';
8 |
9 | async function getPortfolioHistoricals({ interval, span }) {
10 | const historicals = {};
11 | const positions = await this.getPositions();
12 | await eachLimit(positions, 10, async (position, positionCb) => {
13 | try {
14 | const qty = parseFloat(position.quantity);
15 | if (qty > 0) {
16 | const { body } = await this.p_historicals(position.instrument.symbol, interval, span);
17 | historicals[position.instrument.symbol] = body;
18 | }
19 | } catch (err) {
20 | this.log({
21 | level: 'error',
22 | log: JSON.stringify({
23 | message: err.message,
24 | stack: err.stack,
25 | }),
26 | });
27 | throw err;
28 | }
29 | // quirky hack for inconsistent async-es library
30 | if (positionCb) {
31 | return positionCb();
32 | }
33 | return null;
34 | });
35 | return historicals;
36 | }
37 |
38 | async function getPositions(db) {
39 | let positions = [];
40 | const { body } = await this.p_positions();
41 | if (Object.prototype.hasOwnProperty.call(body, 'results')) {
42 | positions = body.results;
43 | await eachLimit(positions, 10, async (position, positionCb) => {
44 | try {
45 | const instrument = await this.m_url(position.instrument);
46 | position.instrument = instrument.body;
47 | // update db if/when db object is provided
48 | if (db !== undefined) {
49 | if (Object.prototype.hasOwnProperty.call(position.instrument, 'splits')) {
50 | const rsp = await this.m_url(position.instrument.splits);
51 | // const rsp = await this.m_splits(position.instrument.symbol);
52 | const splits = rsp.body;
53 | await each(splits.results, async ({ url }, urlCb) => {
54 | const splt = await this.m_url(url);
55 | const split = splt.body;
56 | await db.run(`
57 | INSERT OR IGNORE INTO splits
58 | (symbol, date, multiplier, divisor)
59 | values ($symbol, $date, $multiplier, $divisor);
60 | `, {
61 | $symbol: position.instrument.symbol,
62 | $divisor: split.divisor,
63 | $multiplier: split.multiplier,
64 | $date: split.execution_date,
65 | });
66 |
67 | // quirky hack for inconsistent async-es library
68 | if (urlCb) {
69 | return urlCb();
70 | }
71 | return null;
72 | });
73 | }
74 |
75 | await db.run(`
76 | INSERT INTO positions (broker, symbol, qty, average_cost, raw)
77 | values ($broker, $symbol, $qty, $average_cost, $raw)
78 | ON CONFLICT(broker, symbol) DO UPDATE
79 | SET qty = $qty, average_cost = $average_cost, raw = $raw;
80 | `, {
81 | $broker: 'robinhood',
82 | $symbol: position.instrument.symbol,
83 | $qty: position.quantity,
84 | $average_cost: position.average_buy_price,
85 | $raw: JSON.stringify(position),
86 | });
87 | }
88 | } catch (err) {
89 | this.log({
90 | level: 'error',
91 | log: JSON.stringify({
92 | message: err.message,
93 | stack: err.stack,
94 | }),
95 | });
96 | throw err;
97 | }
98 | // quirky hack for inconsistent async-es library
99 | if (positionCb) {
100 | return positionCb();
101 | }
102 | return null;
103 | });
104 | }
105 | return positions;
106 | }
107 |
108 | async function getAccounts(db) {
109 | let accounts = [];
110 | const { body } = await this.p_accounts();
111 | if (Object.prototype.hasOwnProperty.call(body, 'results')) {
112 | accounts = body.results;
113 | await eachLimit(accounts, 10, async (account, accountCb) => {
114 | try {
115 | await db.run(`
116 | INSERT INTO accounts (broker, id, name, type, buying_power, portfolio_cash, raw)
117 | values ($broker, $id, $name, $type, $buying_power, $portfolio_cash, $raw)
118 | ON CONFLICT(broker, id) DO UPDATE
119 | SET name = $name, type = $type, buying_power = $buying_power, portfolio_cash = $portfolio_cash, raw = $raw;
120 | `, {
121 | $broker: 'robinhood',
122 | $id: account.account_number,
123 | $name: account.account_number,
124 | $type: account.type,
125 | $buying_power: account.buying_power,
126 | $portfolio_cash: account.portfolio_cash,
127 | $raw: JSON.stringify(account),
128 | });
129 | } catch (err) {
130 | this.log({
131 | level: 'error',
132 | log: JSON.stringify({
133 | message: err.message,
134 | stack: err.stack,
135 | }),
136 | });
137 | throw err;
138 | }
139 | // quirky hack for inconsistent async-es library
140 | if (accountCb) {
141 | return accountCb();
142 | }
143 | return null;
144 | });
145 | }
146 | return accounts;
147 | }
148 |
149 | async function orderHistory(db, fromDate) {
150 | let latestCached;
151 | if (fromDate) {
152 | latestCached = fromDate;
153 | } else {
154 | // get most recent latest order from
155 | try {
156 | const order = await db.get('select updated_at from orders where created_at = (select MAX(created_at) from orders where broker = $broker) and broker = $broker limit 1;', { $broker: 'robinhood' });
157 | if (order) {
158 | latestCached = DateTime.fromISO(order.updated_at).plus({ milliseconds: 1 }).toUTC().toISO();
159 | }
160 | } catch (err) {
161 | this.log({
162 | level: 'error',
163 | log: JSON.stringify({
164 | message: err.message,
165 | stack: err.stack,
166 | }),
167 | });
168 | }
169 | }
170 |
171 | // get order history
172 | let orders = [];
173 | let cursor = null;
174 | let next = null;
175 | do {
176 | try {
177 | const options = {};
178 | if (cursor) {
179 | options.cursor = cursor;
180 | }
181 | if (latestCached) {
182 | options.updated_at = latestCached;
183 | }
184 | const { body } = await this.p_orders(options);
185 | if (Array.isArray(body.results)) {
186 | orders = [...orders, ...body.results];
187 | }
188 | if (body.next) {
189 | next = new URL(body.next);
190 | cursor = next.searchParams.get('cursor');
191 | } else {
192 | next = null;
193 | }
194 | } catch (err) {
195 | this.log({
196 | level: 'error',
197 | log: JSON.stringify({
198 | message: err.message,
199 | stack: err.stack,
200 | }),
201 | });
202 | next = null;
203 | }
204 | } while (next !== null);
205 |
206 | await eachLimit(orders, 10, async (order) => {
207 | try {
208 | const instrument = await this.m_url(order.instrument);
209 | order.instrument = instrument.body;
210 | let price = null;
211 | if (order.average_price) {
212 | price = parseFloat(order.average_price);
213 | }
214 | if (order.price) {
215 | price = parseFloat(order.price);
216 | }
217 | await db.run(`
218 | INSERT INTO orders (broker, account, symbol, created_at, updated_at, state, type, quantity, price, action, raw)
219 | VALUES ($broker, $account, $symbol, $created_at, $updated_at, $state, $type, $quantity, $price, $action, $raw)
220 | ON CONFLICT(broker, account, symbol, created_at) DO UPDATE SET
221 | updated_at = $updated_at, state = $state, price = $price, raw = $raw;
222 | `,
223 | {
224 | $broker: 'robinhood',
225 | $account: order.account,
226 | $symbol: order.instrument.symbol,
227 | $created_at: order.created_at,
228 | $updated_at: order.updated_at,
229 | $state: order.state,
230 | $price: price,
231 | $action: order.side,
232 | $type: order.type,
233 | $quantity: order.quantity,
234 | $raw: JSON.stringify(order),
235 | });
236 | } catch (err) {
237 | this.log({
238 | level: 'error',
239 | log: JSON.stringify({
240 | message: err.message,
241 | stack: err.stack,
242 | }),
243 | });
244 | }
245 | return null;
246 | });
247 |
248 | return orders;
249 | }
250 |
251 | export default (log, token) => (
252 | new Promise((resolve, reject) => {
253 | const rh = Robinhood(token, (err) => {
254 | if (err) {
255 | reject(err);
256 | }
257 | // set log function
258 | rh.log = log;
259 |
260 | // set async functions
261 | // auth_token()
262 | rh.p_auth_token = promisify(rh.auth_token);
263 | // expire_token(callback)
264 | rh.p_expire_token = promisify(rh.expire_token);
265 | // investment_profile(callback)
266 | rh.p_investment_profile = promisify(rh.investment_profile);
267 | // instruments(symbol, callback)
268 | rh.p_insturments = promisify(rh.instruments);
269 | // quote_data(stock, callback)
270 | rh.p_quote_data = promisify(rh.quote_data);
271 | // accounts(callback)
272 | rh.p_accounts = promisify(rh.accounts);
273 | // user(callback)
274 | rh.p_user = promisify(rh.user);
275 | // dividends(callback)
276 | rh.p_dividends = promisify(rh.dividends);
277 | // earnings(option, callback)
278 | rh.p_earnings = promisify(rh.earnings);
279 | // orders(options, callback)
280 | rh.p_orders = promisify(rh.orders);
281 | // positions(callback)
282 | rh.p_positions = promisify(rh.positions);
283 | // nonzero_positions(callback)
284 | rh.p_nonzero_positions = promisify(rh.nonzero_positions);
285 | // place_buy_order(options, callback)
286 | rh.p_place_buy_order = promisify(rh.place_buy_order);
287 | // place_sell_order(options, callback)
288 | rh.p_place_sell_order = promisify(rh.place_sell_order);
289 | // fundamentals(symbol, callback)
290 | rh.p_fundamentals = promisify(rh.fundamentals);
291 | // cancel_order(order, callback)
292 | rh.p_cancel_order = promisify(rh.cancel_order);
293 | // watchlists(name, callback)
294 | rh.p_watchlists = promisify(rh.watchlists);
295 | // create_watch_list(name, callback)
296 | rh.p_create_watch_list = promisify(rh.create_watch_list);
297 | // sp500_up(callback)
298 | rh.p_sp500_up = promisify(rh.sp500_up);
299 | // sp500_down(callback)
300 | rh.p_sp500_down = promisify(rh.sp500_down);
301 | // splits(instrument, callback)
302 | rh.p_splits = promisify(rh.splits);
303 | // historicals(symbol, intv, span, callback)
304 | rh.p_historicals = promisify(rh.historicals);
305 | // url(url, callback)
306 | rh.p_url = promisify(rh.url);
307 | // news(symbol, callback)
308 | rh.p_news = promisify(rh.news);
309 | // tag(tag, callback)
310 | rh.p_tag = promisify(rh.tag);
311 | // popularity(symbol, callback)
312 | rh.p_popularity = promisify(rh.popularity);
313 | // options_positions
314 | rh.p_options_positions = promisify(rh.options_positions);
315 |
316 | // memoized functions
317 | // investment_profile(callback)
318 | // rh.m_investment_profile = memoize(rh.p_investment_profile, { promise: true, maxAge: 14400000 });
319 | // instruments(symbol, callback)
320 | // rh.m_insturments = memoize(rh.p_instruments, { promise: true, maxAge: 14400000 });
321 | // user(callback)
322 | // rh.m_user = memoize(rh.p_user, { promise: true, maxAge: 14400000 });
323 | // dividends(callback)
324 | // rh.m_dividends = memoize(rh.p_dividends, { promise: true, maxAge: 14400000 });
325 | // splits(instrument, callback)
326 | rh.m_splits = memoize(rh.p_splits, { promise: true, maxAge: 14400000 });
327 | // historicals(symbol, intv, span, callback)
328 | rh.m_historicals = memoize(rh.p_historicals, { promise: true, maxAge: 300000 });
329 | // url(url, callback)
330 | rh.m_url = memoize(rh.p_url, { promise: true, maxAge: 60000 });
331 | // tag(tag, callback)
332 | // rh.m_tag = memoize(rh.p_tag, { promise: true, maxAge: 14400000 });
333 |
334 | // custom functions
335 | // orderHistory
336 | rh.orderHistory = orderHistory;
337 | // getAccounts
338 | rh.getAccounts = getAccounts;
339 | // getPositions
340 | rh.getPositions = getPositions;
341 | // getPortfolioHistoricals
342 | rh.getPortfolioHistoricals = getPortfolioHistoricals;
343 |
344 | // return robinhood object
345 | resolve(rh);
346 | });
347 | })
348 | );
349 |
--------------------------------------------------------------------------------
/src/lib/db.js:
--------------------------------------------------------------------------------
1 | import sqlite3 from 'sqlite3';
2 | import { open } from 'sqlite';
3 |
4 | export async function attachDb(db, filename) {
5 | await db.run(`ATTACH DATABASE '${filename}' AS twitter;`);
6 | return db;
7 | }
8 |
9 | export async function openDb(filename) {
10 | const db = await open({
11 | filename,
12 | driver: sqlite3.cached.Database,
13 | });
14 | return db;
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/initDb.js:
--------------------------------------------------------------------------------
1 | import sqlite3 from 'sqlite3';
2 | import { open } from 'sqlite';
3 | import fs from 'fs';
4 | import path from 'path';
5 | import { eachSeries } from 'async-es';
6 |
7 | export default async function initDb(home) {
8 | console.log('starting db initialization');
9 |
10 | if (!fs.existsSync(path.resolve(home, 'trader.db'))) {
11 | // create trader database
12 | const traderDb = await open({
13 | filename: path.resolve(home, 'trader.db'),
14 | driver: sqlite3.cached.Database,
15 | });
16 | // initialize trader database
17 | const queries = `
18 | PRAGMA foreign_keys=OFF;
19 | BEGIN TRANSACTION;
20 | CREATE TABLE IF NOT EXISTS "strategies" (
21 | "id" INTEGER NOT NULL,
22 | "name" TEXT NOT NULL UNIQUE,
23 | "description" TEXT NOT NULL,
24 | "class" TEXT NOT NULL,
25 | PRIMARY KEY("id" AUTOINCREMENT)
26 | );
27 | INSERT INTO strategies VALUES(1,'Reverse Stock Split Arbitrage','Schedule the purchase of 1 share of stock 10 minutes prior to the market close on last trading day before said stock is scheduled to undergo a reverse stock split.','ReverseSplitArbitrage');
28 | CREATE TABLE IF NOT EXISTS "credentials" (
29 | "id" INTEGER NOT NULL CHECK(id=1),
30 | "credentials" TEXT NOT NULL,
31 | PRIMARY KEY("id" AUTOINCREMENT)
32 | );
33 | CREATE TABLE IF NOT EXISTS "actions" (
34 | "id" INTEGER NOT NULL,
35 | "name" TEXT NOT NULL UNIQUE,
36 | PRIMARY KEY("id" AUTOINCREMENT)
37 | );
38 | INSERT INTO actions VALUES(1,'BUY');
39 | INSERT INTO actions VALUES(2,'SELL');
40 | CREATE TABLE IF NOT EXISTS "status" (
41 | "id" INTEGER NOT NULL,
42 | "name" TEXT NOT NULL UNIQUE,
43 | PRIMARY KEY("id" AUTOINCREMENT)
44 | );
45 | INSERT INTO status VALUES(1,'PENDING');
46 | INSERT INTO status VALUES(2,'FILLED');
47 | INSERT INTO status VALUES(3,'CONFIRMED');
48 | INSERT INTO status VALUES(4,'NOT_TRIGGERED');
49 | CREATE TABLE IF NOT EXISTS "brokers" (
50 | "id" INTEGER NOT NULL,
51 | "name" TEXT NOT NULL UNIQUE,
52 | "supported" INTEGER NOT NULL DEFAULT 0,
53 | PRIMARY KEY("id" AUTOINCREMENT)
54 | );
55 | INSERT INTO brokers VALUES(1,'robinhood',1);
56 | INSERT INTO brokers VALUES(2,'webull',0);
57 | INSERT INTO brokers VALUES(3,'fidelity',0);
58 | CREATE TABLE IF NOT EXISTS "api" (
59 | "id" INTEGER NOT NULL,
60 | "service" TEXT NOT NULL UNIQUE,
61 | "inputs" TEXT NOT NULL,
62 | PRIMARY KEY("id" AUTOINCREMENT)
63 | );
64 | INSERT INTO api VALUES(1,'robinhood','["username","password"]');
65 | INSERT INTO api VALUES(2,'twitter','["bearer_token","client_secret","client_token"]');
66 | CREATE TABLE IF NOT EXISTS "orders" (
67 | "account" TEXT NOT NULL,
68 | "broker" TEXT NOT NULL,
69 | "symbol" TEXT NOT NULL,
70 | "created_at" INTEGER NOT NULL,
71 | "updated_at" TIMESTAMP,
72 | "state" TEXT NOT NULL,
73 | "type" TEXT NOT NULL,
74 | "action" TEXT,
75 | "price" NUMERIC,
76 | "quantity" INTEGER NOT NULL,
77 | "raw" TEXT NOT NULL,
78 | PRIMARY KEY("broker","account","symbol","created_at")
79 | );
80 | CREATE TABLE IF NOT EXISTS "accounts" (
81 | "broker" TEXT NOT NULL,
82 | "id" TEXT NOT NULL,
83 | "type" TEXT NOT NULL,
84 | "name" TEXT NOT NULL,
85 | "buying_power" REAL,
86 | "portfolio_cash" REAL,
87 | "raw" TEXT NOT NULL,
88 | PRIMARY KEY("broker","id")
89 | );
90 | CREATE TABLE IF NOT EXISTS "elected_strategies" (
91 | "id" INTEGER NOT NULL,
92 | "active" INTEGER NOT NULL,
93 | "allocation" REAL NOT NULL DEFAULT 0,
94 | "allocations" TEXT NOT NULL DEFAULT '[]',
95 | "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
96 | PRIMARY KEY("id")
97 | );
98 | CREATE TABLE IF NOT EXISTS "positions" (
99 | "broker" TEXT NOT NULL,
100 | "symbol" TEXT NOT NULL,
101 | "qty" INTEGER NOT NULL,
102 | "average_cost" NUMERIC NOT NULL,
103 | "raw" TEXT NOT NULL,
104 | PRIMARY KEY("broker","symbol")
105 | );
106 | DELETE FROM sqlite_sequence;
107 | INSERT INTO sqlite_sequence VALUES('strategies',2);
108 | INSERT INTO sqlite_sequence VALUES('credentials',1);
109 | INSERT INTO sqlite_sequence VALUES('actions',2);
110 | INSERT INTO sqlite_sequence VALUES('status',4);
111 | INSERT INTO sqlite_sequence VALUES('brokers',3);
112 | INSERT INTO sqlite_sequence VALUES('api',2);
113 | COMMIT
114 | `.split(';');
115 | await eachSeries(queries, async (query) => {
116 | await traderDb.run(query);
117 | });
118 |
119 | await traderDb.close();
120 |
121 | console.log('sucessfully initialized trader database');
122 | }
123 |
124 | if (!fs.existsSync(path.resolve(home, 'twitter.db'))) {
125 | // create twitter db
126 | const twitterDb = await open({
127 | filename: path.resolve(home, 'twitter.db'),
128 | driver: sqlite3.cached.Database,
129 | });
130 | // initialize twitter database
131 | const queries = `
132 | PRAGMA foreign_keys=OFF;
133 | BEGIN TRANSACTION;
134 | CREATE TABLE IF NOT EXISTS "tweets" (
135 | "id" TEXT NOT NULL UNIQUE,
136 | "author_id" TEXT NOT NULL,
137 | "created_at" TIMESTAMP NOT NULL,
138 | "text" TEXT NOT NULL,
139 | "entities" TEXT,
140 | "public_metrics" TEXT,
141 | "context_annotations" TEXT,
142 | "withheld" TEXT,
143 | "geo" TEXT,
144 | PRIMARY KEY("id")
145 | );
146 | CREATE TABLE IF NOT EXISTS "following" (
147 | "id" TEXT NOT NULL,
148 | "name" TEXT NOT NULL,
149 | "username" TEXT NOT NULL,
150 | PRIMARY KEY("id")
151 | );
152 | INSERT INTO "following" VALUES('44196397','Elon Musk','elonmusk');
153 | INSERT INTO "following" VALUES('1332370385921306631','Reverse Split Arbitrage','ReverseSplitArb');
154 | INSERT INTO "following" VALUES('898021206967951360','Tesla Daily','TeslaPodcast');
155 | COMMIT
156 | `.split(';');
157 | await eachSeries(queries, async (query) => {
158 | await twitterDb.run(query);
159 | });
160 |
161 | await twitterDb.close();
162 |
163 | console.log('sucessfully initialized twitter database');
164 | }
165 |
166 | console.log('SQLite databases are initialized.');
167 | }
168 |
--------------------------------------------------------------------------------
/src/lib/login.js:
--------------------------------------------------------------------------------
1 | import sqlite3 from 'sqlite3';
2 | import { open } from 'sqlite';
3 | import Cryptr from 'cryptr';
4 | import Robinhood from 'robinhood';
5 |
6 | const internal = {
7 | credentials: null,
8 | cryptr: null,
9 | log: console.log,
10 | };
11 |
12 | const supported = ['robinhood', 'twitter'];
13 |
14 | async function retrieveCredentials(db) {
15 | // retreive encrypted credentials
16 | let creds;
17 | creds = await db.get('select credentials from credentials where id = 1;');
18 | if (creds === undefined || creds.credentials === '') {
19 | return {};
20 | }
21 |
22 | try {
23 | // decrypt credentials
24 | creds = internal.cryptr.decrypt(creds.credentials);
25 |
26 | // parse credentials
27 | creds = JSON.parse(creds);
28 |
29 | internal.log({
30 | level: 'info',
31 | log: 'Credentials successfully decrypted!',
32 | });
33 |
34 | return creds;
35 | } catch (err) {
36 | internal.cryptr = undefined;
37 | internal.log({
38 | level: 'error',
39 | log: JSON.stringify({
40 | message: err.message,
41 | stack: err.stack,
42 | }),
43 | });
44 | throw new Error('invalid password!');
45 | }
46 | }
47 |
48 | export function missingCredentials(creds) {
49 | // validate all services are signed in
50 | let missing = [];
51 | supported.forEach((service) => {
52 | if (Object.prototype.hasOwnProperty.call(creds, service)) {
53 | missing = missing.filter((e) => e !== service);
54 | } else if (missing.indexOf(service) === -1) {
55 | missing.push(service);
56 | }
57 | });
58 | return missing;
59 | }
60 |
61 | export async function login({ filename, password, log }) {
62 | // configure log function
63 | if (log) {
64 | internal.log = log;
65 | }
66 |
67 | // open the sqlite database
68 | // no try catch because we want app to fail if this isn't working.
69 | const db = await open({
70 | filename,
71 | driver: sqlite3.cached.Database,
72 | });
73 |
74 | // set password
75 | internal.cryptr = new Cryptr(password);
76 |
77 | // grab encrypted credentials from the database
78 | internal.credentials = await retrieveCredentials(db);
79 |
80 | // validate user has signed into all required services
81 | const missing = missingCredentials(internal.credentials);
82 |
83 | // not closing db as we are running the appliation after login
84 | // db.close();
85 |
86 | return {
87 | cryptr: internal.cryptr,
88 | credentials: internal.credentials,
89 | db,
90 | missing,
91 | };
92 | }
93 |
94 | export async function updateCredentials(db, cryptr, credentials) {
95 | await db.run(`
96 | insert into credentials (id, credentials)
97 | values (1, $credentials)
98 | on conflict(id) do update set credentials = $credentials;
99 | `, { $credentials: cryptr.encrypt(JSON.stringify(credentials)) });
100 | }
101 |
102 | export function robinhood(username, password) {
103 | return new Promise((resolve, reject) => {
104 | const rh = Robinhood({ username, password }, (err, data) => {
105 | if (err) {
106 | reject(err);
107 | } else {
108 | resolve({ rh, data });
109 | }
110 | });
111 | });
112 | }
113 |
--------------------------------------------------------------------------------
/src/lib/strategies/ReverseSplitArbitrage.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-await-in-loop */
2 | import { DateTime } from 'luxon';
3 | import { eachSeries } from 'async-es';
4 | import Strategy from './Strategy';
5 |
6 | export default class ReverseSplitArbitrage extends Strategy {
7 | constructor(config) {
8 | super(config);
9 |
10 | this.name = 'Reverse Split Arbitrage';
11 |
12 | // rsa regex
13 | this.rsa = /^.*I'm buying (?\d+) shares? of \$(?[a-z]{1,5}) by market close on (?[a-z]{3} \d{1,2}, \d{4})\..*$/im;
14 | // stock ticker regex
15 | this.ticker = /\$(?[A-Z]{1,5})/ig;
16 | }
17 |
18 | async main() {
19 | const self = this;
20 | let following;
21 | try {
22 | following = await self.db.all('select * from twitter.following where username = \'ReverseSplitArb\';');
23 | } catch (err) {
24 | self.log({
25 | level: 'error',
26 | log: JSON.stringify({
27 | message: err.message,
28 | stack: err.stack,
29 | }),
30 | });
31 | }
32 |
33 | await eachSeries(following, async (tweeter, tweeterCb) => {
34 | let cache;
35 | try {
36 | cache = await self.db.get('select * from twitter.tweets where created_at = (select MAX(created_at) from twitter.tweets where author_id = $author_id) and author_id = $author_id limit 1;', { $author_id: tweeter.id });
37 | } catch (err) {
38 | self.log({
39 | level: 'error',
40 | log: JSON.stringify({
41 | message: err.message,
42 | stack: err.stack,
43 | }),
44 | });
45 | }
46 |
47 | // fetch latest data for strategy
48 | const queryParams = {
49 | start_time: `${DateTime.utc().minus({ years: 1 }).toISO()}`,
50 | exclude: 'retweets,replies',
51 | 'tweet.fields': 'id,text,created_at,context_annotations,entities,withheld,public_metrics,geo,author_id',
52 | };
53 | // set since_id to avoide duplicate data
54 | if (cache && cache.id) {
55 | queryParams.since_id = cache.id;
56 | }
57 | let tweets = [];
58 | try {
59 | let response;
60 | do {
61 | response = await self.api.twitter.get(`users/${tweeter.id}/tweets`, queryParams);
62 | if (response.meta.result_count > 0) {
63 | tweets = [...tweets, ...response.data];
64 | }
65 | queryParams.pagination_token = response.meta.next_token;
66 | } while (response.meta.next_token !== undefined);
67 | } catch (err) {
68 | self.log({
69 | level: 'error',
70 | log: JSON.stringify({
71 | message: err.message,
72 | stack: err.stack,
73 | }),
74 | });
75 | }
76 |
77 | // analyze new tweets and store in db
78 | if (tweets.length > 0) {
79 | self.log({
80 | level: 'info',
81 | log: `${tweeter.name} has tweeted since we last checked!!\n`,
82 | });
83 | }
84 |
85 | await eachSeries(tweets, async (tweet, tweetCb) => {
86 | self.log({
87 | level: 'info',
88 | log: `${tweeter.name}: ${tweet.text}\n`,
89 | });
90 |
91 | try {
92 | await self.db.run(`
93 | insert into twitter.tweets
94 | (id, author_id, created_at, text, entities, public_metrics, context_annotations, withheld, geo)
95 | values
96 | ($id, $author_id, $created_at, $text, $entities, $public_metrics, $context_annotations, $withheld, $geo);
97 | `, {
98 | $id: tweet.id || null,
99 | $author_id: tweet.author_id || null,
100 | $created_at: tweet.created_at || null,
101 | $text: tweet.text || null,
102 | $entities: tweet.entities !== undefined ? JSON.stringify(tweet.entities) : null,
103 | $public_metrics: tweet.public_metrics !== undefined ? JSON.stringify(tweet.public_metrics) : null,
104 | $context_annotations: tweet.context_annotations !== undefined ? JSON.stringify(tweet.context_annotations) : null,
105 | $withheld: tweet.withheld !== undefined ? JSON.stringify(tweet.withheld) : null,
106 | $geo: tweet.geo !== undefined ? JSON.stringify(tweet.geo) : null,
107 | });
108 | } catch (err) {
109 | self.log({
110 | level: 'error',
111 | log: JSON.stringify({
112 | message: err.message,
113 | stack: err.stack,
114 | }),
115 | });
116 | }
117 |
118 | // check for rsa match
119 | // look for stock ticker symbols in tweet
120 | if (tweeter.username === 'ReverseSplitArb' && typeof tweet.text === 'string') {
121 | const match = self.rsa.exec(tweet.text);
122 | if (match) {
123 | self.log({
124 | level: 'info',
125 | log: `@${tweeter.username} said to buy ${match.groups.qty} of ${match.groups.ticker} by the close of ${DateTime.fromFormat(match.groups.date, 'LLL dd, yyyy', { zone: 'America/New_York', hour: 16 }).toISO()}!!`,
126 | });
127 | // TODO: execute market buy for each enabled broker
128 | // TODO: ensure orders do not exceed capital limit per broker.
129 | try {
130 | const response = await self.brokers.robinhood.p_quote_data(match.groups.ticker);
131 | const { body } = response;
132 | self.log({
133 | level: 'info',
134 | log: `\nquote for ${match.groups.ticker}:\n${JSON.stringify(body)}\n`,
135 | });
136 | if (body === undefined || (Object.prototype.hasOwnProperty.call(body, 'missing_instruments') && (Array.isArray(body.missing_instruments) && body.missing_instruments.indexOf(match.groups.ticker) >= 0))) {
137 | self.log({
138 | level: 'warn',
139 | log: `Broker: ${'robinhood'} does not support ticker: ${match.groups.ticker}`,
140 | });
141 | } else {
142 | const { results } = body;
143 | if (Array.isArray(results) && results.length > 0) {
144 | self.log({
145 | level: 'info',
146 | log: `Ask price for ${results[0].symbol} is ${results[0].ask_price}, executing limit buy.`,
147 | });
148 | const resp = await self.brokers.robinhood.p_place_buy_order({
149 | type: 'limit',
150 | quantity: 1,
151 | bid_price: results[0].ask_price,
152 | instrument: {
153 | url: results[0].instrument,
154 | symbol: match.groups.ticker,
155 | },
156 | });
157 | self.log({
158 | level: 'info',
159 | log: `Requested limit order of ${match.groups.ticker} on ${'robinhood'}.\n${JSON.stringify(resp)}`,
160 | });
161 | } else {
162 | self.log({
163 | level: 'debug',
164 | log: `Unable to buy of ${match.groups.ticker} on ${'robinhood'}. quote response:\n${JSON.stringify(body)}`,
165 | });
166 | }
167 | }
168 | } catch (err) {
169 | self.log({
170 | level: 'error',
171 | log: JSON.stringify({
172 | message: err.message,
173 | stack: err.stack,
174 | }),
175 | });
176 | }
177 | }
178 | }
179 | if (tweetCb) {
180 | return tweetCb();
181 | }
182 | return null;
183 | });
184 | if (tweeterCb) {
185 | return tweeterCb();
186 | }
187 | return null;
188 | });
189 | }
190 | }
191 |
192 | // look for stock ticker symbols in tweet
193 | // if (typeof tweet.text === 'string') {
194 | // let match;
195 | // do {
196 | // match = this.ticker.exec(tweet.text);
197 | // if (match) {
198 | // const stock = match.groups.ticker;
199 | // this.log({
200 | // level: 'info',
201 | // log: `${tweeter.name} tweeted about ticker symbol ${stock} in their tweet!!`,
202 | // });
203 |
204 | // // quote stock
205 | // try {
206 | // const response = await promisify(this.robinhood.quote_data)(stock);
207 | // this.log({
208 | // level: 'info',
209 | // log: `\nquote for ${stock}:\n${JSON.stringify(response.body)}\n`,
210 | // });
211 | // } catch (err) {
212 | // this.log({
213 | // level: 'error',
214 | // log: err.message,
215 | // });
216 | // }
217 |
218 | // // execute trade (ask for permission from owner if low probability score)
219 |
220 | // // add trade to watch
221 | // }
222 | // // eslint-disable-next-line no-cond-assign
223 | // } while ((match = this.ticker.exec(tweet.text)) !== null);
224 | // }
225 |
--------------------------------------------------------------------------------
/src/lib/strategies/Strategy.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | export default class Strategy {
3 | constructor(config) {
4 | // validate config
5 | if (!(config instanceof Map)) {
6 | throw new Error('config param must be of type "Map"');
7 | }
8 |
9 | this.home = config.get('home');
10 | this.brokers = config.get('brokers');
11 | this.api = config.get('api');
12 | this.db = config.get('db');
13 | this.name = 'strategy';
14 | this.log = config.get('log');
15 | if (this.log === undefined) {
16 | this.log = ({ level, log }) => {
17 | console.log(level, log);
18 | };
19 | }
20 | }
21 |
22 | async main() {
23 | throw new Error('main function must be implemented');
24 | }
25 |
26 | loop() {
27 | this.running = this.main();
28 | this.running.then(() => {
29 | this.running = undefined;
30 | }).catch((err) => {
31 | this.running = undefined;
32 | this.log({
33 | level: 'error',
34 | log: JSON.stringify({
35 | message: err.message,
36 | stack: err.stack,
37 | }),
38 | });
39 | });
40 | }
41 |
42 | start(interval) {
43 | this.log({
44 | level: 'info',
45 | log: `Starting strategy ${this.name}`,
46 | });
47 | // run first loop
48 | this.loop();
49 | // set interval for continuous looping
50 | this.mainInterval = setInterval(() => {
51 | if (this.running === undefined) {
52 | this.loop();
53 | }
54 | }, interval);
55 | }
56 |
57 | async stop() {
58 | this.log({
59 | level: 'info',
60 | log: `Stopping strategy ${this.name}`,
61 | });
62 | clearInterval(this.mainInterval);
63 | if (this.running) {
64 | await this.running;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/lib/trader.js:
--------------------------------------------------------------------------------
1 | import { DateTime } from 'luxon';
2 | import { eachOf } from 'async-es';
3 | import sqlite3 from 'sqlite3';
4 | import { open } from 'sqlite';
5 | import path from 'path';
6 | import api from './api';
7 | import brokers from './brokers';
8 | import ReverseSplitArbitrage from './strategies/ReverseSplitArbitrage';
9 |
10 | // declare db variable in the highest scope
11 | let db;
12 |
13 | // exit boolean determines whether we should abort at the end of a loop.
14 | // paused boolean indicates whether the loop is executing or sleeping.
15 | let initialized = false;
16 | const internal = {
17 | log: console.log,
18 | };
19 |
20 | // keep tabs on the process
21 | const watch = setInterval(() => {
22 | internal.log({
23 | level: 'debug',
24 | log: `\n${DateTime.local().toISO()} - process stats:\ncpu:\n${JSON.stringify(process.cpuUsage())}\nmemory:\n${JSON.stringify(process.memoryUsage())}`,
25 | });
26 | }, 600000);
27 |
28 | export function strategyAction({ action, strategy }) {
29 | if (Object.prototype.hasOwnProperty.call(internal.strategies, strategy.class)) {
30 | switch (action) {
31 | case 'add':
32 | case 'resume':
33 | internal.strategies[strategy.class].start(10000);
34 | break;
35 | case 'remove':
36 | case 'pause':
37 | internal.strategies[strategy.class].stop();
38 | break;
39 | default:
40 | break;
41 | }
42 | }
43 | }
44 |
45 | // graceful shutdown
46 | export async function gracefulShutdown() {
47 | internal.log({
48 | level: 'info',
49 | log: 'preparing for shutddown..',
50 | });
51 | await eachOf(internal.strategies, async (strategy, key, strategyCb) => {
52 | await strategy.stop();
53 | if (strategyCb) {
54 | return strategyCb();
55 | }
56 | return null;
57 | });
58 | clearInterval(watch);
59 | if (db && typeof db.close === 'function') {
60 | try {
61 | await db.close();
62 | internal.log({
63 | level: 'info',
64 | log: 'ready for shutdown.',
65 | });
66 | } catch (err) {
67 | internal.log({
68 | level: 'info',
69 | log: `forcing shutdown shutdown because of:\n${err.message}`,
70 | });
71 | }
72 | }
73 | }
74 |
75 | // TODO: create a stategy base class / interface <-- this!!
76 | /**
77 | * Name
78 | * Description
79 | * project_directory
80 | */
81 | // strategy must inherit base strategy class
82 | // Object.getPrototypeOf(CustomStrategy.constructor) === Strategy;
83 |
84 | // TODO: create an electron ui to monitor, config, review, and etc.
85 | // TODO: throw in some error handling
86 |
87 | export async function main(settings) {
88 | if (initialized) {
89 | return;
90 | }
91 | initialized = true;
92 | internal.log = settings.log;
93 | internal.log({
94 | level: 'info',
95 | log: 'initializing..\n',
96 | });
97 |
98 | // open the sqlite database
99 | // no try catch because we want app to fail if this isn't working.
100 | db = await open({
101 | filename: path.resolve(settings.home, 'trader.db'),
102 | driver: sqlite3.cached.Database,
103 | });
104 |
105 | // attach secondary databases
106 | const databases = (await db.all('PRAGMA database_list;')).map((o) => (o.name));
107 | if (databases.indexOf('twitter') === -1) {
108 | await db.run(`ATTACH DATABASE '${path.resolve(settings.home, 'twitter.db')}' AS twitter;`);
109 | }
110 |
111 | // wait for api initialization
112 | const API = await api(settings.credentials);
113 | internal.log({
114 | level: 'info',
115 | log: 'initialized api\'s',
116 | });
117 |
118 | // wait for broker initialization
119 | const Brokers = await brokers(internal.log, settings.credentials);
120 | internal.log({
121 | level: 'info',
122 | log: 'initialized brokers',
123 | });
124 |
125 | // get accounts
126 | const accounts = await Brokers.robinhood.getAccounts(db);
127 | internal.log({
128 | level: 'info',
129 | log: `Pulled data for ${accounts.length} robinhood accounts`,
130 | });
131 |
132 | // get order history
133 | const orders = await Brokers.robinhood.orderHistory(db);
134 | internal.log({
135 | level: 'info',
136 | log: `Transaction history downloaded... found ${orders.length} new records.`,
137 | });
138 |
139 | // get current positions
140 | const positions = await Brokers.robinhood.getPositions(db);
141 | internal.log({
142 | level: 'info',
143 | log: `Positions updated... ${(positions.filter((p) => (parseFloat(p.quantity) > 0))).length} stock(s) in portfolio.`,
144 | });
145 |
146 | // get portfolio historicals
147 | const historicals = await Brokers.robinhood.getPortfolioHistoricals({
148 | span: 'week',
149 | interval: '5minute',
150 | });
151 | console.log(historicals);
152 |
153 | // load user's elected strategies
154 | const elected = await db.all(`
155 | SELECT s.*, es.active, es.updated_at FROM elected_strategies es
156 | JOIN strategies s
157 | ON es.id = s.id;
158 | `);
159 |
160 | // TODO: run trading logic based on elected strategies
161 | const rsaConfig = new Map();
162 | rsaConfig.set('brokers', Brokers);
163 | rsaConfig.set('api', API);
164 | rsaConfig.set('home', settings.home);
165 | rsaConfig.set('db', db);
166 | rsaConfig.set('log', internal.log);
167 |
168 | internal.strategies = {
169 | ReverseSplitArbitrage: new ReverseSplitArbitrage(rsaConfig),
170 | };
171 |
172 | // start active strategies
173 | elected.forEach((electee) => {
174 | if (Object.prototype.hasOwnProperty.call(electee, 'active')
175 | && electee.active === 1) {
176 | if (Object.prototype.hasOwnProperty.call(electee, 'class')
177 | && Object.prototype.hasOwnProperty.call(internal.strategies, electee.class)) {
178 | internal.strategies[electee.class].start(10000);
179 | }
180 | }
181 | });
182 | }
183 |
--------------------------------------------------------------------------------
/src/logger/index.js:
--------------------------------------------------------------------------------
1 | import { createLogger, format, transports } from 'winston';
2 | import DailyRotateFile from 'winston-daily-rotate-file';
3 | import path from 'path';
4 |
5 | function getLogger(logDirectory) {
6 | const transport = [
7 | new DailyRotateFile({
8 | filename: path.resolve(logDirectory, 'info-%DATE%.log'),
9 | datePattern: 'YYYY-MM-DD',
10 | zippedArchive: true,
11 | maxSize: '20m',
12 | maxFiles: '14d',
13 | }),
14 | ];
15 |
16 | if (process.env.NODE_ENV !== 'production') {
17 | transport.push(
18 | new transports.Console({
19 | format: format.combine(
20 | format.colorize(),
21 | format.simple(),
22 | ),
23 | }),
24 | );
25 | }
26 |
27 | return createLogger({
28 | level: 'silly',
29 | format: format.combine(
30 | format.timestamp({
31 | format: 'YYYY-MM-DD HH:mm:ss.SSS',
32 | }),
33 | // wating on https://github.com/winstonjs/winston/issues/1724, see also https://github.com/winstonjs/logform/pull/106
34 | // format.errors({ stack: true }),
35 | format.splat(),
36 | format.json(),
37 | ),
38 | defaultMeta: { service: 'trader-bot' },
39 | transports: transport,
40 | });
41 | }
42 |
43 | export default getLogger;
44 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import 'roboto-fontface/css/roboto/roboto-fontface.css';
3 | import '@mdi/font/css/materialdesignicons.css';
4 | import vuetify from './plugins/vuetify';
5 | import App from './App.vue';
6 | import router from './router';
7 | import store from './store';
8 |
9 | Vue.config.productionTip = false;
10 |
11 | new Vue({
12 | router,
13 | store,
14 | vuetify,
15 | render: (h) => h(App),
16 | }).$mount('#app');
17 |
--------------------------------------------------------------------------------
/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuetify from 'vuetify/lib/framework';
3 |
4 | Vue.use(Vuetify);
5 |
6 | export default new Vuetify({
7 | });
8 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueRouter from 'vue-router';
3 | import Home from '../views/Home.vue';
4 | import Strategies from '../views/Strategies.vue';
5 | import Logs from '../views/Logs.vue';
6 | import Login from '../views/Login.vue';
7 | import Logout from '../views/Logout.vue';
8 |
9 | Vue.use(VueRouter);
10 |
11 | const routes = [
12 | {
13 | path: '/',
14 | name: 'Home',
15 | component: Home,
16 | },
17 | {
18 | path: '/strategies',
19 | name: 'Strategies',
20 | component: Strategies,
21 | },
22 | {
23 | path: '/logs',
24 | name: 'Logs',
25 | component: Logs,
26 | },
27 | {
28 | path: '/login',
29 | name: 'Login',
30 | component: Login,
31 | },
32 | {
33 | path: '/logout',
34 | name: 'Logout',
35 | component: Logout,
36 | },
37 | ];
38 |
39 | const router = new VueRouter({
40 | mode: 'history',
41 | base: process.env.BASE_URL,
42 | routes,
43 | });
44 |
45 | export default router;
46 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import equal from 'deep-equal';
4 |
5 | Vue.use(Vuex);
6 |
7 | export default new Vuex.Store({
8 | state: {
9 | cryptr: null,
10 | credentials: null,
11 | db: null,
12 | log: null,
13 | home: null,
14 | brokers: {},
15 | accounts: [],
16 | positions: [],
17 | },
18 | mutations: {
19 | updateCredentials(state, credentials) {
20 | state.credentials = credentials;
21 | },
22 | updateCryptr(state, cryptr) {
23 | state.cryptr = cryptr;
24 | },
25 | updateDb(state, db) {
26 | state.db = db;
27 | },
28 | updateLog(state, log) {
29 | state.log = log;
30 | },
31 | updateHome(state, home) {
32 | state.home = home;
33 | },
34 | updateAccounts(state, accounts) {
35 | if (!equal(state.accounts, accounts)) {
36 | state.accounts = accounts;
37 | }
38 | },
39 | updatePositions(state, positions) {
40 | if (!equal(state.postions, positions)) {
41 | state.positions = positions;
42 | }
43 | },
44 | addBroker(state, broker) {
45 | const brokers = {
46 | ...state.brokers,
47 | };
48 | brokers[broker.name] = broker.broker;
49 | state.brokers = brokers;
50 | },
51 | },
52 | actions: {
53 | },
54 | modules: {
55 | },
56 | });
57 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
92 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Login
6 |
7 |
8 |
9 |
10 | Save Credentials
11 |
12 |
13 |
14 |
15 |
16 | submit
17 |
18 |
19 |
20 |
21 |
177 |
--------------------------------------------------------------------------------
/src/views/Logout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Successfully logged out!
4 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/src/views/Logs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
14 |
79 |
--------------------------------------------------------------------------------
/src/views/Strategies.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
141 |
--------------------------------------------------------------------------------
/src/worker/Worker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Pay no attention to the man behind the curtain.
4 |
5 |
6 |
7 |
62 |
--------------------------------------------------------------------------------
/src/worker/worker.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Worker from './Worker.vue';
3 |
4 | Vue.config.productionTip = false;
5 |
6 | new Vue({
7 | render: (h) => h(Worker),
8 | }).$mount('#worker');
9 |
--------------------------------------------------------------------------------
/tests/unit/electron.spec.js:
--------------------------------------------------------------------------------
1 | import testWithSpectron from 'vue-cli-plugin-electron-builder/lib/testWithSpectron';
2 | import { expect } from 'chai';
3 | const spectron = __non_webpack_require__('spectron');
4 |
5 | describe('Application launch', function () {
6 | this.timeout(120000);
7 |
8 | beforeEach(async function () {
9 | const instance = await testWithSpectron(spectron)
10 | this.app = instance.app;
11 | this.stopServe = instance.stopServe;
12 | });
13 |
14 | afterEach(function () {
15 | if (this.app && this.app.isRunning()) {
16 | return this.app.mainProcess.exit(0);
17 | }
18 | });
19 |
20 | it('opens a window', async function () {
21 | await this.app.client.waitUntilWindowLoaded();
22 | expect(await this.app.client.getWindowCount()).to.be.at.least(2);
23 | expect(await this.app.browserWindow.isMinimized()).to.be.false;
24 | expect(await this.app.browserWindow.isVisible()).to.be.true;
25 | const { width, height } = await this.app.browserWindow.getBounds();
26 | expect(width).to.be.above(0);
27 | expect(height).to.be.above(0);
28 | });
29 |
30 | });
31 |
--------------------------------------------------------------------------------
/tests/unit/strategy-selector.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { shallowMount, createLocalVue } from '@vue/test-utils';
3 | import Vuex from 'vuex'
4 | import StrategySelector from '@/components/StrategySelector.vue';
5 |
6 | const localVue = createLocalVue();
7 | localVue.use(Vuex);
8 |
9 | describe('StrategySelector.vue', () => {
10 | let store;
11 |
12 | beforeEach(() => {
13 | store = new Vuex.Store({
14 | state: {
15 | cryptr: null,
16 | credentials: null,
17 | db: null,
18 | log: null,
19 | home: null,
20 | accounts: [],
21 | },
22 | });
23 | });
24 |
25 | it('renders a div', () => {
26 | const wrapper = shallowMount(StrategySelector, {
27 | propsData: { strategies: [], elected: [] },
28 | store,
29 | localVue,
30 | });
31 | expect(wrapper.contains('div')).to.true;
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | // vue.config.js
3 | // const TerserPlugin = require('terser-webpack-plugin');
4 |
5 | module.exports = {
6 | // options...
7 | pages: {
8 | index: 'src/main.js',
9 | worker: 'src/worker/worker.js',
10 | },
11 | pluginOptions: {
12 | electronBuilder: {
13 | externals: ['sqlite3', 'aws-sdk', 'electron-prompt'],
14 | nodeIntegration: true,
15 | builderOptions: {
16 | // options placed here will be merged with
17 | // default configuration and passed to electron-builder
18 | appId: 'com.fischerapps.trader-bot',
19 | productName: 'Trader Bot',
20 | },
21 | },
22 | },
23 | configureWebpack: {
24 | // devtool: 'source-map',
25 | devtool: 'inline-source-map',
26 | // optimization: {
27 | // minimize: true,
28 | // minimizer: [
29 | // new TerserPlugin({
30 | // terserOptions: {
31 | // mangle: false,
32 | // },
33 | // }),
34 | // ],
35 | // },
36 | },
37 | transpileDependencies: [
38 | 'vuetify',
39 | ],
40 | };
41 |
--------------------------------------------------------------------------------