├── .browserslistrc
├── .eslintignore
├── cypress.json
├── vue.config.js
├── cypress
├── fixtures
│ ├── backtest.json
│ ├── live.json
│ ├── importCandles.json
│ ├── deleteLive.json
│ ├── updateConfig.json
│ ├── deleteBacktest.json
│ ├── deleteImportCandles.json
│ ├── makeStrategy.json
│ ├── feedback.json
│ ├── login.json
│ ├── logout.json
│ ├── loginJesseTrade.json
│ ├── generalInfo.json
│ └── getConfig.json
├── server
│ └── index.js
├── support
│ ├── index.js
│ └── commands.js
├── plugins
│ └── index.js
└── integration
│ ├── importCandlePage.spec.js
│ ├── livePage.spec.js
│ ├── backtestPage.spec.js
│ └── navbar.spec.js
├── public
├── favicon.ico
└── index.html
├── babel.config.js
├── src
├── assets
│ ├── imgs
│ │ ├── equity-curve.png
│ │ ├── logo-light.png
│ │ ├── realtime-candle-chart.png
│ │ ├── logo-dark.svg
│ │ └── search-by-algolia-light-background.svg
│ ├── styles
│ │ ├── styles.css
│ │ ├── _animations.css
│ │ ├── _common.css
│ │ └── _tailwind.css
│ └── svg
│ │ ├── moon-stars.svg
│ │ └── sun.svg
├── components
│ ├── Heading.vue
│ ├── EmptyBox.vue
│ ├── Card.vue
│ ├── FullPageLoading.vue
│ ├── Functional
│ │ ├── DatePicker.vue
│ │ ├── ImageLoader.vue
│ │ ├── Spinner.vue
│ │ ├── FormTextarea.vue
│ │ ├── FormInput.vue
│ │ ├── NumberInput.vue
│ │ └── SlideOver.vue
│ ├── StatsBox.vue
│ ├── KeyValueTableSimple.vue
│ ├── Tooltip.vue
│ ├── Logs.vue
│ ├── LoadingSvg.vue
│ ├── DividerWithButtons.vue
│ ├── Divider.vue
│ ├── KeyValueTable.vue
│ ├── Checkbox.vue
│ ├── ThemeSwitch.vue
│ ├── TablePlaceholder.vue
│ ├── ToggleButton.vue
│ ├── Modals
│ │ ├── CustomModal.vue
│ │ └── ConfirmModal.vue
│ ├── Tables
│ │ ├── InfoLogsTable.vue
│ │ └── InfoTable.vue
│ ├── Alert.vue
│ ├── RadioGroups.vue
│ ├── MultipleValuesTable.vue
│ ├── Charts
│ │ ├── EquityCurve.vue
│ │ └── Candles
│ │ │ └── CandlesChart.vue
│ ├── UpdateBanner.vue
│ ├── Tabs.vue
│ └── Exception.vue
├── http.js
├── plugins
│ ├── notyf.js
│ ├── websocket.js
│ └── socketActions.js
├── notifier.js
├── views
│ ├── Candles.vue
│ ├── Loading.vue
│ ├── Backtest.vue
│ ├── Live.vue
│ ├── Optimization.vue
│ ├── About.vue
│ ├── Test.vue
│ ├── LiveOrders.vue
│ ├── MakeStrategy.vue
│ ├── ReportLiveSession.vue
│ ├── Feedback.vue
│ ├── Home.vue
│ ├── Login.vue
│ ├── HelpSearch.vue
│ └── tabs
│ │ └── CandlesTab.vue
├── layouts
│ └── LayoutWithSidebar.vue
├── App.vue
├── router
│ └── index.js
├── main.js
├── helpers.js
└── stores
│ ├── candles.js
│ ├── main.js
│ └── backtest.js
├── postcss.config.js
├── .editorconfig
├── jest.config.js
├── .env.example
├── .gitignore
├── README.md
├── tests
└── unit
│ ├── test-helpers.spec.js
│ └── TestSimpleComponents.spec.js
├── LICENSE
├── tailwind.config.js
├── .eslintrc.js
└── package.json
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 2 years
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | other
2 | public
3 | cypress
4 | tests
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://127.0.0.1:8080/"
3 | }
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // assetsDir: 'static'
3 | }
4 |
--------------------------------------------------------------------------------
/cypress/fixtures/backtest.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Started backtesting..."
3 | }
--------------------------------------------------------------------------------
/cypress/fixtures/live.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Started paper trading..."
3 | }
--------------------------------------------------------------------------------
/cypress/fixtures/importCandles.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Started importing candles..."
3 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jesse-ai/dashboard/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/cypress/fixtures/deleteLive.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Live process with ID of 1 terminated."
3 | }
--------------------------------------------------------------------------------
/cypress/fixtures/updateConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Updated configurations successfully"
3 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/fixtures/deleteBacktest.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Backtest process with ID of 1 terminated."
3 | }
--------------------------------------------------------------------------------
/cypress/fixtures/deleteImportCandles.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Candles process with ID of 1 terminated."
3 | }
--------------------------------------------------------------------------------
/cypress/fixtures/makeStrategy.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "success",
3 | "message": "strategies/test-strategy"
4 | }
--------------------------------------------------------------------------------
/src/assets/imgs/equity-curve.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jesse-ai/dashboard/HEAD/src/assets/imgs/equity-curve.png
--------------------------------------------------------------------------------
/src/assets/imgs/logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jesse-ai/dashboard/HEAD/src/assets/imgs/logo-light.png
--------------------------------------------------------------------------------
/cypress/fixtures/feedback.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "success",
3 | "message": "Feedback submitted successfully"
4 | }
--------------------------------------------------------------------------------
/cypress/fixtures/login.json:
--------------------------------------------------------------------------------
1 | {
2 | "auth_token": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
3 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/styles/styles.css:
--------------------------------------------------------------------------------
1 | @import "_tailwind.css";
2 | @import "_common.css";
3 | @import "_animations.css";
4 |
5 |
--------------------------------------------------------------------------------
/src/components/Heading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/cypress/fixtures/logout.json:
--------------------------------------------------------------------------------
1 | {
2 | "message": "Successfully logged out from Jesse.Trade and removed access_token file"
3 | }
--------------------------------------------------------------------------------
/cypress/fixtures/loginJesseTrade.json:
--------------------------------------------------------------------------------
1 | {
2 | "status": "success",
3 | "message": "Successfully logged in to Jesse.Trade"
4 | }
--------------------------------------------------------------------------------
/src/assets/imgs/realtime-candle-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jesse-ai/dashboard/HEAD/src/assets/imgs/realtime-candle-chart.png
--------------------------------------------------------------------------------
/src/http.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | axios.defaults.baseURL = process.env.VUE_APP_HTTP_PATH
4 |
5 | export default axios
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@vue/cli-plugin-unit-jest',
3 | transform: {
4 | '^.+\\.vue$': 'vue-jest'
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/styles/_animations.css:
--------------------------------------------------------------------------------
1 | .fade-enter-active,
2 | .fade-leave-active {
3 | transition: opacity 0.3s ease;
4 | }
5 | .fade-enter-from,
6 | .fade-leave-to {
7 | opacity: 0;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/EmptyBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Empty
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/assets/styles/_common.css:
--------------------------------------------------------------------------------
1 | /*svg {*/
2 | /* fill: currentColor;*/
3 | /*}*/
4 |
5 | pre {
6 | overflow: auto;
7 | width: 100%;
8 | font-size: 12px;
9 | padding-bottom: 10px;
10 | margin-bottom: 0;
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # For development create ".env.development.local" file
2 |
3 | VUE_APP_IS_DEBUG=yes
4 | # for production, set both of them to to "/" (yes even the ws)
5 | VUE_APP_SOCKET_PATH=ws://127.0.0.1:9000/ws
6 | VUE_APP_HTTP_PATH=http://127.0.0.1:9000
7 |
8 |
--------------------------------------------------------------------------------
/src/plugins/notyf.js:
--------------------------------------------------------------------------------
1 | import { Notyf } from 'notyf'
2 |
3 | export default {
4 | install: (app, settings) => {
5 | settings.position.x = 'left'
6 | app.config.globalProperties.notyf = new Notyf(settings)
7 | app.notyf = app.config.globalProperties.notyf
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/cypress/server/index.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws')
2 |
3 | const wss = new WebSocket.Server({ port: 8001 })
4 |
5 | wss.on('connection', function connection(ws) {
6 | const loginReply = JSON.stringify({
7 | type: 'CONNECTED',
8 | })
9 |
10 | ws.send(loginReply)
11 | })
12 |
--------------------------------------------------------------------------------
/src/notifier.js:
--------------------------------------------------------------------------------
1 | import { Notyf } from 'notyf'
2 | import 'notyf/notyf.min.css'
3 |
4 | const notifier = new Notyf({
5 | duration: 5000,
6 | dismissible: true,
7 | ripple: false,
8 | position: { x: 'left', y: 'bottom' },
9 | types: [
10 | {
11 | type: 'info',
12 | background: '#009efa'
13 | }
14 | ]
15 | })
16 |
17 | export default notifier
18 |
--------------------------------------------------------------------------------
/src/components/FullPageLoading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Loading...
4 |
5 |
This may take a few seconds, please wait
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 | .env
25 |
26 | package-lock.json
27 |
28 | cypress/videos
29 | cypress/screenshots
--------------------------------------------------------------------------------
/src/assets/svg/moon-stars.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/Functional/DatePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
21 |
--------------------------------------------------------------------------------
/cypress/fixtures/generalInfo.json:
--------------------------------------------------------------------------------
1 | {
2 | "exchanges": [
3 | "Binance",
4 | "Binance Futures",
5 | "Binance Inverse Futures",
6 | "Bitfinex",
7 | "Coinbase",
8 | "OKEX",
9 | "Testnet Binance Futures"
10 | ],
11 | "live_exchanges": [
12 | "Binance Futures",
13 | "FTX Futures",
14 | "Testnet Binance Futures"
15 | ],
16 | "strategies": [
17 | "ExampleStrategy",
18 | "TestLiveMode01"
19 | ],
20 | "has_live_plugin_installed": true,
21 | "is_logged_in_to_jesse_trade": true,
22 | "cpu_cores": 8
23 | }
--------------------------------------------------------------------------------
/src/components/StatsBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ name }}
5 |
6 |
7 | {{ value }}
8 |
9 |
10 |
11 |
12 |
13 |
28 |
--------------------------------------------------------------------------------
/src/assets/svg/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/components/Functional/ImageLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/KeyValueTableSimple.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 | {{ item[0] }}
7 |
8 |
9 | {{ item[1] }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
27 |
--------------------------------------------------------------------------------
/src/views/Candles.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/src/layouts/LayoutWithSidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
--------------------------------------------------------------------------------
/src/components/Tooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
35 |
--------------------------------------------------------------------------------
/src/views/Loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 | Loading...
11 |
12 |
13 |
14 |
15 |
16 |
21 |
--------------------------------------------------------------------------------
/src/components/Logs.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
33 |
--------------------------------------------------------------------------------
/src/views/Backtest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
--------------------------------------------------------------------------------
/src/views/Live.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/src/components/LoadingSvg.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
9 |
10 |
25 |
26 |
--------------------------------------------------------------------------------
/src/components/DividerWithButtons.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/src/views/Optimization.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | // eslint-disable-next-line no-unused-vars
19 | module.exports = (on, config) => {
20 | // `on` is used to hook into various events Cypress emits
21 | // `config` is the resolved Cypress config
22 | }
23 |
--------------------------------------------------------------------------------
/src/views/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Close
8 |
9 |
10 |
11 |
12 |
13 |
31 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/src/components/Functional/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dashboard
2 |
3 | This is the source code for the front end of Jesse's GUI dashboard.
4 |
5 | It is made using:
6 | - [VueCLI](https://cli.vuejs.org)
7 | - [VueJS 3](https://vuejs.org)
8 | - [TailwindCSS](https://tailwindcss.com)
9 |
10 | ## Project setup
11 |
12 | To install the dependencies, run:
13 |
14 | ```sh
15 | npm install
16 | ```
17 |
18 | To compile and hot-reload for development:
19 |
20 | ```sh
21 | npm run serve
22 | ```
23 |
24 | After doing so, you'll the URL, which by default is `http://localhost:8080`. To use it with Jesse, you also need to run Jesse using the good old `jesse run` command.
25 |
26 | to compile and minifies for production:
27 | ```sh
28 | npm run build
29 | ```
30 |
31 | Lints and fixes files:
32 |
33 | ```sh
34 | npm run lint
35 | ```
36 |
37 | ## Credits
38 |
39 | Many thanks to the contributors of this repository especially [Nicolay Zlobin](https://github.com/nicolay-zlobin) and [Morteza](https://github.com/morteza-koohgard) who helped a lot with the development of the initial version of the dashboard.
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Jesse
9 |
17 |
18 |
19 |
20 | We're sorry but Jesse doesn't work properly without JavaScript
21 | enabled. Please enable it to continue.
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/tests/unit/test-helpers.spec.js:
--------------------------------------------------------------------------------
1 | import helpers from '@/helpers'
2 |
3 |
4 | test('test helpers.currentTime()', () => {
5 | expect(helpers.currentTime())
6 | .toBe(new Date().toISOString().slice(11, 19))
7 | })
8 |
9 | test('test helpers.timestampToTime()', () => {
10 | expect(helpers.timestampToTime(1588888888000))
11 | .toBe('2020-05-07 22:01:28')
12 | })
13 |
14 | // test helpers.roundPrice() which is A helper function that rounds the input to 2 decimals but only if the number is bigger than 1.
15 | test('helpers.roundPrice()', () => {
16 | // for smaller than 1, it should stay the same
17 | expect(helpers.roundPrice(0.123456789))
18 | .toBe(0.123456789)
19 |
20 | // for bigger than 1, it should round to 2 decimals
21 | expect(helpers.roundPrice(1.123456789))
22 | .toBe(1.12)
23 |
24 | // if type of the input is not a number, return the input
25 | expect(helpers.roundPrice(undefined))
26 | .toBe(undefined)
27 | expect(helpers.roundPrice(null))
28 | .toBe(null)
29 | expect(helpers.roundPrice('string'))
30 | .toBe('string')
31 | })
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jesse
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/views/Test.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
Charts
9 |
10 |
11 | Equity curve
12 |
13 |
14 |
17 |
18 |
21 | Get random data
22 |
23 |
24 |
25 |
26 |
49 |
--------------------------------------------------------------------------------
/src/components/Functional/FormTextarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ title }}
4 |
5 |
11 |
12 |
{{ description }}
13 |
14 |
15 |
16 |
17 |
50 |
--------------------------------------------------------------------------------
/src/components/Divider.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
44 |
--------------------------------------------------------------------------------
/src/components/KeyValueTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 | {{ d[0] }}
12 |
13 |
14 | {{ d[1] }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
37 |
--------------------------------------------------------------------------------
/src/components/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
{{ title }}
13 |
{{ description }}
14 |
15 |
16 |
17 |
18 |
42 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
57 |
--------------------------------------------------------------------------------
/src/components/ThemeSwitch.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
11 |
12 |
13 |
14 |
54 |
--------------------------------------------------------------------------------
/src/components/Functional/FormInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ title }}
5 |
6 |
15 |
16 |
23 |
24 |
{{ description }}
25 |
26 |
27 |
28 |
29 |
68 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors')
2 |
3 | module.exports = {
4 | mode: 'jit',
5 | purge: ['./index.html', './src/**/*.vue', './src/**/*.js'],
6 | darkMode: 'class',
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: '1rem'
11 | },
12 | colors: {
13 | transparent: 'transparent',
14 | current: 'currentColor',
15 | black: colors.black,
16 | green: colors.teal,
17 | red: colors.red,
18 | white: colors.white,
19 | gray: colors.trueGray,
20 | indigo: colors.indigo,
21 | yellow: colors.amber,
22 | 'cool-gray': colors.coolGray,
23 | pink: colors.pink,
24 | blue: colors.blue,
25 | purple: colors.purple,
26 | primary: {
27 | DEFAULT: '#4f46e5',
28 | dark: '#f9b537'
29 | },
30 | // Background colors
31 | backdrop: {
32 | DEFAULT: '#ffffff',
33 | dark: '#333333'
34 | },
35 | // Background secondary colors
36 | 'backdrop-secondary': {
37 | DEFAULT: '#f3f3f6',
38 | dark: '#282828'
39 | },
40 | // Text colors
41 | body: {
42 | DEFAULT: '#333333',
43 | dark: '#f6f7ee'
44 | }
45 | },
46 | extend: {
47 | fontSize: {
48 | xl: ['30px', '36px'], // H1
49 | lg: ['24px', '32px'], // H2
50 | base: ['16px', '24px'],
51 | sm: ['14px', '20px'],
52 | caption: ['12px', '16px']
53 | },
54 | }
55 | },
56 | variants: {
57 | extend: {}
58 | },
59 | plugins: [
60 | require('@tailwindcss/forms')
61 | ],
62 | corePlugins: {}
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/TablePlaceholder.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
--------------------------------------------------------------------------------
/src/views/LiveOrders.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
54 |
--------------------------------------------------------------------------------
/tests/unit/TestSimpleComponents.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import Divider from '../../src/components/Divider.vue'
3 | import Logs from '../../src/components/Logs.vue'
4 | import Checkbox from '../../src/components/Checkbox.vue'
5 |
6 | test('test divider component', () => {
7 | const wrapper = mount(Divider, {
8 | slots: {
9 | default: 'test content'
10 | }
11 | })
12 | // check divider component display sended content in slot tag
13 | expect(wrapper.html()).toContain('test content')
14 | })
15 |
16 | test('test logs component', () => {
17 | const wrapper = mount(Logs, {
18 | props: {
19 | logs: ''
20 | }
21 | })
22 | // first check logs message when there is no logs
23 | expect(wrapper.html()).toContain('No logs available yet')
24 |
25 | // now add data to logs
26 | const loggedWrapper = mount(Logs, {
27 | props: {
28 | logs: 'test content'
29 | }
30 | })
31 | // now logs component display logs messages
32 | expect(loggedWrapper.html()).toContain('test content')
33 | })
34 |
35 | test('test checkbox component', () => {
36 | const wrapper = mount(Checkbox, {
37 | props: {
38 | title: 'test title',
39 | description: 'test description',
40 | object: { test: false },
41 | name: 'test'
42 | }
43 | })
44 |
45 | // check checkbox component display title and description
46 | expect(wrapper.html()).toContain('test title')
47 | expect(wrapper.html()).toContain('test description')
48 |
49 | // check click on input form, set checkbox value to true and object data will change
50 | wrapper.find('[data-test="checkboxInput"]').setValue(true)
51 | expect(wrapper.props().object).toEqual({ test: true })
52 | })
--------------------------------------------------------------------------------
/src/components/Functional/NumberInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 | -
10 |
11 |
12 |
16 |
17 |
18 | +
19 |
20 |
21 |
22 |
23 |
24 |
65 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 |
4 | env: {
5 | node: true
6 | },
7 |
8 | extends: [
9 | 'plugin:vue/vue3-recommended',
10 | '@vue/standard'
11 | ],
12 |
13 | parserOptions: {
14 | parser: 'babel-eslint'
15 | },
16 |
17 | rules: {
18 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
20 | // Custom rules below
21 | 'vue/singleline-html-element-content-newline': 'off',
22 | 'vue/html-closing-bracket-spacing': 'off',
23 | 'vue/html-closing-bracket-newline': 'off',
24 | 'vue/no-template-shadow': 'off',
25 | 'no-multiple-empty-lines': 0,
26 | 'no-trailing-spaces': process.env.NODE_ENV === 'production' ? 'warn' : 'warn',
27 | 'space-before-function-paren': ['error', {
28 | anonymous: 'always',
29 | named: 'always',
30 | asyncArrow: 'always'
31 | }],
32 | 'prefer-const': process.env.NODE_ENV === 'production' ? 'error' : 'error',
33 | 'padded-blocks': process.env.NODE_ENV === 'production' ? 'warn' : 'warn',
34 | 'comma-dangle': 0,
35 | 'no-unused-vars': process.env.NODE_ENV === 'production' ? 'warn' : 'warn',
36 | 'vue/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'error',
37 | 'vue/no-unused-components': process.env.NODE_ENV === 'production' ? 'error' : 'error',
38 | 'vue/max-attributes-per-line': ['error', {
39 | singleline: 4,
40 | multiline: {
41 | max: 4,
42 | allowFirstLine: true
43 | }
44 | }],
45 | 'no-extra-semi': 'warn',
46 | 'vue/no-mutating-props': 'off',
47 | 'vue/no-v-html': 'off'
48 | },
49 |
50 | overrides: [
51 | {
52 | files: [
53 | '**/__tests__/*.{j,t}s?(x)',
54 | '**/tests/unit/**/*.spec.{j,t}s?(x)'
55 | ],
56 | env: {
57 | jest: true
58 | }
59 | }
60 | ]
61 | }
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jesse-dashboard",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "test": "vue-cli-service test:unit",
9 | "lint": "vue-cli-service lint",
10 | "cypress": "cypress run",
11 | "cypress-browser": "cypress open",
12 | "start-cypress-server": "nodemon --watch server cypress/server/index.js"
13 | },
14 | "dependencies": {
15 | "@headlessui/vue": "^1.3.0",
16 | "@heroicons/vue": "^1.0.1",
17 | "@sentry/tracing": "^6.14.1",
18 | "@sentry/vue": "^6.14.1",
19 | "@tailwindcss/forms": "^0.3.3",
20 | "axios": "^0.21.1",
21 | "core-js": "^3.6.5",
22 | "dayjs": "^1.10.5",
23 | "lightweight-charts": "^3.3.0",
24 | "lodash": "^4.17.21",
25 | "notyf": "^3.10.0",
26 | "pinia": "^2.0.0-beta.3",
27 | "vue": "^3.1.1",
28 | "vue-inline-svg": "^3.0.0-beta.3",
29 | "vue-router": "^4.0.10",
30 | "websocket-as-promised": "^2.0.1",
31 | "ws": "^8.2.3"
32 | },
33 | "devDependencies": {
34 | "@vue/cli-plugin-babel": "~5.0.0-beta.2",
35 | "@vue/cli-plugin-eslint": "~5.0.0-beta.2",
36 | "@vue/cli-plugin-router": "~5.0.0-beta.2",
37 | "@vue/cli-plugin-unit-jest": "~4.5.0",
38 | "@vue/cli-plugin-vuex": "~5.0.0-beta.2",
39 | "@vue/cli-service": "~5.0.0-beta.2",
40 | "@vue/compiler-sfc": "^3.0.0",
41 | "@vue/eslint-config-standard": "^6.0.0",
42 | "@vue/test-utils": "^2.0.0-0",
43 | "autoprefixer": "^10.2.6",
44 | "babel-eslint": "^10.1.0",
45 | "cypress": "^8.5.0",
46 | "eslint": "^7.28.0",
47 | "eslint-plugin-import": "^2.20.2",
48 | "eslint-plugin-node": "^11.1.0",
49 | "eslint-plugin-promise": "^5.1.0",
50 | "eslint-plugin-standard": "^5.0.0",
51 | "eslint-plugin-vue": "^7.0.0",
52 | "nodemon": "^2.0.13",
53 | "postcss": "^8.3.0",
54 | "tailwindcss": "^2.2.0",
55 | "typescript": "~3.9.3",
56 | "vue-jest": "^5.0.0-0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router'
2 | import { useMainStore } from '@/stores/main'
3 |
4 | // Views
5 | import Candles from '@/views/Candles'
6 | import Backtest from '@/views/Backtest'
7 | import Test from '@/views/Test' // For debug purpose
8 | import Live from '@/views/Live'
9 | import Optimization from '@/views/Optimization'
10 |
11 | import { watch } from 'vue'
12 | import Home from '@/views/Home'
13 |
14 | // Check whether socket is connected or not
15 | const isSocketConnected = (to, from, next) => {
16 | const store = useMainStore()
17 |
18 | if (store.isConnected) {
19 | next()
20 | } else {
21 | const unwatch = watch(store,
22 | (state) => {
23 | if (state.isConnected) {
24 | unwatch()
25 | next()
26 | }
27 | },
28 | { deep: true }
29 | )
30 | }
31 | }
32 |
33 | const routes = [
34 | { path: '/backtest', redirect: '/backtest/1' },
35 | { path: '/candles', redirect: '/candles/1' },
36 | { path: '/live', redirect: '/live/1' },
37 | { path: '/optimization', redirect: '/optimization/1' },
38 | {
39 | path: '/',
40 | component: Home,
41 | name: 'Home',
42 | beforeEnter: isSocketConnected,
43 | },
44 | {
45 | path: '/candles/:id',
46 | component: Candles,
47 | name: 'Candles',
48 | beforeEnter: isSocketConnected,
49 | },
50 | {
51 | path: '/backtest/:id',
52 | component: Backtest,
53 | name: 'Backtest',
54 | beforeEnter: isSocketConnected,
55 | },
56 | {
57 | path: '/live/:id',
58 | component: Live,
59 | name: 'Live',
60 | beforeEnter: isSocketConnected,
61 | },
62 | {
63 | path: '/optimization/:id',
64 | component: Optimization,
65 | name: 'Optimization',
66 | beforeEnter: isSocketConnected,
67 | },
68 | {
69 | path: '/test',
70 | component: Test,
71 | name: 'Test',
72 | beforeEnter: isSocketConnected,
73 | },
74 | ]
75 |
76 | const router = createRouter({
77 | history: createWebHashHistory(process.env.BASE_URL),
78 | mode: 'hash',
79 | routes
80 | })
81 |
82 | export default router
83 |
--------------------------------------------------------------------------------
/src/components/ToggleButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ title }} ({{ disabledGuide }})
6 |
7 | {{ description }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
63 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp, markRaw } from 'vue'
2 | import App from './App.vue'
3 | import router from './router'
4 | // Plugins
5 | import websocket from './plugins/websocket'
6 | // Sentry
7 | // import * as Sentry from '@sentry/vue'
8 | // import { Integrations } from '@sentry/tracing'
9 |
10 | import './assets/styles/styles.css'
11 |
12 |
13 | import { createPinia } from 'pinia'
14 | const pinia = createPinia()
15 |
16 | // create Vue instance
17 | const app = createApp(App)
18 |
19 | // // sentry
20 | // Sentry.init({
21 | // app,
22 | // dsn: 'https://f5d14d55118a4ed5895272599a63ec60@sentry.jesse.trade/2',
23 | // integrations: [
24 | // new Integrations.BrowserTracing({
25 | // routingInstrumentation: Sentry.vueRouterInstrumentation(router),
26 | // // tracingOrigins: ['localhost', 'my-site-url.com', /^\//],
27 | // // tracingOrigins: ['localhost', /^\//],
28 | // // allow tracingOrigins to be anything using regex
29 | // tracingOrigins: [/^\//],
30 | // }),
31 | // ],
32 | // // Set tracesSampleRate to 1.0 to capture 100%
33 | // // of transactions for performance monitoring.
34 | // // We recommend adjusting this value in production
35 | // tracesSampleRate: 1.0,
36 | // logErrors: true,
37 | // })
38 |
39 | app.use(pinia)
40 | app.use(router)
41 |
42 | let wsPath = ''
43 | if (process.env.VUE_APP_SOCKET_PATH === '/') {
44 | wsPath = ((window.location.protocol === 'https:') ? 'wss://' : 'ws://') + window.location.host + '/ws'
45 | } else {
46 | wsPath = `${process.env.VUE_APP_SOCKET_PATH}`
47 | }
48 | app.use(websocket, {
49 | socketPath: wsPath
50 | })
51 | pinia.use(({ store }) => {
52 | store.$router = markRaw(router)
53 | })
54 |
55 | // import and register vue components globally
56 | const files = require.context('./components', true, /\.vue$/i)
57 | files.keys().map(key =>
58 | app.component(
59 | key
60 | .split('/')
61 | .pop()
62 | .split('.')[0],
63 | files(key).default
64 | )
65 | )
66 |
67 | app.mount('#app')
68 |
69 | // display a warning asking user if he's sure about closing the window when they try to close the browser tab
70 | window.addEventListener('beforeunload', (event) => {
71 | event.returnValue = 'Are you sure you want to leave?'
72 | })
73 |
--------------------------------------------------------------------------------
/src/components/Modals/CustomModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
56 |
--------------------------------------------------------------------------------
/src/components/Tables/InfoLogsTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 | Info logs
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 16:14:01
21 |
22 |
23 |
24 | OPENED short position: Binance Futures, BTC-USDT, -0.178, $38042.2
25 |
26 |
27 |
28 |
29 |
30 |
31 | 16:14:01
32 |
33 |
34 |
35 | Charged 2.03 as fee. Balance for USDT on Binance.
36 |
37 |
38 |
39 |
40 |
41 |
42 | 16:14:01
43 |
44 |
45 |
46 | EXECUTED order: BTC-USDT, MARKET, sell, -0.178, $38042.2
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
71 |
72 |
--------------------------------------------------------------------------------
/src/views/MakeStrategy.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Filling this form will create a new strategy class with all the starting methods in it.
5 |
6 |
7 |
8 |
9 |
Strategy name:
10 |
16 |
17 |
18 |
19 |
The strategy will be located at:
20 |
23 | {{ `strategies/${form.name}/__init__.py` }}
24 |
25 |
26 |
27 |
28 | Cancel
29 | Create
30 |
31 |
32 |
33 |
34 |
77 |
--------------------------------------------------------------------------------
/src/components/Alert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
16 | Dismiss
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
89 |
--------------------------------------------------------------------------------
/src/assets/styles/_tailwind.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss/base";
2 |
3 | @layer base {
4 | html {
5 | @apply text-body bg-backdrop;
6 | }
7 |
8 | html.dark {
9 | @apply text-body-dark bg-backdrop-dark;
10 | }
11 |
12 | h1, .h1 {
13 | @apply text-xl;
14 | }
15 |
16 | h2, .h2 {
17 | @apply text-lg;
18 | }
19 |
20 | h3, .h3 {
21 | @apply text-base font-semibold;
22 | }
23 | }
24 |
25 | @import "tailwindcss/components";
26 | @import "tailwindcss/utilities";
27 |
28 | @layer components {
29 | .btn-primary {
30 | @apply font-medium select-none items-center px-2.5 py-1.5 border border-transparent rounded shadow-sm text-white bg-indigo-600 dark:bg-indigo-400 hover:bg-indigo-700 dark:hover:bg-indigo-300 focus:outline-none dark:text-black text-base tracking-wide;
31 | }
32 |
33 | .btn-secondary {
34 | @apply font-medium select-none items-center shadow-sm px-2.5 py-1.5 rounded text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-700 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600 hover:bg-gray-100 focus:outline-none text-base tracking-wide;
35 | }
36 |
37 | .btn-cancel {
38 | @apply font-medium select-none items-center shadow-sm px-2.5 py-1.5 rounded text-red-500 dark:text-red-400 bg-white dark:bg-gray-700 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600 hover:bg-gray-100 focus:outline-none text-base tracking-wide;
39 | }
40 |
41 | .btn-danger {
42 | @apply font-medium select-none items-center justify-center px-2.5 py-1.5 border border-transparent rounded shadow-sm text-white bg-red-600 hover:bg-red-500 focus:outline-none tracking-wide;
43 | }
44 |
45 | .btn-success {
46 | @apply font-medium select-none items-center justify-center px-2.5 py-1.5 border border-transparent rounded shadow-sm text-white bg-green-600 hover:bg-green-500 focus:outline-none tracking-wide;
47 | }
48 |
49 | .btn-link {
50 | @apply font-medium select-none items-center hover:underline focus:outline-none text-base tracking-wide;
51 | }
52 |
53 | .btn-nav {
54 | @apply ml-2 p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-full text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 focus:outline-none
55 | }
56 |
57 | .input {
58 | @apply block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-400 focus:border-indigo-400 sm:text-sm dark:bg-gray-800 dark:border-gray-900
59 | }
60 |
61 | .input-label {
62 | @apply block text-sm font-medium text-gray-700 dark:text-gray-300
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/views/ReportLiveSession.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | If you think something is wrong with your running live session, you can submit a report.
4 | By submitting this form, the logs of this session will be sent to Jesse's developers so we can see what's going on.
5 |
6 |
7 | Your exchange API keys and strategies are safe and are never sent to us.
8 |
9 |
10 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
30 | Cancel
31 | Submit
32 |
33 |
34 |
35 |
90 |
--------------------------------------------------------------------------------
/src/views/Feedback.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | I would love to hear your feedback whether it's about a bug, suggestion, something you like, or something you hate about Jesse!
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
31 | Cancel
32 |
33 |
34 |
37 | Submit
38 |
39 |
40 |
41 |
42 |
43 |
89 |
--------------------------------------------------------------------------------
/cypress/integration/importCandlePage.spec.js:
--------------------------------------------------------------------------------
1 | const { default: axios } = require('axios')
2 |
3 | describe('test home page', () => {
4 | beforeEach(() => {
5 | // mock important requests
6 | cy.intercept('post', '/auth', { fixture: 'login.json' }).as('login')
7 | cy.intercept('post', '/general-info', { fixture: 'generalInfo.json' }).as('generalInfo')
8 | cy.intercept('post', '/get-config', { fixture: 'getConfig.json' }).as('getConfig')
9 | cy.intercept('post', '/update-config', { fixture: 'updateConfig.json' }).as('updateConfig')
10 | cy.intercept('post', '/import-candles', { fixture: 'importCandles.json' }).as('importCandles')
11 | cy.intercept('delete', '/import-candles', { fixture: 'deleteImportCandles.json' }).as('deleteImportCandles')
12 |
13 | // remove cookies and storage
14 | sessionStorage.auth_key = null
15 | axios.defaults.headers.common.Authorization = null
16 | // visit first page and type password
17 | cy.visit('/')
18 | cy.contains('Welcome Back!')
19 | cy.get('input').type('test')
20 | cy.get('button').click()
21 | cy.get('#import-candles-page-button').click()
22 | cy.wait(50)
23 | })
24 |
25 | it('test import candles page', () => {
26 | // test inputs of import candle candles pages
27 | cy.get('[data-cy="candles-exchange"]').contains('Testnet Binance Futures')
28 | cy.get('[data-cy="candles-exchange"]').contains('Bitfinex')
29 | cy.get('[data-cy="candles-exchange"]').select('OKEX')
30 | cy.get('[data-cy="candles-exchange"]').should('have.value', 'OKEX')
31 | // check symbols errors
32 | cy.get('[data-cy="candles-symbol"]').type('sometext')
33 | cy.get('[data-cy="symbol-error-section"]').should('contain.text', 'Symbol parameter must contain "-" character!')
34 | cy.get('[data-cy="candles-symbol"]').type('somesome')
35 | cy.get('[data-cy="symbol-error-section"]').should('contain.text', 'Maximum symbol length is exceeded!')
36 | cy.get('[data-cy="candles-start-date"]').should('have.value', '2021-01-01')
37 |
38 | // test start button
39 | cy.get('[data-cy="start-button"]').click()
40 | cy.wait(50)
41 | cy.contains('Please wait')
42 | // press cancel button
43 | cy.get('[data-cy="import-candles-cancel-button"]').click()
44 | cy.wait(50)
45 | cy.get('[data-cy="candles-page-content"]').should('include.text', 'Exchange')
46 |
47 | // test start in new tab button
48 | cy.get('[data-cy="start-new-tab-button"]').click()
49 | cy.wait(50)
50 | cy.get('[data-cy="tab1"]').click()
51 | cy.wait(50)
52 | cy.contains('Please wait')
53 | // press cancel button
54 | cy.get('[data-cy="import-candles-cancel-button"]').click()
55 | cy.wait(50)
56 | cy.get('[data-cy="candles-page-content"]').should('include.text', 'Start Date')
57 | })
58 | })
--------------------------------------------------------------------------------
/src/components/RadioGroups.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ title }}
4 |
5 |
6 |
7 | {{ title }}
8 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ setting }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
80 |
--------------------------------------------------------------------------------
/src/plugins/websocket.js:
--------------------------------------------------------------------------------
1 | import WebSocketAsPromised from 'websocket-as-promised'
2 | import socketActions from '@/plugins/socketActions'
3 |
4 | import { useMainStore } from '@/stores/main'
5 |
6 | export default {
7 | install: (app, settings) => {
8 | const mainStore = useMainStore()
9 |
10 | const loginWatchInterval = setInterval(function () {
11 | if (mainStore.isAuthenticated) {
12 | let url = settings.socketPath
13 | if (window.Cypress) {
14 | url = 'ws://127.0.0.1:8001/ws'
15 | }
16 |
17 | const wsp = new WebSocketAsPromised(`${url}?token=${sessionStorage.auth_key}`, {
18 | packMessage: data => JSON.stringify(data),
19 | unpackMessage: data => JSON.parse(data),
20 | attachRequestId: (data, requestId) => Object.assign({ id: requestId }, data),
21 | extractRequestId: data => data && data.id
22 | })
23 |
24 | let openIntervalId = null
25 | let reopenAttempts = 3
26 |
27 | function wsOpen () {
28 | wsp.open()
29 | .then(async () => {
30 | mainStore.isConnected = true
31 |
32 | if (openIntervalId) {
33 | clearInterval(openIntervalId)
34 | }
35 |
36 | if (reopenAttempts < 3) {
37 | app.notyf.success('WebSocket reconnected')
38 | }
39 |
40 | // Reset reopen attempts after ws reopened
41 | reopenAttempts = 3
42 | })
43 | .catch(error => {
44 | console.error('Socket encountered error.', error)
45 |
46 | if (process.env.VUE_APP_IS_DEBUG === 'yes') {
47 | app.notyf.error(error.message)
48 | }
49 | })
50 | }
51 |
52 |
53 |
54 | wsp.onClose.addListener(async () => {
55 | console.log('Connection closed.')
56 |
57 | mainStore.isConnected = false
58 | if (openIntervalId) clearInterval(openIntervalId)
59 |
60 | if (reopenAttempts > 0) {
61 | // Trying to re-open web-socket after close
62 | openIntervalId = setInterval(() => {
63 | console.log('Trying to re-open web-socket')
64 | reopenAttempts--
65 | wsOpen()
66 | }, 3000)
67 | } else {
68 | console.log('Socket can\'t re-establish connection!')
69 | }
70 | })
71 |
72 | // Listen ws events and pass data to Pinia's actions
73 | wsp.onUnpackedMessage.addListener(async message => {
74 | const event = message.event
75 | const data = message.data
76 | const id = message.id
77 | const actions = socketActions().get(event)
78 |
79 | // console.log(event, id, data)
80 |
81 | if (actions !== undefined) {
82 | actions.forEach(action => {
83 | action(id, data)
84 | })
85 | }
86 | })
87 |
88 | app.config.globalProperties.$wsp = wsp
89 | app.wsp = app.config.globalProperties.$wsp
90 | wsOpen()
91 | clearInterval(loginWatchInterval)
92 | }
93 | }, 1000)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/Functional/SlideOver.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
9 |
10 |
11 |
40 |
41 |
42 |
43 |
44 |
45 |
80 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import utc from 'dayjs/plugin/utc'
3 | import _ from 'lodash'
4 | dayjs.extend(utc)
5 |
6 | const helpers = {
7 | timestampToTime (timestamp) {
8 | return dayjs(parseInt(timestamp)).utc().format('YYYY-MM-DD HH:mm:ss')
9 | },
10 |
11 | timestampToTimeOnly (timestamp) {
12 | return dayjs(parseInt(timestamp)).utc().format('HH:mm:ss')
13 | },
14 |
15 | timestampToDate (timestamp) {
16 | return dayjs(parseInt(timestamp)).utc().format('YYYY-MM-DD')
17 | },
18 |
19 | currentTime () {
20 | return dayjs().utc().format('HH:mm:ss')
21 | },
22 |
23 | localStorageSet (key, value) {
24 | localStorage.setItem(key, JSON.stringify(value))
25 | },
26 |
27 | localStorageGet (key) {
28 | return JSON.parse(localStorage.getItem(key))
29 | },
30 |
31 | getDefaultFromLocalStorage (key, defaultForm) {
32 | return _.merge(defaultForm, helpers.localStorageGet(key))
33 | },
34 |
35 | secondsToHumanReadable (seconds) {
36 | const hours = Math.floor(seconds / 3600)
37 | const minutes = Math.floor((seconds - (hours * 3600)) / 60)
38 | const secondsLeft = _.round(
39 | seconds - (hours * 3600) - (minutes * 60),
40 | 2
41 | )
42 | return `${hours}h ${minutes}m ${secondsLeft}s`
43 | },
44 |
45 | remainingTimeText (seconds) {
46 | if (Math.round(seconds) === 0) {
47 | return 'Please wait...'
48 | }
49 |
50 | if (seconds > 60) {
51 | const remainingSecondsInReadableFormat = this.secondsToHumanReadable(
52 | seconds
53 | )
54 |
55 | return `${remainingSecondsInReadableFormat} remaining...`
56 | }
57 |
58 | return `${Math.round(seconds)} seconds remaining...`
59 | },
60 |
61 | /**
62 | * A helper function that rounds the input to 2 decimals but only if the number is bigger than 1.
63 | * Used for displaying prices
64 | */
65 | roundPrice (price) {
66 | if (price > 1) {
67 | return _.round(price, 2)
68 | }
69 |
70 | return price
71 | },
72 |
73 | colorBasedOnSide (orderSide) {
74 | if (orderSide === 'buy') {
75 | return 'text-green-600 dark:text-green-400'
76 | } else if (orderSide === 'sell') {
77 | return 'text-red-500 dark:text-red-400'
78 | } else {
79 | return 'text-gray-900 dark:text-gray-200'
80 | }
81 | },
82 |
83 | colorBasedOnType (positionType) {
84 | if (positionType === 'long') {
85 | return 'text-green-600 dark:text-green-400'
86 | } else if (positionType === 'short') {
87 | return 'text-red-500 dark:text-red-400'
88 | } else {
89 | return 'text-gray-900 dark:text-gray-200'
90 | }
91 | },
92 |
93 | colorBasedOnNumber (num) {
94 | if (num > 0) {
95 | return 'text-green-600 dark:text-green-400'
96 | } else if (num < 0) {
97 | return 'text-red-500 dark:text-red-400'
98 | } else {
99 | return 'text-gray-900 dark:text-gray-200'
100 | }
101 | },
102 |
103 | currentTheme () {
104 | if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
105 | return 'dark'
106 | } else {
107 | return 'light'
108 | }
109 | },
110 | }
111 |
112 | export default helpers
113 |
--------------------------------------------------------------------------------
/cypress/integration/livePage.spec.js:
--------------------------------------------------------------------------------
1 | const { default: axios } = require('axios')
2 |
3 | describe('test home page', () => {
4 | beforeEach(() => {
5 | // mock important requests
6 | cy.intercept('post', '/auth', { fixture: 'login.json' }).as('login')
7 | cy.intercept('post', '/general-info', { fixture: 'generalInfo.json' }).as('generalInfo')
8 | cy.intercept('post', '/get-config', { fixture: 'getConfig.json' }).as('getConfig')
9 | cy.intercept('post', '/update-config', { fixture: 'updateConfig.json' }).as('updateConfig')
10 | cy.intercept('post', '/logout-jesse-trade', { fixture: 'logout.json' }).as('logout')
11 | cy.intercept('post', '/live', { fixture: 'live.json' }).as('live')
12 | cy.intercept('delete', '/live', { fixture: 'deleteLive.json' }).as('deleteLive')
13 | cy.intercept('post', '/login-jesse-trade', { fixture: 'loginJesseTrade.json' }).as('loginJesseTrade')
14 |
15 | // remove cookies and storage
16 | sessionStorage.auth_key = null
17 | axios.defaults.headers.common.Authorization = null
18 | // visit first page and type password
19 | cy.visit('/')
20 | cy.contains('Welcome Back!')
21 | cy.get('input').type('test')
22 | cy.get('button').click()
23 | cy.get('#live-page-button').click()
24 | cy.wait(50)
25 | })
26 |
27 | it('test live page', () => {
28 | // check page url
29 | cy.url().should('include', '/live/1')
30 |
31 | // we check routes component previously in backtest page
32 |
33 | // press live start button
34 | cy.get('[data-cy="live-start-button"]').click()
35 | cy.wait(50)
36 | cy.contains('Please wait')
37 | // press cancel button
38 | cy.get('[data-cy="live-cancel-button"]').click()
39 | cy.wait(50)
40 | // check we are in live page
41 | cy.get('[data-cy="live-page-content"]').should('include.text', 'Routes')
42 | cy.get('[data-cy="live-page-content"]').should('include.text', 'Options')
43 |
44 | // close notification
45 | cy.get('.notyf__dismiss-btn').click()
46 | // by mocking request app think we are login. for watching login button of live page
47 | // open navbar
48 | cy.get('[data-cy="nav-dropdown-menu-button"]').click()
49 | cy.wait(50)
50 | // press logout
51 | cy.get('[name=nav-logout-button]').click()
52 | cy.wait(50)
53 | cy.get('[data-cy="confirm-logout-button"]').click()
54 | cy.get('[data-cy="nav-dropdown-menu-button"]').click()
55 | cy.wait(50)
56 | // check logout not exist and watch login button on live page
57 | cy.get('[name=nav-logout-button]').should('not.exist')
58 | cy.get('[data-cy="live-action-button"]').should('have.text', ' Login to Jesse.Trade ')
59 |
60 | // close notifications
61 | cy.get('.notyf__dismiss-btn').click()
62 |
63 | // now check login button
64 | cy.get('[data-cy="live-login-button"]').click()
65 | cy.wait(50)
66 | // must visit login slideover
67 | cy.get('[data-cy="slideover-title"]').should('have.text', 'Login to your Jesse account')
68 | cy.get('[data-cy="login-cancel-button"]').click()
69 | cy.wait(50)
70 | cy.get('[data-cy="nav-dropdown-menu-button"]').click()
71 | cy.get('[name=nav-login-button]').should('not.exist')
72 | })
73 | })
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Need help? Check out the
8 | docs
9 |
10 | or search the help center:
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
30 |
{{ item.description }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
106 |
--------------------------------------------------------------------------------
/src/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Welcome Back!
13 |
14 |
15 |
16 |
Enter your password to continue:
17 |
18 |
19 |
20 |
42 |
43 |
44 |
45 |
46 |
104 |
--------------------------------------------------------------------------------
/src/components/MultipleValuesTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
11 | {{ item }}
12 |
13 |
14 |
15 |
16 |
17 |
19 |
24 |
25 |
28 |
31 |
33 |
34 |
35 |
36 |
39 |
42 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
52 | Empty List
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
89 |
--------------------------------------------------------------------------------
/src/components/Charts/EquityCurve.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
144 |
145 |
--------------------------------------------------------------------------------
/src/components/Tables/InfoTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 | Paper trade
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Started at
21 |
22 |
23 |
24 | a minute ago
25 |
26 |
27 |
28 |
29 |
30 |
31 | Current time
32 |
33 |
34 |
35 | 2021-06-02T16:14:26
36 |
37 |
38 |
39 |
40 |
41 |
42 | Errors/info
43 |
44 |
45 |
46 | 0/18
47 |
48 |
49 |
50 |
51 |
52 |
53 | Active orders
54 |
55 |
56 |
57 | 2
58 |
59 |
60 |
61 |
62 |
63 |
64 | Open positions
65 |
66 |
67 |
68 | 2
69 |
70 |
71 |
72 |
73 |
74 |
75 | Started/current balance
76 |
77 |
78 |
79 | 10000/9993.7
80 |
81 |
82 |
83 |
84 |
85 |
86 | Debug mode
87 |
88 |
89 |
90 | True
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
115 |
--------------------------------------------------------------------------------
/src/stores/candles.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import _ from 'lodash'
3 | import helpers from '@/helpers'
4 | import axios from '@/http'
5 | import notifier from '../notifier'
6 |
7 | let idCounter = 0
8 |
9 | function newTab () {
10 | return _.cloneDeep({
11 | id: ++idCounter,
12 | name: 'Tab 0',
13 | form: helpers.getDefaultFromLocalStorage('candlesForm', {
14 | start_date: '2021-01-01',
15 | exchange: '',
16 | symbol: '',
17 | }),
18 | results: {
19 | showResults: false,
20 | executing: false,
21 | progressbar: {
22 | current: 0,
23 | estimated_remaining_seconds: 0
24 | },
25 | metrics: [],
26 | infoLogs: '',
27 | exception: {
28 | error: '',
29 | traceback: ''
30 | },
31 | alert: {
32 | message: '',
33 | type: ''
34 | }
35 | }
36 | })
37 | }
38 |
39 | export const useCandlesStore = defineStore({
40 | id: 'candles',
41 | state: () => ({
42 | tabs: {
43 | 1: newTab()
44 | }
45 | }),
46 | actions: {
47 | addTab () {
48 | const tab = newTab()
49 | this.tabs[tab.id] = tab
50 | return this.$router.push(`/candles/${tab.id}`)
51 | },
52 | startInNewTab (id) {
53 | const tab = newTab()
54 | tab.form = _.cloneDeep(this.tabs[id].form)
55 | this.tabs[tab.id] = tab
56 | this.start(tab.id)
57 | },
58 | start (id) {
59 | this.tabs[id].results.progressbar.current = 0
60 | this.tabs[id].results.executing = true
61 | this.tabs[id].results.infoLogs = ''
62 | this.tabs[id].results.exception.traceback = ''
63 | this.tabs[id].results.exception.error = ''
64 | this.tabs[id].results.alert.message = ''
65 |
66 | axios.post('/import-candles', {
67 | id,
68 | exchange: this.tabs[id].form.exchange,
69 | symbol: this.tabs[id].form.symbol,
70 | start_date: this.tabs[id].form.start_date,
71 | }).catch(error => {
72 | notifier.error(`[${error.response.status}]: ${error.response.statusText}`)
73 | this.tabs[id].results.executing = false
74 | })
75 | },
76 | cancel (id) {
77 | if (this.tabs[id].results.exception.error) {
78 | this.tabs[id].results.executing = false
79 | return
80 | }
81 |
82 | axios.delete('/import-candles', {
83 | headers: {},
84 | data: {
85 | id
86 | }
87 | }).then(() => {
88 | // if was in test cancel execution directly
89 | if (window.Cypress) {
90 | this.tabs[id].results.executing = false
91 | }
92 | }).catch(error => notifier.error(`[${error.response.status}]: ${error.response.statusText}`))
93 | },
94 |
95 | progressbarEvent (id, data) {
96 | this.tabs[id].results.progressbar = data
97 |
98 | if (this.tabs[id].results.progressbar.current < 100 && this.tabs[id].results.executing === false) {
99 | this.tabs[id].results.executing = true
100 | }
101 | },
102 | alertEvent (id, data) {
103 | this.tabs[id].results.alert = data
104 |
105 | // session is finished:
106 | this.tabs[id].results.progressbar.current = 100
107 | this.tabs[id].results.executing = false
108 | this.tabs[id].results.exception.traceback = ''
109 | this.tabs[id].results.exception.error = ''
110 | },
111 | infoLogEvent (id, data) {
112 | this.tabs[id].results.infoLogs += `[${helpers.timestampToTime(
113 | data.timestamp
114 | )}] ${data.message}\n`
115 | },
116 | exceptionEvent (id, data) {
117 | this.tabs[id].results.exception.error = data.error
118 | this.tabs[id].results.exception.traceback = data.traceback
119 | },
120 | terminationEvent (id) {
121 | if (this.tabs[id].results.executing) {
122 | this.tabs[id].results.executing = false
123 | notifier.success('Session terminated successfully')
124 | }
125 | },
126 | }
127 | })
128 |
--------------------------------------------------------------------------------
/src/components/UpdateBanner.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 | {{ show.message }}
9 |
10 |
11 | Update Guide →
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
110 |
--------------------------------------------------------------------------------
/src/plugins/socketActions.js:
--------------------------------------------------------------------------------
1 | import { useBacktestStore } from '@/stores/backtest'
2 | import { useCandlesStore } from '@/stores/candles'
3 | import { useLiveStore } from '@/stores/live'
4 | import { useOptimizationStore } from '@/stores/optimization'
5 |
6 | export default function () {
7 | const backtest = useBacktestStore()
8 | const candles = useCandlesStore()
9 | const live = useLiveStore()
10 | const optimize = useOptimizationStore()
11 |
12 | // Prepare needed actions for each socket event
13 | return new Map([
14 | // backtest
15 | ['backtest.candles_info', [
16 | backtest.candlesInfoEvent
17 | ]],
18 | ['backtest.routes_info', [
19 | backtest.routesInfoEvent
20 | ]],
21 | ['backtest.progressbar', [
22 | backtest.progressbarEvent
23 | ]],
24 | ['backtest.metrics', [
25 | backtest.metricsEvent
26 | ]],
27 | ['backtest.hyperparameters', [
28 | backtest.hyperparametersEvent
29 | ]],
30 | ['backtest.info_log', [
31 | backtest.infoLogEvent
32 | ]],
33 | ['backtest.equity_curve', [
34 | backtest.equityCurveEvent
35 | ]],
36 | ['backtest.exception', [
37 | backtest.exceptionEvent
38 | ]],
39 | ['backtest.general_info', [
40 | backtest.generalInfoEvent
41 | ]],
42 | ['backtest.termination', [
43 | backtest.terminationEvent
44 | ]],
45 | ['backtest.alert', [
46 | backtest.alertEvent
47 | ]],
48 |
49 | // candles
50 | ['candles.progressbar', [
51 | candles.progressbarEvent
52 | ]],
53 | ['candles.alert', [
54 | candles.alertEvent
55 | ]],
56 | ['candles.exception', [
57 | candles.exceptionEvent
58 | ]],
59 | ['candles.termination', [
60 | candles.terminationEvent
61 | ]],
62 |
63 | // live and paper
64 | ['papertrade.progressbar', [
65 | live.progressbarEvent
66 | ]],
67 | ['livetrade.progressbar', [
68 | live.progressbarEvent
69 | ]],
70 | ['papertrade.positions', [
71 | live.positionsEvent
72 | ]],
73 | ['livetrade.positions', [
74 | live.positionsEvent
75 | ]],
76 | ['papertrade.orders', [
77 | live.ordersEvent
78 | ]],
79 | ['livetrade.orders', [
80 | live.ordersEvent
81 | ]],
82 | ['papertrade.general_info', [
83 | live.generalInfoEvent
84 | ]],
85 | ['livetrade.general_info', [
86 | live.generalInfoEvent
87 | ]],
88 | ['papertrade.watch_list', [
89 | live.watchlistEvent
90 | ]],
91 | ['livetrade.watch_list', [
92 | live.watchlistEvent
93 | ]],
94 | ['papertrade.current_candles', [
95 | live.currentCandlesEvent
96 | ]],
97 | ['livetrade.current_candles', [
98 | live.currentCandlesEvent
99 | ]],
100 | ['papertrade.info_log', [
101 | live.infoLogEvent
102 | ]],
103 | ['livetrade.info_log', [
104 | live.infoLogEvent
105 | ]],
106 | ['papertrade.error_log', [
107 | live.errorLogEvent
108 | ]],
109 | ['livetrade.error_log', [
110 | live.errorLogEvent
111 | ]],
112 | ['papertrade.error_log', [
113 | live.errorLogEvent
114 | ]],
115 | ['livetrade.error_log', [
116 | live.errorLogEvent
117 | ]],
118 | ['papertrade.exception', [
119 | live.exceptionEvent
120 | ]],
121 | ['livetrade.exception', [
122 | live.exceptionEvent
123 | ]],
124 | ['papertrade.unexpectedTermination', [
125 | live.unexpectedTerminationEvent
126 | ]],
127 | ['livetrade.unexpectedTermination', [
128 | live.unexpectedTerminationEvent
129 | ]],
130 | ['papertrade.termination', [
131 | live.terminationEvent
132 | ]],
133 | ['livetrade.termination', [
134 | live.terminationEvent
135 | ]],
136 |
137 | ['optimize.progressbar', [
138 | optimize.progressbarEvent
139 | ]],
140 | ['optimize.general_info', [
141 | optimize.generalInfoEvent
142 | ]],
143 | ['optimize.metrics', [
144 | optimize.metricsEvent
145 | ]],
146 | ['optimize.exception', [
147 | optimize.exceptionEvent
148 | ]],
149 | ['optimize.termination', [
150 | optimize.terminationEvent
151 | ]],
152 | ['optimize.alert', [
153 | optimize.alertEvent
154 | ]],
155 | ['optimize.best_candidates', [
156 | optimize.bestCandidatesEvent
157 | ]],
158 | ])
159 | }
160 |
161 |
162 |
--------------------------------------------------------------------------------
/src/components/Tabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
18 |
19 | {{ tab.results.executing ? '' : `Tab ${index + 1}` }} {{ tab.results.executing ? `${tab.form.symbol ?tab.form.symbol : tab.form.routes[0].symbol}`: `` }} {{ tab.results.executing && !tab.results.showResults ? ' | ' + tab.results.progressbar.current + '%' : '' }} {{ tab.results.showResults ? ' - Results' : '' }}
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
41 |
42 |
43 |
44 |
45 |
46 |
99 |
--------------------------------------------------------------------------------
/src/views/HelpSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
26 |
30 |
31 |
35 |
36 | Searching ...
37 |
38 |
39 |
40 |
41 |
45 |
48 |
49 |
56 |
62 |
63 |
64 | Something went wrong.
65 |
66 |
67 |
68 |
69 |
73 |
85 |
86 |
87 |
91 |
92 |
93 |
94 |
98 |
101 |
110 |
116 | No items were found for the entered phrase.
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
171 |
--------------------------------------------------------------------------------
/src/components/Modals/ConfirmModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{ title }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{ description }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | {{ type === 'danger' ? 'Cancel' : 'Close' }}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
132 |
--------------------------------------------------------------------------------
/src/stores/main.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import axios from '@/http'
3 | import _ from 'lodash'
4 | import notifier from '../notifier'
5 |
6 | export const useMainStore = defineStore({
7 | id: 'main',
8 | state: () => ({
9 | baseURL: '',
10 | isConnected: false,
11 | isInitiated: false,
12 | isAuthenticated: false,
13 | hasLivePluginInstalled: false,
14 | systemInfo: {},
15 | updateInfo: {},
16 | planInfo: {},
17 | theme: localStorage.theme,
18 | modals: {
19 | settings: false,
20 | exceptionReport: false,
21 | feedback: false,
22 | makeStrategy: false,
23 | about: false,
24 | },
25 | settings: {
26 | backtest: {
27 | logging: {
28 | order_submission: true,
29 | order_cancellation: true,
30 | order_execution: true,
31 | position_opened: true,
32 | position_increased: true,
33 | position_reduced: true,
34 | position_closed: true,
35 | shorter_period_candles: false,
36 | trading_candles: true,
37 | balance_update: true,
38 | },
39 | warm_up_candles: 210,
40 | exchanges: {},
41 | },
42 | live: {
43 | persistency: true,
44 | generate_candles_from_1m: false,
45 | logging: {
46 | order_submission: true,
47 | order_cancellation: true,
48 | order_execution: true,
49 | position_opened: true,
50 | position_increased: true,
51 | position_reduced: true,
52 | position_closed: true,
53 | shorter_period_candles: false,
54 | trading_candles: true,
55 | balance_update: true,
56 | },
57 | warm_up_candles: 210,
58 | exchanges: {},
59 | notifications: {
60 | enabled: true,
61 | position_report_timeframe: '1h',
62 | events: {
63 | errors: true,
64 | started_session: true,
65 | terminated_session: true,
66 | submitted_orders: true,
67 | cancelled_orders: true,
68 | executed_orders: true,
69 | opened_position: true,
70 | updated_position: true,
71 | },
72 | },
73 | },
74 | optimization: {
75 | cpu_cores: 2,
76 | // sharpe, calmar, sortino, omega
77 | ratio: 'sharpe',
78 | warm_up_candles: 210,
79 | exchange: {
80 | balance: 10_000,
81 | fee: 0.001,
82 | type: 'futures',
83 | futures_leverage: 3,
84 | futures_leverage_mode: 'cross',
85 | },
86 | }
87 | },
88 | strategies: [],
89 | exchangeInfo: {},
90 | jesse_supported_timeframes: [],
91 | }),
92 | getters: {
93 | backtestingExchangeNames () {
94 | const arr = []
95 | for (const key in this.exchangeInfo) {
96 | if (this.exchangeInfo[key].modes.backtesting) {
97 | arr.push(key)
98 | }
99 | }
100 | // sort arr's items by name alphabetically
101 | return arr.sort()
102 | }
103 | },
104 | actions: {
105 | initiate () {
106 | axios.post('/general-info').then(res => {
107 | const data = res.data
108 | this.systemInfo = data.system_info
109 | this.updateInfo = data.update_info
110 | this.strategies = data.strategies
111 | this.exchangeInfo = data.exchanges
112 | this.jesseSupportedTimeframes = data.jesse_supported_timeframes
113 | this.hasLivePluginInstalled = data.has_live_plugin_installed
114 | this.planInfo = data.plan_info
115 |
116 | // create the list of exchanges by setting the default values (further down we
117 | // will override the default values with the user's settings fetched from the database)
118 | // loop through the this.exchangeInfo object
119 | for (const key in this.exchangeInfo) {
120 | const value = this.exchangeInfo[key]
121 |
122 | // for backtests
123 | if (value.modes.backtesting) {
124 | this.settings.backtest.exchanges[key] = {
125 | name: key,
126 | fee: value.fee,
127 | balance: 10000,
128 | type: value.type,
129 | }
130 | if (value.type === 'futures') {
131 | this.settings.backtest.exchanges[key].futures_leverage_mode = 'cross'
132 | this.settings.backtest.exchanges[key].futures_leverage = 2
133 | }
134 | }
135 |
136 | // for live trading
137 | if (value.modes.live_trading) {
138 | this.settings.live.exchanges[value.name] = {
139 | name: key,
140 | fee: value.fee,
141 | futures_leverage_mode: 'cross',
142 | futures_leverage: 2,
143 | balance: 10_000,
144 | }
145 | }
146 | }
147 |
148 | // fetch and merge the user's settings from the database
149 | axios.post('/get-config', {
150 | current_config: this.settings
151 | }).then(res => {
152 | this.settings = res.data.data.data
153 | this.isInitiated = true
154 | }).catch(error => {
155 | notifier.error(`[${error.response.status}]: ${error.response.statusText}`)
156 | })
157 | }).catch(error => {
158 | const msg = `${error.response.data.error}`
159 | console.error(msg)
160 | notifier.error(msg)
161 | })
162 |
163 | // set baseUrl to axios.defaults.baseURL
164 | this.baseURL = axios.defaults.baseURL
165 | },
166 |
167 | updateConfig: _.throttle(
168 | function () {
169 | if (!this.isInitiated) return
170 |
171 | axios.post('/update-config', {
172 | current_config: this.settings
173 | }).catch(error => {
174 | notifier.error(`[${error.response.status}]: ${error.response.statusText}`)
175 | })
176 | },
177 | 1000,
178 | { leading: true, trailing: true }
179 | )
180 | }
181 | })
182 |
--------------------------------------------------------------------------------
/cypress/fixtures/getConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "data": {
4 | "optimization": {
5 | "ratio": "sharpe",
6 | "cpu_cores": 2,
7 | "exchange": {
8 | "fee": 0.001,
9 | "balance": 10000,
10 | "futures_leverage": 3,
11 | "futures_leverage_mode": "cross"
12 | },
13 | "warm_up_candles": 210
14 | },
15 | "live": {
16 | "logging": {
17 | "position_closed": true,
18 | "order_submission": true,
19 | "trading_candles": true,
20 | "shorter_period_candles": false,
21 | "position_reduced": true,
22 | "position_opened": true,
23 | "order_cancellation": true,
24 | "order_execution": true,
25 | "position_increased": true,
26 | "balance_update": true
27 | },
28 | "notifications": {
29 | "events": {
30 | "opened_position": true,
31 | "errors": true,
32 | "updated_position": true,
33 | "submitted_orders": true,
34 | "terminated_session": true,
35 | "executed_orders": true,
36 | "started_session": true,
37 | "cancelled_orders": true
38 | },
39 | "enabled": true,
40 | "position_report_timeframe": "1h"
41 | },
42 | "exchanges": {
43 | "FTX Futures": {
44 | "futures_leverage": 2,
45 | "name": "FTX Futures",
46 | "futures_leverage_mode": "cross",
47 | "settlement_currency": "USD",
48 | "fee": 0.001,
49 | "balance": 10000
50 | },
51 | "Testnet Binance Futures": {
52 | "futures_leverage": 2,
53 | "name": "Testnet Binance Futures",
54 | "futures_leverage_mode": "cross",
55 | "settlement_currency": "USDT",
56 | "fee": 0.001,
57 | "balance": 10000
58 | },
59 | "Binance Futures": {
60 | "futures_leverage": 2,
61 | "name": "Binance Futures",
62 | "futures_leverage_mode": "cross",
63 | "settlement_currency": "USDT",
64 | "fee": 0.001,
65 | "balance": 10000
66 | }
67 | },
68 | "warm_up_candles": 210
69 | },
70 | "backtest": {
71 | "logging": {
72 | "position_closed": true,
73 | "order_submission": true,
74 | "trading_candles": true,
75 | "shorter_period_candles": false,
76 | "position_reduced": true,
77 | "position_opened": true,
78 | "order_cancellation": true,
79 | "order_execution": true,
80 | "position_increased": true,
81 | "balance_update": true
82 | },
83 | "exchanges": {
84 | "Testnet Binance Futures": {
85 | "futures_leverage": 2,
86 | "name": "Testnet Binance Futures",
87 | "futures_leverage_mode": "isolated",
88 | "settlement_currency": "USDT",
89 | "fee": 0.001,
90 | "balance": 10000
91 | },
92 | "OKEX": {
93 | "futures_leverage": 2,
94 | "name": "OKEX",
95 | "futures_leverage_mode": "isolated",
96 | "settlement_currency": "USDT",
97 | "fee": 0.001,
98 | "balance": 10000
99 | },
100 | "Binance Inverse Futures": {
101 | "futures_leverage": 2,
102 | "name": "Binance Inverse Futures",
103 | "futures_leverage_mode": "isolated",
104 | "settlement_currency": "USDT",
105 | "fee": 0.001,
106 | "balance": 10000
107 | },
108 | "Binance": {
109 | "futures_leverage": 2,
110 | "name": "Binance",
111 | "futures_leverage_mode": "isolated",
112 | "settlement_currency": "USDT",
113 | "fee": 0.001,
114 | "balance": 10000
115 | },
116 | "Bitfinex": {
117 | "futures_leverage": 2,
118 | "name": "Bitfinex",
119 | "futures_leverage_mode": "isolated",
120 | "settlement_currency": "USD",
121 | "fee": 0.001,
122 | "balance": 10000
123 | },
124 | "Coinbase": {
125 | "futures_leverage": 2,
126 | "name": "Coinbase",
127 | "futures_leverage_mode": "isolated",
128 | "settlement_currency": "USD",
129 | "fee": 0.001,
130 | "balance": 10000
131 | },
132 | "Binance Futures": {
133 | "futures_leverage": 2,
134 | "name": "Binance Futures",
135 | "futures_leverage_mode": "isolated",
136 | "settlement_currency": "USDT",
137 | "fee": 0.001,
138 | "balance": 10000
139 | }
140 | },
141 | "warm_up_candles": 210
142 | }
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/cypress/integration/backtestPage.spec.js:
--------------------------------------------------------------------------------
1 | const { default: axios } = require('axios')
2 |
3 | describe('test home page', () => {
4 | beforeEach(() => {
5 | // mock important requests
6 | cy.intercept('post', '/auth', { fixture: 'login.json' }).as('login')
7 | cy.intercept('post', '/general-info', { fixture: 'generalInfo.json' }).as('generalInfo')
8 | cy.intercept('post', '/get-config', { fixture: 'getConfig.json' }).as('getConfig')
9 | cy.intercept('post', '/update-config', { fixture: 'updateConfig.json' }).as('updateConfig')
10 | cy.intercept('post', '/backtest', { fixture: 'backtest.json' }).as('backtest')
11 | cy.intercept('delete', '/backtest', { fixture: 'deleteBacktest.json' }).as('backtest')
12 |
13 | // remove cookies and storage
14 | sessionStorage.auth_key = null
15 | axios.defaults.headers.common.Authorization = null
16 | // visit first page and type password
17 | cy.visit('/')
18 | cy.contains('Welcome Back!')
19 | cy.get('input').type('test')
20 | cy.get('button').click()
21 | cy.visit('/#/backtest')
22 | cy.wait(50)
23 | })
24 | it('test routes section', () => {
25 | // close notification
26 | cy.get('.notyf__dismiss-btn').click()
27 |
28 | // check main title of page
29 | cy.get('[data-cy="backtest-content-section"]').should('include.text', 'Routes')
30 | cy.get('[data-cy="backtest-content-section"]').should('include.text', 'Options')
31 |
32 | // check add trading route
33 | cy.get('[data-cy="add-route"]').click()
34 | cy.wait(50)
35 | cy.get('[data-cy="trading-route-exchange1"]').should('have.value', 'Binance')
36 | cy.get('[data-cy="trading-route-symbol1"]').should('have.value', '')
37 | cy.get('[data-cy="trading-route-exchange1"]').should('include.text', 'Bitfinex')
38 |
39 | // check trading route delete button
40 | cy.get('[data-cy="trading-route-menu-button1"]').click()
41 | cy.wait(50)
42 | cy.get('[name=trading-delete-menu1]').click()
43 | cy.get('[data-cy="trading-route-exchange1"]').should('not.exist');
44 |
45 | // check trading route duplicate
46 | cy.get('[data-cy="trading-route-exchange0"]').select('Coinbase')
47 | cy.wait(100)
48 | cy.get('[data-cy="trading-route-menu-button0"]').click()
49 | cy.wait(50)
50 | cy.get('[name=trading-duplicate-menu0]').click()
51 | cy.get('[data-cy="trading-route-exchange1"]').should('have.value', 'Coinbase');
52 |
53 | // check trading route move up
54 | cy.get('[data-cy="trading-route-exchange1"]').select('Binance')
55 | cy.wait(50)
56 | cy.get('[data-cy="trading-route-menu-button1"]').click()
57 | cy.wait(50)
58 | cy.get('[name=trading-moveup-menu1]').click()
59 | cy.get('[data-cy="trading-route-exchange0"]').should('have.value', 'Binance');
60 |
61 | // check trading route move down
62 | cy.wait(50)
63 | cy.get('[data-cy="trading-route-menu-button0"]').click()
64 | cy.wait(50)
65 | cy.get('[name=trading-movedown-menu0]').click()
66 | cy.get('[data-cy="trading-route-exchange1"]').should('have.value', 'Binance');
67 |
68 | // check add extra route button
69 | cy.get('[data-cy="add-extra-route"]').click()
70 | cy.get('[data-cy="extra-route-exchange0"]').should('have.value', 'Binance')
71 |
72 | // check extra route duplicate
73 | cy.get('[data-cy="extra-route-exchange0"]').select('Coinbase')
74 | cy.wait(50)
75 | cy.get('[data-cy="extra-route-menu-button0"]').click()
76 | cy.wait(50)
77 | cy.get('[name=extra-duplicate-menu0]').click()
78 | cy.get('[data-cy="extra-route-exchange1"]').should('have.value', 'Coinbase');
79 |
80 | // check delete button of extra route
81 | cy.get('[data-cy="extra-route-menu-button1"]').click()
82 | cy.wait(50)
83 | cy.get('[name=extra-delete-menu1]').click()
84 | cy.get('[data-cy="extra-route-exchange1"]').should('not.exist');
85 |
86 | // check errors of routes
87 | cy.get('[data-cy="trading-route-symbol1"]').type('btcsdescdscds')
88 | cy.wait(50)
89 | cy.get('[data-cy="error0"]').should('have.text', 'Maximum symbol length is exceeded!')
90 | cy.get('[data-cy="error1"]').should('have.text', 'Symbol parameter must contain "-" character!')
91 |
92 | cy.get('[data-cy="extra-route-symbol0"]').type('BTC-USDT')
93 | cy.get('[data-cy="error2"]').should('have.text', 'Extra routes timeframe and routes timeframe must be different')
94 |
95 | cy.get('[data-cy="trading-route-exchange1"]').select('Coinbase')
96 | cy.wait(50)
97 | cy.get('[data-cy="trading-route-symbol1"]').clear()
98 | cy.get('[data-cy="trading-route-symbol1"]').type('BTC-USDT')
99 | cy.get('[data-cy="error0"]').should("include.text", "each exchange-symbol pair can be traded only once!")
100 | })
101 |
102 | it('test options, duration and tabs', () => {
103 | // options fields
104 | cy.get('[data-cy="backtest-option-section"]').should("include.text", 'Debug Mode')
105 | cy.get('[data-cy="backtest-option-section"]').should("include.text", 'Export JSON')
106 |
107 | // duration field text
108 | cy.get('[data-cy="backtest-start-date"]').should('have.value', '2021-01-01')
109 |
110 | // press start button
111 | cy.get('[data-cy="start-button"]').click()
112 | cy.wait(50)
113 | cy.contains('Tab 1 - 0%')
114 | cy.wait(50)
115 | cy.contains('Please wait')
116 | // press cancel button
117 | cy.get('[data-cy="backtest-cancel-button"]').click()
118 | cy.wait(50)
119 | cy.contains('Routes')
120 |
121 | // press new tab button
122 | cy.get('[data-cy="start-new-tab-button"]').click()
123 | cy.wait(50)
124 | cy.get('[data-cy="tab1"]').click()
125 | cy.wait(50)
126 | cy.contains('Please wait')
127 | // press cancel
128 | cy.get('[data-cy="backtest-cancel-button"]').click()
129 | cy.wait(50)
130 | cy.contains('Routes')
131 |
132 | // check remove button
133 | cy.get('[data-cy="tab-close-button1"]').click()
134 | cy.wait(50)
135 | cy.get('[data-cy="tab-close-button1"]').should('not.exist')
136 | })
137 | })
--------------------------------------------------------------------------------
/src/assets/imgs/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/imgs/search-by-algolia-light-background.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Charts/Candles/CandlesChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
249 |
--------------------------------------------------------------------------------
/src/components/Exception.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
10 | If the exception you're seeing is not clear and you think it might be a bug, please send us a report to help
11 | us debugging and fixing it in a future release.
12 |
13 |
14 |
15 |
16 | Exception:
17 |
20 |
21 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
43 |
44 |
49 |
50 |
51 |
52 |
53 | Cancel
54 |
57 | Submit
58 |
59 |
60 |
61 |
62 |
63 |
66 |
67 | Report
68 |
70 |
71 |
72 | {{ copied ? 'Copied' : 'Copy' }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
89 |
90 |
91 |
92 |
93 |
94 |
210 |
--------------------------------------------------------------------------------
/cypress/integration/navbar.spec.js:
--------------------------------------------------------------------------------
1 | const { default: axios } = require('axios')
2 |
3 | describe('test home page', () => {
4 | beforeEach(() => {
5 | // mock important requests
6 | cy.intercept('post', '/auth', { fixture: 'login.json' }).as('login')
7 | cy.intercept('post', '/general-info', { fixture: 'generalInfo.json' }).as('generalInfo')
8 | cy.intercept('post', '/get-config', { fixture: 'getConfig.json' }).as('getConfig')
9 | cy.intercept('post', '/update-config', { fixture: 'updateConfig.json' }).as('updateConfig')
10 | cy.intercept('post', 'feedback', { fixture: 'feedback.json' }).as('feedback')
11 | cy.intercept('post', 'make-strategy', { fixture: 'makeStrategy.json' }).as('makeStrategy')
12 |
13 | // remove cookies and storage
14 | sessionStorage.auth_key = null
15 | axios.defaults.headers.common.Authorization = null
16 | // visit first page and type password
17 | cy.visit('/')
18 | cy.contains('Welcome Back!')
19 | cy.get('input').type('test')
20 | cy.get('button').click()
21 | })
22 |
23 | it('test nav bar elements', () => {
24 | // close notification login notification
25 | cy.get('.notyf__dismiss-btn').click()
26 |
27 | // check left navbar links
28 | // click on each link and check page url
29 | cy.get('#import-candles-page-button').click()
30 | cy.url().should('include', '/candles/1')
31 |
32 | cy.get('#backtest-page-button').click()
33 | cy.url().should('include', '/backtest/1')
34 |
35 | cy.get('#live-page-button').click()
36 | cy.url().should('include', '/live/1')
37 | cy.wait(500)
38 |
39 | // check feedback
40 | cy.get('#open-feedback-button').click()
41 | cy.wait(50)
42 | // must display feedback modal
43 | cy.get('#feedback-description').should('include.text', "I would love to hear your feedback whether it's about a bug, suggestion, something you like, or something you hate about Jesse")
44 | // submit button is disabled when description is empty
45 | cy.get('#feedback-submit-button').should('be.disabled')
46 | // type some description
47 | cy.get('#description').type('some test description')
48 | cy.wait(50)
49 | cy.get('#description').should('have.value', 'some test description')
50 | // by typing description submit button is activated
51 | cy.get('#feedback-submit-button').should('not.disabled')
52 | cy.get('#feedback-submit-button').click()
53 | // check feedback notification
54 | cy.get('.notyf__message').should('include.text', 'Feedback submitted successfully')
55 | // after feedback description input will be reset
56 | cy.get('#feedback-description').should('have.value', '')
57 | cy.get('#feedback-description').should('not.exist')
58 |
59 | // close notification
60 | cy.get('.notyf__dismiss-btn').click()
61 |
62 | // check theme Switch. by clicking theme switch icon, icon will be change
63 | cy.get('#nav-sun-icon').should('not.exist')
64 | cy.get('#theme-switch-button').click().should(() => {
65 | expect(localStorage.getItem('theme')).to.eq('dark')
66 | })
67 | cy.get('#theme-switch-button').click().should(() => {
68 | expect(localStorage.getItem('theme')).to.eq('light')
69 | })
70 |
71 | // check settings
72 | cy.get('[data-cy=settings-icon]').click()
73 | cy.wait(50)
74 | // check backtests tab
75 | cy.get('[data-cy=Backtest-setting]').click()
76 | // check another tab not opened
77 | cy.get('[data-cy="optimization-setting-tab"]').should('not.exist')
78 | cy.get('[data-cy="live-setting-tab"]').should('not.exist')
79 | cy.get('[data-cy=backtest-setting-tab]').should('exist')
80 | // check backtest tab data
81 | cy.get('[data-cy=backtest-setting-logs-checkboxes]').should('include.text', 'Trading Candles')
82 | cy.get('[data-cy=backtest-setting-data-input]').should('exist')
83 | cy.get('[data-cy=backtest-setting-exchange-binance').should('exist')
84 | cy.get('[data-cy=backtest-setting-exchange-binance-futures').should('exist')
85 | cy.get('[data-cy=backtest-setting-exchange-bitfinex').should('exist')
86 |
87 | // check Optimization
88 | cy.get('[data-cy=Optimization-setting]').click()
89 | // check another tab not exist
90 | cy.get('[data-cy="backtest-setting-tab"]').should('not.exist')
91 | cy.get('[data-cy="live-setting-tab"]').should('not.exist')
92 | // check optimization tab data
93 | cy.contains('Fitness Function')
94 | cy.get('[data-cy=optimization-setting-tab').should('exist')
95 | cy.get('[data-cy=optimization-warmup-candles-input]').should('exist')
96 | cy.get('[data-cy=ratio-radio-groups]').should('exist')
97 |
98 | // check live tab
99 | cy.get('[data-cy=Live-setting]').click()
100 | // check another tab not exist
101 | cy.get('[data-cy="backtest-setting-tab"]').should('not.exist')
102 | cy.get('[data-cy="backtest-optimization-tab"]').should('not.exist')
103 | // check live tab data
104 | cy.get('[data-cy="setting-live-tab"]').should('include.text', "You can filter the types of events that you want to be logged. Logging is often useful for debugging and recommended. Hence, it doesn't hurt to enable them all: ")
105 | cy.get('[data-cy="setting-live-tab"]').should('include.text', "Order Submission")
106 | cy.get('[data-cy="setting-live-tab"]').should('include.text', "1m candles")
107 | cy.get('[data-cy="live-setting-warmup-candles-input"]').should('exist')
108 | cy.get('[data-cy="setting-live-tab"]').should('include.text', "Jesse can notify every time something interesting happens so you don't have to monitor your bots 24/7. Currently, Telegram and Discord drivers are supported")
109 | cy.get('[data-cy="live-setting-enabled-notification"]').should('exist')
110 | cy.get('[data-cy="setting-live-tab"]').should('include.text', "Errors")
111 | cy.get('[data-cy="setting-live-tab"]').should('include.text', "Opened Positions")
112 | cy.get('[data-cy="live-setting-report-notification-timeframe"]').should('have.value', "1h")
113 | // close setting slide over
114 | cy.get('#slideover-close-button').click()
115 | cy.wait(50)
116 |
117 | // test dropdown menu
118 | cy.get('[data-cy="nav-dropdown-menu-button"]').click()
119 | cy.get('[data-cy="nav-dropdown-menu-items"]').should('exist')
120 | // open make strategy slide over
121 | cy.get('[data-cy="nav-create-strategy"]').click()
122 | cy.wait(50)
123 | // check make strategy modal
124 | cy.get("#make-strategy-modal").should('exist')
125 | cy.get("#make-strategy-modal").should('include.text', 'Filling this form will create a new strategy class with all the starting methods in it')
126 | cy.get('#strategy').type('test-strategy')
127 | cy.get('[data-cy="make-strategy-button"]').click()
128 | // make strategy message
129 | cy.get('.notyf__message').should('include.text', 'Success')
130 | cy.get('.notyf__dismiss-btn').click()
131 | cy.get('[data-cy="nav-dropdown-menu-button"]').click()
132 | // check nav menu links url
133 | cy.get('[data-cy="nav-documentation-link"]').should('have.attr', 'href', 'https://docs.jesse.trade/')
134 | cy.get('[data-cy="nav-strategies-link"]').should('have.attr', 'href', 'https://jesse.trade/strategies')
135 | cy.get('[data-cy="nav-help-center-link"]').should('have.attr', 'href', 'https://jesse.trade/help')
136 | })
137 | })
--------------------------------------------------------------------------------
/src/views/tabs/CandlesTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Cancel
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
38 |
39 |
40 |
44 | {{ item }}
45 |
46 |
47 |
48 |
49 |
54 |
55 |
56 |
63 |
64 |
65 |
66 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | Rerun
82 |
83 |
84 |
85 | New backtest
86 |
87 |
88 |
89 |
90 |
91 |
92 | Start
93 |
94 |
95 |
96 |
97 | Start in a new tab
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
208 |
209 |
--------------------------------------------------------------------------------
/src/stores/backtest.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import _ from 'lodash'
3 | import helpers from '@/helpers'
4 | import axios from '@/http'
5 | import { useMainStore } from '@/stores/main'
6 | import notifier from '../notifier'
7 |
8 | let idCounter = 0
9 |
10 | /**
11 | * A function that returns required data for a new tab
12 | */
13 | function newTab () {
14 | return _.cloneDeep({
15 | id: ++idCounter,
16 | name: 'Tab 0',
17 | form: helpers.getDefaultFromLocalStorage('backtestForm', {
18 | start_date: '2021-01-01',
19 | finish_date: '2021-06-01',
20 | debug_mode: false,
21 | export_chart: false,
22 | export_tradingview: false,
23 | export_full_reports: false,
24 | export_csv: false,
25 | export_json: false,
26 | routes: [],
27 | extra_routes: []
28 | }),
29 | results: {
30 | showResults: false,
31 | executing: false,
32 | logsModal: false,
33 | progressbar: {
34 | current: 0,
35 | estimated_remaining_seconds: 0
36 | },
37 | routes_info: [],
38 | metrics: [],
39 | hyperparameters: [],
40 | generalInfo: {},
41 | infoLogs: '',
42 | exception: {
43 | error: '',
44 | traceback: ''
45 | },
46 | charts: {
47 | equity_curve: []
48 | },
49 | alert: {
50 | message: '',
51 | type: ''
52 | }
53 | }
54 | })
55 | }
56 |
57 | export const useBacktestStore = defineStore({
58 | id: 'backtest',
59 | state: () => ({
60 | tabs: {
61 | 1: newTab()
62 | }
63 | }),
64 | actions: {
65 | addTab () {
66 | const tab = newTab()
67 | this.tabs[tab.id] = tab
68 | return this.$router.push(`/backtest/${tab.id}`)
69 | },
70 | startInNewTab (id) {
71 | const tab = newTab()
72 | tab.form = _.cloneDeep(this.tabs[id].form)
73 | this.tabs[tab.id] = tab
74 | this.start(tab.id)
75 | },
76 | start (id) {
77 | this.tabs[id].results.progressbar.current = 0
78 | this.tabs[id].results.executing = true
79 | this.tabs[id].results.infoLogs = ''
80 | this.tabs[id].results.exception.traceback = ''
81 | this.tabs[id].results.exception.error = ''
82 | this.tabs[id].results.alert.message = ''
83 |
84 | const mainStore = useMainStore()
85 |
86 | // make sure symbols are uppercase
87 | this.tabs[id].form.routes = this.tabs[id].form.routes.map(route => {
88 | route.symbol = route.symbol.toUpperCase()
89 | return route
90 | })
91 | // also for extra_routes
92 | this.tabs[id].form.extra_routes = this.tabs[id].form.extra_routes.map(route => {
93 | route.symbol = route.symbol.toUpperCase()
94 | return route
95 | })
96 |
97 | axios.post('/backtest', {
98 | id,
99 | routes: this.tabs[id].form.routes,
100 | extra_routes: this.tabs[id].form.extra_routes,
101 | config: mainStore.settings.backtest,
102 | start_date: this.tabs[id].form.start_date,
103 | finish_date: this.tabs[id].form.finish_date,
104 | debug_mode: this.tabs[id].form.debug_mode,
105 | export_csv: this.tabs[id].form.export_csv,
106 | export_chart: this.tabs[id].form.export_chart,
107 | export_tradingview: this.tabs[id].form.export_tradingview,
108 | export_full_reports: this.tabs[id].form.export_full_reports,
109 | export_json: this.tabs[id].form.export_json,
110 | }).catch(error => {
111 | notifier.error(`[${error.response.status}]: ${error.response.statusText}`)
112 | this.tabs[id].results.executing = false
113 | })
114 | },
115 | cancel (id) {
116 | if (this.tabs[id].results.exception.error) {
117 | this.tabs[id].results.executing = false
118 | return
119 | }
120 |
121 | axios.delete('/backtest', {
122 | headers: {},
123 | data: {
124 | id
125 | }
126 | }).then(() => {
127 | // this is for passing cypress tests
128 | if (window.Cypress) {
129 | this.tabs[id].results.executing = false
130 | }
131 | }).catch(error => notifier.error(`[${error.response.status}]: ${error.response.statusText}`))
132 | },
133 | rerun (id) {
134 | this.tabs[id].results.showResults = false
135 | this.start(id)
136 | },
137 | newBacktest (id) {
138 | this.tabs[id].results.showResults = false
139 | },
140 |
141 | candlesInfoEvent (id, data) {
142 | const list = [
143 | ['Period', data.duration],
144 | ['Starting Date', helpers.timestampToDate(
145 | data.starting_time
146 | )],
147 | ['Ending Date', helpers.timestampToDate(data.finishing_time)],
148 | ['Exchange Type', data.exchange_type],
149 | ]
150 | if (data.exchange_type === 'futures') {
151 | list.push(['Leverage', data.leverage])
152 | list.push(['Leverage Mode', data.leverage_mode])
153 | }
154 | this.tabs[id].results.info = list
155 | },
156 | routesInfoEvent (id, data) {
157 | const arr = [['Exchange', 'Symbol', 'Timeframe', 'Strategy']]
158 | data.forEach(item => {
159 | arr.push([
160 | { value: item.exchange, style: '' },
161 | { value: item.symbol, style: '' },
162 | { value: item.timeframe, style: '' },
163 | { value: item.strategy_name, style: '' },
164 | ])
165 | })
166 | this.tabs[id].results.routes_info = arr
167 | },
168 | progressbarEvent (id, data) {
169 | this.tabs[id].results.progressbar = data
170 | },
171 | infoLogEvent (id, data) {
172 | this.tabs[id].results.infoLogs += `[${helpers.timestampToTime(
173 | data.timestamp
174 | )}] ${data.message}\n`
175 | },
176 | exceptionEvent (id, data) {
177 | this.tabs[id].results.exception.error = data.error
178 | this.tabs[id].results.exception.traceback = data.traceback
179 | },
180 | generalInfoEvent (id, data) {
181 | this.tabs[id].results.generalInfo = data
182 | },
183 | hyperparametersEvent (id, data) {
184 | this.tabs[id].results.hyperparameters = data
185 | },
186 | metricsEvent (id, data) {
187 | // no trades were executed
188 | if (data === null) {
189 | this.tabs[id].results.metrics = []
190 | return
191 | }
192 |
193 | this.tabs[id].results.metrics = [
194 | ['Total Closed Trades', data.total],
195 | ['Total Net Profit', `${_.round(data.net_profit, 2)} (${_.round(data.net_profit_percentage, 2)}%)`],
196 | ['Starting => Finishing Balance', `${_.round(data.starting_balance, 2)} => ${_.round(data.finishing_balance, 2)}`],
197 | ['Open Trades', data.total_open_trades],
198 | // ['Open Trade\' PNL', data.open_pl],
199 | ['Total Paid Fees', _.round(data.fee, 2)],
200 | ['Max Drawdown', `${_.round(data.max_drawdown, 2)}%`],
201 | ['Annual Return', `${_.round(data.annual_return, 2)}%`],
202 | ['Expectancy', `${_.round(data.expectancy, 2)} (${_.round(data.expectancy_percentage, 2)}%)`],
203 | ['Avg Win | Avg Loss', `${_.round(data.average_win, 2)} | ${_.round(data.average_loss, 2)}`],
204 | ['Ratio Avg Win / Avg Loss', _.round(data.ratio_avg_win_loss, 2)],
205 | ['Win-rate', `${_.round(data.win_rate * 100, 2)}%`],
206 | ['Longs | Shorts', `${_.round(data.longs_percentage, 2)}% | ${_.round(data.shorts_percentage, 2)}%`],
207 | ['Avg Holding Time', helpers.secondsToHumanReadable(data.average_holding_period)],
208 | ['Winning Trades Avg Holding Time', helpers.secondsToHumanReadable(data.average_winning_holding_period)],
209 | ['Losing Trades Avg Holding Time', helpers.secondsToHumanReadable(data.average_losing_holding_period)],
210 | ['Sharpe Ratio', _.round(data.sharpe_ratio, 2)],
211 | ['Calmar Ratio', _.round(data.calmar_ratio, 2)],
212 | ['Sortino Ratio', _.round(data.sortino_ratio, 2)],
213 | ['Omega Ratio', _.round(data.omega_ratio, 2)],
214 | ['Winning Streak', data.winning_streak],
215 | ['Losing Streak', data.losing_streak],
216 | ['Largest Winning Trade', _.round(data.largest_winning_trade, 2)],
217 | ['Largest Losing Trade', _.round(data.largest_losing_trade, 2)],
218 | ['Total Winning Trades', data.total_winning_trades],
219 | ['Total Losing Trades', data.total_losing_trades]
220 | ]
221 | },
222 | equityCurveEvent (id, data) {
223 | // no trades were executed
224 | if (data === null) {
225 | this.tabs[id].results.charts.equity_curve = []
226 | } else {
227 | this.tabs[id].results.charts.equity_curve = []
228 | data.forEach(item => {
229 | this.tabs[id].results.charts.equity_curve.push({
230 | value: item.balance,
231 | time: item.timestamp
232 | })
233 | })
234 | }
235 |
236 | // backtest is finished, time to show charts:
237 | this.tabs[id].results.executing = false
238 | this.tabs[id].results.showResults = true
239 | },
240 | terminationEvent (id) {
241 | if (this.tabs[id].results.executing) {
242 | this.tabs[id].results.executing = false
243 | notifier.success('Session terminated successfully')
244 | }
245 | },
246 | alertEvent (id, data) {
247 | this.tabs[id].results.alert = data
248 | },
249 | }
250 | })
251 |
--------------------------------------------------------------------------------