├── .nvmrc ├── .prettierignore ├── .gitignore ├── src ├── screenshot.png ├── yahooFinance.png ├── index.jsx ├── yahooConverter.js ├── technicalindicators.js └── Main.jsx ├── .prettierrc.js ├── webpack ├── loaders.js ├── webpack.prod.js └── webpack.dev.js ├── .dockerignore ├── Dockerfile ├── CONTRIBUTING.md ├── babel.config.js ├── scripts ├── start.js ├── build.js ├── dev.js └── utils.js ├── .github ├── workflows │ ├── upgrade-tag-version.yaml │ └── build-and-deploy-to-scaleway.yaml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.20.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .DS_Store -------------------------------------------------------------------------------- /src/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomtom94/stockmarketpredictions/HEAD/src/screenshot.png -------------------------------------------------------------------------------- /src/yahooFinance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomtom94/stockmarketpredictions/HEAD/src/yahooFinance.png -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 140, 3 | tabWidth: 2, 4 | semi: false, 5 | singleQuote: true, 6 | trailingComma: 'none' 7 | } 8 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Main from './Main' 4 | 5 | ReactDOM.render(
, document.querySelector('#root')) 6 | -------------------------------------------------------------------------------- /webpack/loaders.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: [ 3 | { 4 | test: /\.(js|jsx)$/, 5 | exclude: /node_modules/, 6 | loader: "babel-loader", 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | Dockerfile 3 | .dockerignore 4 | .git 5 | .gitignore 6 | .prettierrc.js 7 | .prettierignore 8 | .eslintrc.js 9 | .eslintignore 10 | .env 11 | README.md 12 | 13 | .github -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.20.0-alpine 2 | 3 | USER root 4 | 5 | RUN mkdir -p /usr/src/app 6 | 7 | WORKDIR /usr/src/app 8 | 9 | ENV NPM_CONFIG_LOGLEVEL warn 10 | ENV PORT 80 11 | 12 | COPY . . 13 | 14 | RUN apk add --update npm 15 | 16 | RUN npx browserslist@latest --update-db 17 | 18 | RUN npm run build 19 | 20 | EXPOSE 80 21 | 22 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors and mainteners 2 | 3 | You can make a PR Pull Request whenever you want, you just need minimum 2 people (you and someone else) to validate your PR in order to merge it on the master branch. You don't even need the administrator to validate your PR. 4 | 5 | Please just follow the main guidelines of this project => give the easiest way to launch the app in order to use artificial intelligence via Tensorflow.js. 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | targets: "defaults", 9 | useBuiltIns: "usage", 10 | corejs: { version: "3.25", proposals: false }, 11 | shippedProposals: true, 12 | }, 13 | ], 14 | "@babel/preset-react", 15 | ]; 16 | 17 | const plugins = ["react-hot-loader/babel"]; 18 | 19 | return { 20 | presets, 21 | plugins, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | 4 | const { paths } = require("./utils"); 5 | 6 | const PORT = process.env.PORT || 3030; 7 | 8 | const app = express(); 9 | 10 | const start = async () => { 11 | try { 12 | app.use(cors()); 13 | 14 | app.use(express.static(paths.build)); 15 | 16 | app.listen(PORT, (err) => { 17 | if (err) { 18 | throw err; 19 | } 20 | console.log(`Server listening on port ${PORT} 🌎`); 21 | }); 22 | } catch (error) { 23 | console.error(error); 24 | } 25 | }; 26 | 27 | start(); 28 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-tag-version.yaml: -------------------------------------------------------------------------------- 1 | name: 1) Upgrade tag version 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | # pull_request: 8 | # branches: 9 | # - master 10 | 11 | jobs: 12 | build: 13 | name: Generate tag 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | name: Check out code 18 | with: 19 | fetch-depth: 0 20 | 21 | - uses: anothrNick/github-tag-action@1.36.0 22 | name: Bump version and push tag 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.REPO_ACCESS_TOKEN }} 25 | WITH_V: true 26 | DEFAULT_BUMP: none -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const rimraf = require("rimraf"); 3 | const webpackConfig = require("../webpack/webpack.prod"); 4 | 5 | const { compilerListener, paths, compilation } = require("./utils"); 6 | 7 | const build = async () => { 8 | try { 9 | rimraf.sync(paths.build); 10 | 11 | process.env.NODE_ENV = "production"; 12 | 13 | const compiler = webpack(webpackConfig); 14 | 15 | compiler.run((err, stats) => compilation(err, stats, compiler.stats)); 16 | 17 | await compilerListener(compiler); 18 | 19 | console.log("Webpack compilation client and server done !"); 20 | } catch (error) { 21 | console.error(error); 22 | } 23 | }; 24 | 25 | build(); 26 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const path = require("path"); 3 | const { paths, templateContent } = require("../scripts/utils"); 4 | 5 | module.exports = { 6 | name: "client", 7 | mode: "production", 8 | target: "web", 9 | entry: { 10 | bundle: [paths.client], 11 | }, 12 | output: { 13 | path: path.join(paths.build, paths.publicPath), 14 | filename: "bundle-[fullhash].js", 15 | publicPath: paths.publicPath, 16 | }, 17 | resolve: { 18 | modules: [paths.src, "node_modules"], 19 | extensions: [".js", ".jsx"], 20 | }, 21 | module: require("./loaders.js"), 22 | plugins: [ 23 | new HtmlWebpackPlugin({ 24 | templateContent, 25 | }), 26 | ], 27 | stats: "normal", 28 | }; 29 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const path = require("path"); 3 | const webpack = require("webpack"); 4 | const { paths, templateContent } = require("../scripts/utils"); 5 | 6 | module.exports = { 7 | name: "client", 8 | mode: "development", 9 | target: "web", 10 | devtool: "source-map", 11 | entry: { 12 | bundle: [paths.client], 13 | }, 14 | output: { 15 | path: path.join(paths.build, paths.publicPath), 16 | filename: "bundle.js", 17 | publicPath: paths.publicPath, 18 | }, 19 | resolve: { 20 | modules: [paths.src, "node_modules"], 21 | extensions: [".js", ".jsx"], 22 | alias: { 23 | "react-dom": "@hot-loader/react-dom", 24 | }, 25 | }, 26 | module: require("./loaders.js"), 27 | plugins: [ 28 | new HtmlWebpackPlugin({ 29 | templateContent, 30 | }), 31 | new webpack.HotModuleReplacementPlugin(), 32 | ], 33 | stats: "minimal", 34 | }; 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thomas Aumaitre 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/yahooConverter.js: -------------------------------------------------------------------------------- 1 | export default (data) => 2 | data.chart.result[0].timestamp.reduce( 3 | (acc, timestamp, index) => ({ 4 | ...acc, 5 | 'Time Series (Daily)': { 6 | ...acc['Time Series (Daily)'], 7 | [`${new Date(timestamp * 1000).getFullYear()}-${new Date(timestamp * 1000).getMonth() + 1}-${new Date( 8 | timestamp * 1000 9 | ).getDate()}`]: { 10 | '1. open': data.chart.result[0].indicators.quote[0].open[index], 11 | '2. high': data.chart.result[0].indicators.quote[0].high[index], 12 | '3. low': data.chart.result[0].indicators.quote[0].low[index], 13 | '4. close': data.chart.result[0].indicators.adjclose[0].adjclose[index], 14 | '5. volume': data.chart.result[0].indicators.quote[0].volume[index] 15 | } 16 | } 17 | }), 18 | { 19 | 'Meta Data': { 20 | '1. Information': 'Daily Prices (open, high, low, close) and Volumes', 21 | '2. Symbol': data.chart.result[0].meta.symbol, 22 | '3. Last Refreshed': undefined, 23 | '4. Output Size': data.chart.result[0].timestamp.length + 1, 24 | '5. Time Zone': data.chart.result[0].meta.exchangeTimezoneName 25 | }, 26 | 'Time Series (Daily)': {} 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stockmarketpredictions", 3 | "version": "v0.2.3", 4 | "description": "Stock Market Predictions", 5 | "main": "scripts/start.js", 6 | "engines": { 7 | "node": ">=16.3.0" 8 | }, 9 | "repository": { 10 | "type": "git" 11 | }, 12 | "author": { 13 | "name": "Thomas Aumaitre", 14 | "email": "thomas.aumaitre@gmail.com" 15 | }, 16 | "browserslist": [ 17 | "defaults" 18 | ], 19 | "license": "SEE LICENSE IN LICENSE", 20 | "scripts": { 21 | "build": "node scripts/build.js", 22 | "start": "node scripts/start.js", 23 | "dev": "node scripts/dev.js", 24 | "install:clean": "rm -rf node_modules/ && rm -rf dist/ && rm -rf package-lock.json && npm install" 25 | }, 26 | "dependencies": { 27 | "@babel/core": "^7.16.0", 28 | "@babel/preset-env": "^7.16.0", 29 | "@babel/preset-react": "^7.16.0", 30 | "@hot-loader/react-dom": "^16.8.3", 31 | "@tensorflow/tfjs": "^3.13.0", 32 | "axios": "^0.25.0", 33 | "babel-loader": "^8.2.3", 34 | "core-js": "^3.25.3", 35 | "cors": "^2.8.5", 36 | "express": "^4.17.1", 37 | "highcharts": "^9.3.2", 38 | "highcharts-react-official": "^3.1.0", 39 | "html-webpack-plugin": "^5.5.0", 40 | "path": "^0.12.7", 41 | "react": "^16.14.0", 42 | "react-dom": "^16.14.0", 43 | "react-hot-loader": "^4.13.0", 44 | "rimraf": "^3.0.2", 45 | "webpack": "^5.61.0" 46 | }, 47 | "devDependencies": { 48 | "webpack-dev-middleware": "^5.2.1", 49 | "webpack-hot-middleware": "^2.25.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const rimraf = require("rimraf"); 3 | const express = require("express"); 4 | const webpackDevMiddleware = require("webpack-dev-middleware"); 5 | const webpackHotMiddleware = require("webpack-hot-middleware"); 6 | const cors = require("cors"); 7 | const webpackConfig = require("../webpack/webpack.dev"); 8 | 9 | const { compilerListener, paths } = require("./utils"); 10 | 11 | const PORT = 3030; 12 | 13 | const app = express(); 14 | 15 | const dev = async () => { 16 | try { 17 | rimraf.sync(paths.build); 18 | 19 | process.env.NODE_ENV = "development"; 20 | 21 | webpackConfig.entry.bundle = [ 22 | `webpack-hot-middleware/client?path=http://localhost:${PORT}/__webpack_hmr&timeout=2000`, 23 | ...webpackConfig.entry.bundle, 24 | ]; 25 | webpackConfig.output.hotUpdateMainFilename = 26 | "updates/[fullhash].hot-update.json"; 27 | webpackConfig.output.hotUpdateChunkFilename = 28 | "updates/[id].[fullhash].hot-update.js"; 29 | 30 | const compiler = webpack(webpackConfig); 31 | 32 | app.use(cors()); 33 | 34 | app.use( 35 | webpackDevMiddleware(compiler, { 36 | publicPath: webpackConfig.output.publicPath, 37 | stats: webpackConfig.stats, 38 | writeToDisk: true, 39 | }) 40 | ); 41 | 42 | app.use( 43 | webpackHotMiddleware(compiler, { 44 | log: console.log, 45 | path: "/__webpack_hmr", 46 | heartbeat: 2000, 47 | }) 48 | ); 49 | 50 | await compilerListener(compiler); 51 | 52 | app.listen(PORT, (err) => { 53 | if (err) { 54 | throw err; 55 | } 56 | console.log(`Hot dev server http://localhost:${PORT} 🌎`); 57 | }); 58 | } catch (error) { 59 | console.error(error); 60 | } 61 | }; 62 | 63 | dev(); 64 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | const appDirectory = fs.realpathSync(process.cwd()); 5 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); 6 | const paths = { 7 | publicPath: "/", 8 | build: resolveApp("dist"), 9 | client: resolveApp("src/index.jsx"), 10 | src: resolveApp("src"), 11 | }; 12 | 13 | const compilerListener = (compiler) => { 14 | return new Promise((resolve, reject) => { 15 | compiler.hooks.compile.tap(compiler.name, () => { 16 | console.log(`Compiling ${compiler.name} please wait...`); 17 | }); 18 | 19 | compiler.hooks.failed.tap(compiler.name, (error) => { 20 | reject(error); 21 | }); 22 | compiler.hooks.done.tap(compiler.name, (stats) => { 23 | if (!stats.hasErrors()) { 24 | resolve(); 25 | } 26 | if (stats.hasErrors()) { 27 | stats.compilation.errors.forEach((error) => { 28 | reject(error); 29 | }); 30 | } 31 | if (stats.hasWarnings()) { 32 | stats.compilation.warnings.forEach((warning) => { 33 | console.warn(warning); 34 | }); 35 | } 36 | }); 37 | }); 38 | }; 39 | 40 | const compilation = (err, stats, format) => { 41 | if (err) { 42 | console.error(err.stack || err); 43 | if (err.details) { 44 | console.error(err.details); 45 | } 46 | return; 47 | } 48 | 49 | console.log(stats.toString(format)); 50 | }; 51 | 52 | const templateContent = ` 53 | 54 | 55 | 56 | 57 | Stock Market Predictions 58 | 59 | 60 |
61 | 62 | `; 63 | 64 | module.exports = { 65 | compilerListener, 66 | paths, 67 | compilation, 68 | templateContent, 69 | }; 70 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy-to-scaleway.yaml: -------------------------------------------------------------------------------- 1 | name: 2) Build and deploy to Scaleway 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | name: Build and push image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | name: Check out code 15 | with: 16 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 17 | 18 | - name: Bump version in package.json file 19 | run: | 20 | sed -i '0,/"version": "/{s/"version": "\([^#]*\)",/"version": "${{ github.ref_name }}",/}' package.json package-lock.json 21 | 22 | - uses: EndBug/add-and-commit@v9 23 | with: 24 | message: Bump version to ${{ github.ref_name }} 25 | push: origin HEAD:master 26 | 27 | - uses: mr-smithers-excellent/docker-build-push@v5 28 | name: Build & push Docker image 29 | with: 30 | image: stockmarketpredictionsfront 31 | tags: ${{ github.ref_name }} 32 | registry: rg.fr-par.scw.cloud/funcscwstockmarketpredictiocvxl0lfn 33 | dockerfile: Dockerfile 34 | username: ${{ secrets.SCW_ACCESS_KEY }} 35 | password: ${{ secrets.SCW_SECRET_TOKEN }} 36 | 37 | - name: Patch Scaleway Serverless container front 38 | uses: wei/curl@v1 39 | with: 40 | args: --location --request PATCH https://api.scaleway.com/containers/v1beta1/regions/fr-par/containers/b19dce0b-18ba-4ef0-a1cf-3eacb7e15207 --header 'X-Auth-Token:${{ secrets.SCW_SECRET_TOKEN }}' --header 'Content-Type:application/json' --data-raw '{\"registry_image\":\"rg.fr-par.scw.cloud/funcscwstockmarketpredictiocvxl0lfn/stockmarketpredictionsfront:${{ github.ref_name }}\"}' 41 | 42 | - name: Deploy Scaleway Serverless container front 43 | uses: wei/curl@v1 44 | with: 45 | args: --location --request POST https://api.scaleway.com/containers/v1beta1/regions/fr-par/containers/b19dce0b-18ba-4ef0-a1cf-3eacb7e15207/deploy --header 'X-Auth-Token:${{ secrets.SCW_SECRET_TOKEN }}' --header 'Content-Type:application/json' --data-raw '{}' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elegant and for real stock predictions App with Tensorflow.js 2 | 3 | ## Introduction 4 | 5 | Predictions made on Amazon stock market (fresh data until 2022-02-03) with Tensorflow.js. But you can download freshened up data :) 6 | 7 | Check out this app in live [stockmarketpredictiocvxl0lfn-smpfront.functions.fnc.fr-par.scw.cloud](https://stockmarketpredictiocvxl0lfn-smpfront.functions.fnc.fr-par.scw.cloud) 8 | 9 | ## Table of contents 10 | 11 | - [Motivations](#motivations) 12 | - [Getting started](#getting-started) 13 | - [Requirements](#requirements) 14 | - [Start in dev mode](#start-in-dev-mode) 15 | - [Start in production mode](#start-in-production-mode) 16 | - [With Node.js](#with-nodejs) 17 | - [With Docker](#with-docker) 18 | - [Must know about the app](#must-know-about-the-app) 19 | - [Import daily data from Yahoo Finance](#import-daily-data-from-yahoo-finance) 20 | - [Continuous Integration and Continuous Delivery](#continuous-integration-and-continuous-delivery) 21 | - [Notes](#notes) 22 | 23 | ## Motivations 24 | 25 | Just wanna spend my time using Tensorflow.js :) 26 | Data come from the free API [alphavantage.co](https://www.alphavantage.co/query?function=TIME_SERIES_DAILY_ADJUSTED&symbol=AMZN&outputsize=full&apikey=NOKEY) 27 | 28 | ![alt text](src/screenshot.png?raw=true "Result graph with the predictions line") 29 | 30 | ## Getting started 31 | 32 | Clone the repo 33 | 34 | ```git 35 | git clone https://github.com/tomtom94/stockmarketpredictions.git 36 | ``` 37 | 38 | ```git 39 | cd stockmarketpredictions 40 | ``` 41 | 42 | ### Requirements 43 | 44 | Node.js version v16.3.0 minimum (because we need to use the [js optional chaining operator](https://node.green/#ES2020)). Hopefully you got `nvm` command already installed (best way to install node), hence just do 45 | 46 | ```nvm 47 | nvm use 48 | ``` 49 | 50 | it's gonna use the `.nvmrc` file with v16.20.0 51 | 52 | ### Start in dev mode 53 | 54 | ```npm 55 | npm install 56 | ``` 57 | 58 | Run dev mode with 59 | 60 | ```npm 61 | npm run dev 62 | ``` 63 | 64 | it's gonna start an hot dev middleware with an express server ;) ready to work `http://localhost:3000` 65 | 66 | ### Start in production mode 67 | 68 | #### With Node.js 69 | 70 | ```npm 71 | npm install 72 | ``` 73 | 74 | Run build mode with 75 | 76 | ```npm 77 | npm run build 78 | ``` 79 | 80 | it's gonna build in `dist/` 81 | 82 | Then run in production mode 83 | 84 | ```npm 85 | npm run start 86 | ``` 87 | 88 | ;) it's gonna start the only one SSR express server out of the box for internet `http://localhost:3000` or environment port used. 89 | 90 | #### With Docker 91 | 92 | ```docker 93 | docker build -t stockmarketpredictions . 94 | ``` 95 | 96 | ```docker 97 | docker run -p 80:80 stockmarketpredictions 98 | ``` 99 | 100 | Then open `http://localhost:80` 101 | 102 | ## Must know about the app 103 | 104 | You better use a good search engine like [Qwant.com](https://qwant.com), don't use Google. please. 105 | 106 | ### Import daily data from Yahoo Finance 107 | 108 | You can run any company stock market you want, just download it yourself from Yahoo Finance. 109 | 110 | In order to do so choose your own ticker for example CGG company in France, from this page https://fr.finance.yahoo.com/chart/CGG.PA open your browser console, click 5A to display 5 years of data. Then copy the gross json result from the api call into the texte area in the web page, click Submit Yahoo Data button. 111 | 112 | ![alt text](src/yahooFinance.png?raw=true "Yahoo Finance") 113 | 114 | The screenshot above will help you do the job. 115 | 116 | 117 | ### Continuous Integration and Continuous Delivery 118 | 119 | When pushing or merging on master branch, you can trigger Github Actions with a commit message that includes `#major`, `#minor` or `#patch`. 120 | 121 | Example of commit message in order to start a deployment : 122 | 123 | ```git 124 | git commit -m "#major this is a big commit" 125 | ``` 126 | 127 | ```git 128 | git commit -m "#patch this is a tiny commit" 129 | ``` 130 | 131 | ## Notes 132 | 133 | If ever you wanna brainstorm, download my resume you are gonna find my phone number 134 | 135 | - [thomasdeveloper-react.com](https://www.thomasdeveloper-react.com) 136 | -------------------------------------------------------------------------------- /src/technicalindicators.js: -------------------------------------------------------------------------------- 1 | export const SMA = ({ period, data }) => { 2 | return data.reduce((acc, curr, index, array) => { 3 | const baseIndex = index - period + 1; 4 | if (baseIndex < 0) { 5 | return acc; 6 | } 7 | const targetValues = array.slice(baseIndex, index + 1); 8 | const total = targetValues.reduce( 9 | (total, targetValue) => total + Number(targetValue[1]["4. close"]), 10 | 0 11 | ); 12 | const value = total / period; 13 | return [...acc, value]; 14 | }, []); 15 | }; 16 | 17 | export const EMA = ({ period, data }) => { 18 | const k = 2 / (period + 1); 19 | let EMAYesterday; 20 | return data.reduce((acc, curr, index, array) => { 21 | if (!EMAYesterday) { 22 | const value = Number(curr[1]["4. close"]); 23 | EMAYesterday = value; 24 | return acc; 25 | } 26 | const value = Number(curr[1]["4. close"]) * k + EMAYesterday * (1 - k); 27 | EMAYesterday = value; 28 | return [...acc, value]; 29 | }, []); 30 | }; 31 | 32 | export const stochastic = ({ period, data }) => { 33 | // https://investexcel.net/how-to-calculate-the-stochastic-oscillator/ 34 | return data.reduce((acc, curr, index, array) => { 35 | const baseIndex = index - period + 1; 36 | if (baseIndex < 0) { 37 | return acc; 38 | } 39 | const targetValues = array.slice(baseIndex, index + 1); 40 | const currentValue = targetValues[targetValues.length - 1][1]["4. close"]; 41 | const highestHigh = targetValues.reduce((acc, targetValue) => { 42 | const indicator = Number(targetValue[1]["2. high"]); 43 | if (!acc || indicator > acc) { 44 | return indicator; 45 | } 46 | return acc; 47 | }, null); 48 | const lowestLow = targetValues.reduce((acc, targetValue) => { 49 | const indicator = Number(targetValue[1]["3. low"]); 50 | if (!acc || indicator < acc) { 51 | return indicator; 52 | } 53 | return acc; 54 | }, null); 55 | const value = 56 | ((currentValue - lowestLow) / (highestHigh - lowestLow)) * 100; 57 | return [...acc, value]; 58 | }, []); 59 | }; 60 | 61 | export const RSI = ({ period, data }) => { 62 | // https://www.macroption.com/rsi-calculation/ 63 | return data.reduce((acc, curr, index, array) => { 64 | const baseIndex = index - period; 65 | if (baseIndex < 0) { 66 | return acc; 67 | } 68 | const targetValues = array.slice(baseIndex, index + 1); 69 | let _lastCloseUpMove; 70 | const upMoves = targetValues.reduce( 71 | (acc, targetValue) => { 72 | if (!_lastCloseUpMove) { 73 | _lastCloseUpMove = Number(targetValue[1]["4. close"]); 74 | return acc; 75 | } 76 | const diff = Number(targetValue[1]["4. close"]) - _lastCloseUpMove; 77 | 78 | _lastCloseUpMove = Number(targetValue[1]["4. close"]); 79 | if (diff < 0) { 80 | return acc; 81 | } 82 | return { total: acc.total + diff, count: acc.count + 1 }; 83 | }, 84 | { total: 0, count: 0 } 85 | ); 86 | 87 | let _lastCloseDownMove; 88 | const downMoves = targetValues.reduce( 89 | (acc, targetValue) => { 90 | if (!_lastCloseDownMove) { 91 | _lastCloseDownMove = Number(targetValue[1]["4. close"]); 92 | return acc; 93 | } 94 | const diff = Number(targetValue[1]["4. close"]) - _lastCloseDownMove; 95 | _lastCloseDownMove = Number(targetValue[1]["4. close"]); 96 | if (diff > 0) { 97 | return acc; 98 | } 99 | return { total: acc.total + Math.abs(diff), count: acc.count + 1 }; 100 | }, 101 | { total: 0, count: 0 } 102 | ); 103 | const averageUpMoves = 104 | upMoves.count > 0 ? upMoves.total / upMoves.count : null; 105 | const averageDownMoves = 106 | downMoves.count > 0 ? downMoves.total / downMoves.count : null; 107 | const relativeStrength = 108 | averageDownMoves > 0 ? averageUpMoves / averageDownMoves : null; 109 | const value = 110 | relativeStrength === null ? 100 : 100 - 100 / (1 + relativeStrength); 111 | 112 | return [...acc, value]; 113 | }, []); 114 | }; 115 | 116 | export const seasonality = ({ element, fn, days }) => { 117 | const timestamp = new Date(element[0]).getTime(); 118 | const value = Math[fn](timestamp * ((2 * Math.PI) / days)); 119 | return value; 120 | }; 121 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | thomas.aumaitre@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/Main.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useCallback, useState, useRef } from 'react' 2 | import { hot } from 'react-hot-loader/root' 3 | import Highcharts from 'highcharts/highstock' 4 | import axios from 'axios' 5 | import HighchartsReact from 'highcharts-react-official' 6 | import * as tf from '@tensorflow/tfjs' 7 | import { SMA, RSI, stochastic, seasonality, EMA } from './technicalindicators' 8 | import yahooConverter from './yahooConverter' 9 | import stockMarketDataDaily from './stockMarketDataDaily.json' 10 | 11 | const Main = () => { 12 | const [data, setData] = useState([]) 13 | const [series, setSeries] = useState([]) 14 | const [isModelTraining, setIsModelTraining] = useState(false) 15 | const [modelLogs, setModelLogs] = useState([]) 16 | const [modelResultTraining, setModelResultTraining] = useState(null) 17 | const modelLogsRef = useRef([]) 18 | const [investing, setInvesting] = useState({ start: 1000, end: null }) 19 | const [dataEma10, setDataEma10] = useState(null) 20 | const [dataEma20, setDataEma20] = useState(null) 21 | const [dataEma50, setDataEma50] = useState(null) 22 | const [dataSma10, setDataSma10] = useState(null) 23 | const [dataSma20, setDataSma20] = useState(null) 24 | const [dataSma50, setDataSma50] = useState(null) 25 | const [dataSma100, setDataSma100] = useState(null) 26 | const [dataRsi7, setDataRsi7] = useState(null) 27 | const [dataRsi14, setDataRsi14] = useState(null) 28 | const [dataRsi28, setDataRsi28] = useState(null) 29 | const [dataStochastic7, setDataStochastic7] = useState(null) 30 | const [dataStochastic14, setDataStochastic14] = useState(null) 31 | const [formError, setFormError] = useState(null) 32 | const [sampleData, setSampleData] = useState(null) 33 | const [graphTitle, setGraphTitle] = useState(null) 34 | const [recurrence, setRecurrence] = useState(16) 35 | const [strategy, setStrategy] = useState(2) 36 | const [isPredictionLoading, setIsPredictionLoading] = useState(false) 37 | 38 | const epochs = 7 39 | const timeserieSize = recurrence 40 | const batchSize = 32 41 | 42 | useEffect(() => { 43 | const areAllDataReady = 44 | data.length > 0 && 45 | dataEma10 && 46 | dataEma20 && 47 | dataEma50 && 48 | dataSma10 && 49 | dataSma20 && 50 | dataSma50 && 51 | dataSma100 && 52 | dataRsi7 && 53 | dataRsi14 && 54 | dataRsi28 && 55 | dataStochastic7 && 56 | dataStochastic14 57 | if (areAllDataReady) { 58 | const sampleDataRaw = splitData([0, 1]).map((e) => e[1]) 59 | const { dataNormalized: sampleDataNormalized, dimensionParams: sampleDimensionParams } = normalizeData(sampleDataRaw) 60 | setSampleData({ 61 | sampleDataRaw: JSON.parse(JSON.stringify(sampleDataRaw)) 62 | .reverse() 63 | .filter((e, i) => i < 5), 64 | sampleDimensionParams 65 | }) 66 | } 67 | }, [ 68 | data, 69 | dataEma10, 70 | dataEma20, 71 | dataEma50, 72 | dataSma10, 73 | dataSma20, 74 | dataSma50, 75 | dataSma100, 76 | dataRsi7, 77 | dataRsi14, 78 | dataRsi28, 79 | dataStochastic7, 80 | dataStochastic14 81 | ]) 82 | 83 | useEffect(() => { 84 | if (data.length) { 85 | setDataStochastic7( 86 | stochastic({ 87 | period: 7, 88 | data 89 | }) 90 | ) 91 | setDataStochastic14( 92 | stochastic({ 93 | period: 14, 94 | data 95 | }) 96 | ) 97 | setDataRsi7( 98 | RSI({ 99 | period: 7, 100 | data 101 | }) 102 | ) 103 | setDataRsi14( 104 | RSI({ 105 | period: 14, 106 | data 107 | }) 108 | ) 109 | setDataRsi28( 110 | RSI({ 111 | period: 28, 112 | data 113 | }) 114 | ) 115 | setDataSma10( 116 | SMA({ 117 | period: 10, 118 | data 119 | }) 120 | ) 121 | setDataSma20( 122 | SMA({ 123 | period: 20, 124 | data 125 | }) 126 | ) 127 | setDataSma50( 128 | SMA({ 129 | period: 50, 130 | data 131 | }) 132 | ) 133 | setDataEma10( 134 | EMA({ 135 | period: 10, 136 | data 137 | }) 138 | ) 139 | setDataEma20( 140 | EMA({ 141 | period: 20, 142 | data 143 | }) 144 | ) 145 | setDataEma50( 146 | EMA({ 147 | period: 50, 148 | data 149 | }) 150 | ) 151 | setDataSma100( 152 | SMA({ 153 | period: 100, 154 | data 155 | }) 156 | ) 157 | } 158 | }, [data]) 159 | 160 | useEffect(() => { 161 | setData(Object.entries(stockMarketDataDaily['Time Series (Daily)']).sort((a, b) => new Date(a[0]) - new Date(b[0]))) 162 | setGraphTitle(stockMarketDataDaily['Meta Data']['2. Symbol']) 163 | }, [stockMarketDataDaily]) 164 | 165 | // useEffect(() => { 166 | // setData( 167 | // stockMarketDataHourly.sort((a, b) => new Date(a[0]) - new Date(b[0])) 168 | // ); 169 | // setGraphTitle("AMZN Hourly"); 170 | // }, [stockMarketDataHourly]); 171 | 172 | useEffect(() => { 173 | if (data.length && series.findIndex((serie) => serie.name === 'Stock value') === -1) { 174 | setSeries([ 175 | ...series, 176 | { 177 | type: 'area', 178 | id: 'dataseries', 179 | name: 'Stock value', 180 | data: data.map((serie) => [new Date(serie[0]).getTime(), Number(serie[1]['4. close'])]), 181 | gapSize: 5, 182 | tooltip: { 183 | valueDecimals: 2 184 | }, 185 | fillColor: { 186 | linearGradient: { 187 | x1: 0, 188 | y1: 0, 189 | x2: 0, 190 | y2: 1 191 | }, 192 | stops: [ 193 | [0, Highcharts.getOptions().colors[0]], 194 | [1, Highcharts.color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')] 195 | ] 196 | }, 197 | threshold: null 198 | } 199 | ]) 200 | } 201 | }, [data, series]) 202 | 203 | const getNewStock = async (event) => { 204 | event.preventDefault() 205 | if (!event.target.symbol && !event.target.yahooData) { 206 | return 207 | } 208 | setGraphTitle(null) 209 | setSampleData(null) 210 | setFormError(null) 211 | setInvesting({ start: 1000, end: null }) 212 | setDataEma10(null) 213 | setDataEma20(null) 214 | setDataEma50(null) 215 | setDataSma10(null) 216 | setDataSma20(null) 217 | setDataSma50(null) 218 | setDataSma100(null) 219 | setDataRsi7(null) 220 | setDataRsi14(null) 221 | setDataRsi28(null) 222 | setDataStochastic7(null) 223 | setDataStochastic14(null) 224 | setModelResultTraining(null) 225 | setModelLogs([]) 226 | setIsModelTraining(false) 227 | setSeries([]) 228 | setData([]) 229 | modelLogsRef.current = [] 230 | 231 | try { 232 | if (event.target.symbol) { 233 | const { data } = await axios.get( 234 | `https://www.alphavantage.co/query?${new URLSearchParams({ 235 | function: 'TIME_SERIES_DAILY_ADJUSTED', 236 | symbol: event.target.symbol.value, 237 | outputsize: 'full', 238 | apikey: '73H4T3JL70SI8VON' 239 | })}`, 240 | { 241 | method: 'GET', 242 | headers: { 'Content-Type': 'application/json' } 243 | } 244 | ) 245 | if (data['Error Message']) { 246 | throw new Error(data['Error Message']) 247 | } 248 | setData(Object.entries(data['Time Series (Daily)']).sort((a, b) => new Date(a[0]) - new Date(b[0]))) 249 | setGraphTitle(data['Meta Data']['2. Symbol']) 250 | } 251 | if (event.target.yahooData) { 252 | const yahooDataTreated = yahooConverter(JSON.parse(event.target.yahooData.value)) 253 | setData(Object.entries(yahooDataTreated['Time Series (Daily)']).sort((a, b) => new Date(a[0]) - new Date(b[0]))) 254 | setGraphTitle(yahooDataTreated['Meta Data']['2. Symbol']) 255 | } 256 | } catch (error) { 257 | setFormError(error.message) 258 | } 259 | } 260 | 261 | const splitData = (trainingRange) => { 262 | const descEma10 = JSON.parse(JSON.stringify(dataEma10)).reverse() 263 | const descEma20 = JSON.parse(JSON.stringify(dataEma20)).reverse() 264 | const descEma50 = JSON.parse(JSON.stringify(dataEma50)).reverse() 265 | const descSma10 = JSON.parse(JSON.stringify(dataSma10)).reverse() 266 | const descSma20 = JSON.parse(JSON.stringify(dataSma20)).reverse() 267 | const descSma50 = JSON.parse(JSON.stringify(dataSma50)).reverse() 268 | const descSma100 = JSON.parse(JSON.stringify(dataSma100)).reverse() 269 | const descRsi7 = JSON.parse(JSON.stringify(dataRsi7)).reverse() 270 | const descRsi14 = JSON.parse(JSON.stringify(dataRsi14)).reverse() 271 | const descRsi28 = JSON.parse(JSON.stringify(dataRsi28)).reverse() 272 | const descStochastic7 = JSON.parse(JSON.stringify(dataStochastic7)).reverse() 273 | const descStochastic14 = JSON.parse(JSON.stringify(dataStochastic14)).reverse() 274 | const dataRaw = JSON.parse(JSON.stringify(data)) 275 | .reverse() 276 | .reduce((acc, curr, index, array) => { 277 | if ( 278 | !descEma10[index] || 279 | !descEma20[index] || 280 | !descEma50[index] || 281 | !descSma10[index] || 282 | !descSma20[index] || 283 | !descSma50[index] || 284 | !descSma100[index] || 285 | !descRsi7[index] || 286 | !descRsi14[index] || 287 | !descRsi28[index] || 288 | !descStochastic7[index] || 289 | !descStochastic14[index] 290 | ) { 291 | return acc 292 | } 293 | return [ 294 | ...acc, 295 | [ 296 | curr[0], 297 | [ 298 | Number(curr[1]['4. close']), 299 | Number(curr[1]['1. open']), 300 | Number(curr[1]['2. high']), 301 | Number(curr[1]['3. low']), 302 | // Number(curr[1]["5. volume"]), 303 | descEma10[index], 304 | descEma20[index], 305 | descEma50[index], 306 | descSma10[index], 307 | descSma20[index], 308 | descSma50[index], 309 | descSma100[index], 310 | descRsi7[index], 311 | descRsi14[index], 312 | descRsi28[index], 313 | descStochastic7[index], 314 | descStochastic14[index] 315 | // seasonality({ element: curr, fn: "cos", days: 7 * 24 * 60 * 60 }), 316 | // seasonality({ element: curr, fn: "sin", days: 7 * 24 * 60 * 60 }), 317 | ] 318 | ] 319 | ] 320 | }, []) 321 | .reverse() 322 | const [bottom, top] = trainingRange 323 | const baseIndex = bottom === 0 ? 0 : Math.ceil(bottom * dataRaw.length) - 1 324 | const chunk = dataRaw.slice(baseIndex, top === 1 ? undefined : baseIndex + 1 + Math.floor((top - bottom) * dataRaw.length)) 325 | return chunk 326 | } 327 | 328 | const createTimeseriesDimensionForRNN = (inputs) => { 329 | inputs.reverse() 330 | const chunks = [] 331 | for (let i = 0; i < inputs.length - 1; i++) { 332 | chunks.push(inputs.slice(i, i + timeserieSize)) 333 | } 334 | 335 | const newChunks = chunks.map((e) => e.reverse()).reverse() 336 | const timeseriesChunks = [] 337 | newChunks.forEach((chunk) => { 338 | if (chunk.length === timeserieSize) { 339 | timeseriesChunks.push(chunk) 340 | } 341 | }) 342 | return timeseriesChunks 343 | } 344 | 345 | const normalizeData = (dataRaw, params) => { 346 | const unstackData = (stachData, axis) => stachData.map((e) => e[axis]) 347 | const dataPerDimension = [] 348 | for (let i = 0; i < dataRaw[0].length; i++) { 349 | dataPerDimension.push(unstackData(dataRaw, i)) 350 | } 351 | let dimensionParams = params 352 | 353 | if (!params) { 354 | dimensionParams = dataPerDimension.map((dimension) => { 355 | const mean = dimension.reduce((acc, curr) => acc + curr, 0) / dimension.length 356 | const min = dimension.reduce((acc, curr) => { 357 | if (acc === null || curr < acc) { 358 | return curr 359 | } 360 | return acc 361 | }, null) 362 | const max = dimension.reduce((acc, curr) => { 363 | if (acc === null || curr > acc) { 364 | return curr 365 | } 366 | return acc 367 | }, null) 368 | return { 369 | min, 370 | max, 371 | mean, 372 | std: Math.sqrt(dimension.map((e) => (e - mean) ** 2).reduce((acc, curr) => acc + curr, 0) / dimension.length) 373 | // https://www.geeksforgeeks.org/numpy-std-in-python/ 374 | } 375 | }) 376 | } 377 | const epsilon = 1e-3 378 | return { 379 | dataNormalized: createTimeseriesDimensionForRNN( 380 | dataRaw.map((set) => 381 | set.map( 382 | (e, i) => (e - dimensionParams[i].mean) / (dimensionParams[i].std + epsilon) 383 | // https://www.tensorflow.org/tutorials/structured_data/time_series#normalize_the_data 384 | ) 385 | ) 386 | ), 387 | dimensionParams 388 | } 389 | } 390 | 391 | const unNormalizeData = (dataRaw, dimensionParam) => dataRaw.map((e) => e * dimensionParam.std + dimensionParam.mean) 392 | 393 | const makeDataset = async (range) => { 394 | const dataRange = splitData(range).map((e) => e[1]) 395 | const { dataNormalized, dimensionParams } = normalizeData(dataRange) 396 | const xDataset = tf.data.array(dataNormalized).take(dataNormalized.length - 1) 397 | const yDataset = tf.data 398 | .array(dataNormalized) 399 | .map((e) => e[e.length - 1][0]) 400 | .skip(1) 401 | const xyDataset = tf.data.zip({ xs: xDataset, ys: yDataset }).batch(batchSize).shuffle(batchSize) 402 | // const datasetLogs = []; 403 | // await xyDataset.forEachAsync((e) => { 404 | // datasetLogs.push(e); 405 | // }); 406 | // console.log("datasetLogs", datasetLogs); 407 | return { 408 | dataset: xyDataset, 409 | dimensionParams 410 | } 411 | } 412 | 413 | const createModel = async () => { 414 | try { 415 | rebootSeries() 416 | setIsModelTraining(true) 417 | setModelResultTraining(null) 418 | const { dataset: train } = await makeDataset([0, 0.7]) 419 | const { dataset: validate } = await makeDataset([0.7, 0.9]) 420 | const model = tf.sequential() 421 | 422 | const cells = [ 423 | tf.layers.lstmCell({ units: 16 }), 424 | tf.layers.lstmCell({ units: 16 }), 425 | tf.layers.lstmCell({ units: 16 }), 426 | tf.layers.lstmCell({ units: 16 }) 427 | ] 428 | 429 | model.add( 430 | tf.layers.rnn({ 431 | cell: cells, 432 | inputShape: [timeserieSize, 16], 433 | returnSequences: false 434 | }) 435 | ) 436 | 437 | model.add( 438 | tf.layers.dense({ 439 | units: 1 440 | }) 441 | ) 442 | 443 | // model.summary(); 444 | 445 | model.compile({ 446 | optimizer: 'adam', 447 | loss: 'meanSquaredError', 448 | metrics: ['accuracy'] 449 | }) 450 | setModelLogs([]) 451 | modelLogsRef.current = [] 452 | const history = await model.fitDataset(train, { 453 | epochs, 454 | validationData: validate, 455 | callbacks: { 456 | onEpochEnd: (epoch, log) => { 457 | modelLogsRef.current.push([epoch + 1, log.loss]) 458 | setModelLogs([...modelLogsRef.current]) 459 | } 460 | } 461 | }) 462 | const result = { 463 | model: model, 464 | stats: history 465 | } 466 | 467 | setModelResultTraining(result) 468 | } catch (error) { 469 | throw error 470 | } finally { 471 | setIsModelTraining(false) 472 | } 473 | } 474 | 475 | const gessLabels = (inputs, dimensionParams) => { 476 | const xs = tf.tensor3d(inputs) 477 | let outputs = modelResultTraining.model.predict(xs) 478 | outputs = Array.from(outputs.dataSync()) 479 | const results = unNormalizeData(outputs, dimensionParams[0]) 480 | return results 481 | } 482 | 483 | const makePredictions = async () => { 484 | setIsPredictionLoading(true) 485 | await new Promise((r) => setTimeout(r, 300)) 486 | try { 487 | const newSeries = rebootSeries() 488 | const xs = splitData([0.9, 1]) 489 | const timeseriesChunks = createTimeseriesDimensionForRNN(xs) 490 | const predictions = [] 491 | const flagsSerie = [] 492 | let _money = investing.start 493 | let _flag = {} 494 | let _value 495 | let _lastValue 496 | let _ys 497 | timeseriesChunks.forEach((chunk, index, array) => { 498 | const { dataNormalized, dimensionParams } = normalizeData(chunk.map((e) => e[1])) 499 | const value = chunk[chunk.length - 1][1][0] 500 | _lastValue = value 501 | const date = chunk[chunk.length - 1][0] 502 | const [ys] = gessLabels(dataNormalized, dimensionParams) 503 | if (_ys) { 504 | const predEvol = (ys - _ys) / _ys 505 | let flag = {} 506 | 507 | /** 508 | * First we must define what type of order 509 | */ 510 | if ( 511 | (strategy == 1 ? predEvol > 0 && ys > value : true) && 512 | (strategy == 2 ? predEvol > 0 : true) && 513 | (strategy == 3 ? ys > value : true) 514 | ) { 515 | flag.type = 'buy' 516 | } 517 | if ( 518 | (strategy == 1 ? predEvol < 0 && ys < value : true) && 519 | (strategy == 2 ? predEvol < 0 : true) && 520 | (strategy == 3 ? ys < value : true) 521 | ) { 522 | flag.type = 'sell' 523 | } 524 | 525 | /** 526 | * Second we make an action if the type of order changed from before 527 | */ 528 | if (_flag.type !== flag.type && flag.type) { 529 | if (!_value) { 530 | _value = value 531 | } 532 | let realEvolv2 = (value - _value) / _value 533 | 534 | if (_flag.type === 'buy') { 535 | _money = _money * (1 + realEvolv2) 536 | } 537 | if (_flag.type === 'sell') { 538 | _money = _money * (1 + -1 * realEvolv2) 539 | } 540 | _value = value 541 | flag.label = `Investing ${Math.round(_money)}$ at value ${value}` 542 | flagsSerie.push({ 543 | x: new Date(date).getTime(), 544 | title: flag.type, 545 | text: flag.label, 546 | color: flag.type === 'buy' ? 'green' : 'red' 547 | }) 548 | _flag = flag 549 | } 550 | } 551 | 552 | let datePredicted 553 | const nextChunk = array[index + 1] 554 | if (nextChunk) { 555 | const nextDate = nextChunk[nextChunk.length - 1][0] 556 | datePredicted = new Date(nextDate).getTime() 557 | } else { 558 | const lastDate = chunk[chunk.length - 1][0] 559 | datePredicted = new Date(lastDate).setDate(new Date(lastDate).getDate() + 1) 560 | } 561 | 562 | predictions.push([datePredicted, ys]) 563 | _ys = ys 564 | }) 565 | ;(function finishOffTheLastTrade() { 566 | let realEvolv2 = (_lastValue - _value) / _value 567 | 568 | if (_flag.type === 'buy') { 569 | _money = _money * (1 + realEvolv2) 570 | } 571 | if (_flag.type === 'sell') { 572 | _money = _money * (1 + -1 * realEvolv2) 573 | } 574 | })() 575 | setInvesting({ start: 1000, end: _money }) 576 | setSeries([ 577 | ...newSeries, 578 | { 579 | type: 'line', 580 | name: 'Predicted value', 581 | data: predictions 582 | }, 583 | { 584 | type: 'flags', 585 | data: flagsSerie, 586 | onSeries: 'dataseries', 587 | shape: 'circlepin', 588 | width: 18 589 | } 590 | ]) 591 | } catch (error) { 592 | throw error 593 | } finally { 594 | setIsPredictionLoading(false) 595 | } 596 | } 597 | 598 | const rebootSeries = () => { 599 | setInvesting({ start: 1000, end: null }) 600 | const serieIndex = series.findIndex((serie) => serie.name === 'Predicted value') 601 | let newSeries = series 602 | if (serieIndex !== -1) { 603 | newSeries.splice(serieIndex, 2) 604 | setSeries([...newSeries]) 605 | } 606 | return newSeries 607 | } 608 | 609 | const options = { 610 | rangeSelector: { 611 | selected: 4 612 | }, 613 | 614 | title: { 615 | text: `${graphTitle} stock market` 616 | }, 617 | 618 | tooltip: { 619 | style: { 620 | width: '200px' 621 | }, 622 | valueDecimals: 4, 623 | shared: true 624 | }, 625 | 626 | yAxis: { 627 | title: { 628 | text: 'stock value' 629 | } 630 | }, 631 | xAxis: { 632 | type: 'datetime' 633 | }, 634 | series 635 | } 636 | 637 | const options2 = { 638 | title: { 639 | text: 'Model training graph' 640 | }, 641 | 642 | subtitle: { 643 | text: 'Tensorflow.js models loss through training' 644 | }, 645 | 646 | yAxis: { 647 | title: { 648 | text: 'Loss' 649 | } 650 | }, 651 | 652 | xAxis: { 653 | title: { 654 | text: 'Epoch' 655 | }, 656 | min: 1, 657 | max: epochs 658 | }, 659 | 660 | series: [ 661 | { 662 | type: 'line', 663 | name: 'loss', 664 | data: modelLogs 665 | } 666 | ], 667 | 668 | responsive: { 669 | rules: [ 670 | { 671 | condition: { 672 | maxWidth: 500 673 | } 674 | } 675 | ] 676 | } 677 | } 678 | return ( 679 |
680 |

Welcome to Stock Market Predictions App with Tensorflow.js

681 |

Compile AI models with RNN Recurrent Neural Network of LSTM Long-Short Term Memory layers in the browser

682 | 683 | 703 | {formError &&

{formError}

} 704 | {series.length > 0 && ( 705 | <> 706 | 707 | 708 |
709 | 725 |
726 | {' '} 734 | 735 | When you click this button you must stay on this page ! Otherwise your computer will put all your browser's work in 736 | standby. 737 |
738 | It's around 4 minutes work just take a coffee, depends your device's power, don't do this with your smartphone ;) it's 739 | gonna compile batches of {recurrence} periods. 740 |
741 | Bigger are the batches, longer it's gonna take time, smoother would be the prediction line. 742 |
743 |
744 |
745 | 746 | Define the flag order strategy (you can change it anytime even when the tensorflow model is already compiled, just need to 747 | click Make predictions again) : 748 | 749 |
750 | { 758 | setStrategy(Number(event.target.value)) 759 | }} 760 | /> 761 | 764 |
765 | { 773 | setStrategy(Number(event.target.value)) 774 | }} 775 | /> 776 | 779 |
780 | { 788 | setStrategy(Number(event.target.value)) 789 | }} 790 | /> 791 | 795 |
796 | 804 |
805 | 810 | More details on Github 811 | 812 | {isModelTraining && ( 813 |

814 | Please stay on the page, and leave your computer do the job ;) 815 |

816 | )} 817 | {isPredictionLoading && ( 818 |

819 | Please stay on the page, predictions are loading ;) 820 |

821 | )} 822 | {modelLogs.length > 0 && ( 823 | <> 824 | {investing.end ? ( 825 |

{`You invested ${investing.start}$, you get off the train with ${Math.round(investing.end)}$`}

826 | ) : ( 827 |

{`You are investing ${investing.start}$, click on Make predictions button`}

828 | )} 829 | 830 | 831 | 832 | )} 833 |

