├── .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 | 
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 | 
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 |
684 |
685 |
692 |
693 |
694 |
701 |
702 |
703 | {formError &&
{formError}
}
704 | {series.length > 0 && (
705 | <>
706 |
707 |
708 |
709 |
710 | Define how many periods are needed for the neural network to detect recurrent patterns :
711 | {
716 | setRecurrence(isNaN(Number(event.target.value)) ? event.target.value : Number(event.target.value))
717 | }}
718 | />
719 | {typeof recurrence !== 'number' && Only use number }
720 | {typeof recurrence === 'number' && recurrence < 2 && (
721 | Minimum 2 recurrences in order to normalize inputs with mean and standard derivation
722 | )}
723 | {typeof recurrence === 'number' && recurrence > 32 && It may take a lot of time }
724 |
725 |
726 |
732 | {isModelTraining ? '1. Model is training' : '1. Create and validate model'}
733 | {' '}
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 |
762 | Default - The prediction next day is higher than the prediction today (the reverse for sell order)
763 |
764 |
765 |
{
773 | setStrategy(Number(event.target.value))
774 | }}
775 | />
776 |
777 | Classic - The prediction next day is higher than the real value today (the reverse for sell order)
778 |
779 |
780 |
{
788 | setStrategy(Number(event.target.value))
789 | }}
790 | />
791 |
792 | Secure - The prediction next day is higher than the prediction today & the prediction next day is higher than the real value
793 | today (the reverse for sell order)
794 |
795 |
796 |
802 | 2. Make predictions
803 |
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 | min
880 | max
881 | mean
882 | std
883 |
884 |
885 |
886 | {Object.entries(sampleData.sampleDimensionParams).map((e1, i1) => (
887 |
888 | {Object.values(e1[1]).map((e2, i2) => (
889 | {e2}
890 | ))}
891 |
892 | ))}
893 |
894 |
895 |
Example data raw below
896 |
897 |
898 |
899 | Close value
900 | Open value
901 | High
902 | Low
903 | {/* Volume */}
904 | EMA10
905 | EMA20
906 | EMA50
907 | SMA10
908 | SMA20
909 | SMA50
910 | SMA100
911 | RSI7
912 | RSI14
913 | RSI28
914 | Stochastic7
915 | Stochastic14
916 | {/* Weekly seasonality cosinus */}
917 | {/* Weekly seasonality sinus */}
918 |
919 |
920 |
921 | {sampleData.sampleDataRaw.map((e1, i1) => (
922 |
923 | {e1.map((e2, i2) => (
924 | {e2}
925 | ))}
926 |
927 | ))}
928 |
929 |
930 | >
931 | )}
932 |
933 | >
934 | )}
935 |
936 | )
937 | }
938 |
939 | export default hot(Main)
940 |
--------------------------------------------------------------------------------