The financial indicators used are the followings :

834 |
    835 |
  • Close value
  • 836 |
  • Open value
  • 837 |
  • Daily high value
  • 838 |
  • Daily low value
  • 839 | {/*
  • Daily volume
  • */} 840 |
  • EMA10 (Exponential Moving Average 10 periods)
  • 841 |
  • EMA20 (Exponential Moving Average 20 periods)
  • 842 |
  • EMA50 (Exponential Moving Average 50 periods)
  • 843 |
  • SMA10 (Simple Moving Average 10 periods)
  • 844 |
  • SMA20 (Simple Moving Average 20 periods)
  • 845 |
  • SMA50 (Simple Moving Average 50 periods)
  • 846 |
  • SMA100 (Simple Moving Average 100 periods)
  • 847 |
  • RSI7 (Relative Strength Index 7 periods)
  • 848 |
  • RSI14 (Relative Strength Index 14 periods)
  • 849 |
  • RSI28 (Relative Strength Index 28 periods)
  • 850 |
  • Stochastic7 (last 7 periods)
  • 851 |
  • Stochastic14 (last 14 periods)
  • 852 | {/*
  • Weekly seasonality
  • */} 853 |
854 |

855 | {`We use a (80%, 10%, 10%) periods split for : training, validation, 856 | and test set.`} 857 |

858 |

859 | {`For training and validation set compilation works with batch size of ${batchSize}, which means 1 batch includes ${batchSize} timeseries of ${recurrence} recurrences.`} 860 |

861 |
    862 |
  • Create and validate model button : use training and validation set (80% and 10%).
  • 863 |
  • 864 | {`Make predictions button : use test set (10%). Every day we 865 | predict the day after's value relative to the last sequence of ${timeserieSize} periods. (you may need to zoom on the graph)`} 866 |
  • 867 |
868 | {sampleData && ( 869 | /** 870 | * When you are developing and changing things 871 | * you must put this html in commentary to avoid trouble 872 | */ 873 | <> 874 |
875 |

Describe the whole data below

876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | {Object.entries(sampleData.sampleDimensionParams).map((e1, i1) => ( 887 | 888 | {Object.values(e1[1]).map((e2, i2) => ( 889 | 890 | ))} 891 | 892 | ))} 893 | 894 |
minmaxmeanstd
{e2}
895 |

Example data raw below

896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | {/* */} 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | {/* */} 917 | {/* */} 918 | 919 | 920 | 921 | {sampleData.sampleDataRaw.map((e1, i1) => ( 922 | 923 | {e1.map((e2, i2) => ( 924 | 925 | ))} 926 | 927 | ))} 928 | 929 |
Close valueOpen valueHighLowVolumeEMA10EMA20EMA50SMA10SMA20SMA50SMA100RSI7RSI14RSI28Stochastic7Stochastic14Weekly seasonality cosinusWeekly seasonality sinus
{e2}
930 | 931 | )} 932 |
933 | 934 | )} 935 |
936 | ) 937 | } 938 | 939 | export default hot(Main) 940 | --------------------------------------------------------------------------------