How to create a stock portfolio app with ReactJS and Firebase?

The following tutorial is focused on the idea of how to create a simple stock portfolio application using ReactJS and Firebase database. The main functionalities will be to create your portfolio by adding new stocks or removing existing ones and track their performance. When the portfolio is ready the user will see a monitor page where based on the current price of the stock the profit/loss of that position will be calculated for each stock individually and also on the portfolio level. The last and very important feature will be the option to read daily news from different sources related to the companies in the portfolio. Sounds very exciting, right? 

stock portfolio app

stock portfolio app

Tech Stack 

As we have already understood we are going to use ReactJS to build this application which will help us to monitor our stock positions. We will not dive into details about the installation of ReactJS and will make the assumption that you know it or you will be able to find it in the documentation of ReactJS

ReactJS will be a great asset in our hands and will assist us with the conditional rendering and the state management. No doubt both will add additional value to the user experience. However, we are going to need a place where we can save some information. In our case we would like to save the tickers of the stocks, which we have in the portfolio and the price on which we have the position, so there is no need to enter them each time when we use the application. Therefore, we will use the Firebase platform which was developed by Google. More specifically we will employ their database service. The next section will be devoted to how to set up a Firebase database if you are not familiar with it.

Additionally, we are going to need a Stock API where to take the current price from. There are many API-s that can be used but we will mention only several of them which have high user ratings and provide many additional useful resources.

Firebase

For those of you who are familiar with the platform and know how to set up a database, feel free to skip this section and move to the next one. 

First of all you will need an account. When your account is ready you just have to go to https://console.firebase.google.com/u/0/  and start with the creation of a project. We will provide you with screenshots and comments of the important steps in the process. 

add project

Choose a name for your project. The name which we chose is Stock Portfolio but for the scope of our tutorial this name will not have any impact. On the next step you will be asked about Google Analytics. Keep it off for now, as we are not going to track anything. 

create a project

When your project is ready you will find many options in the menu on the left side. The one that we need is called Realtime Database. We are not going to create different users and authentication in this tutorial, so you can choose to start in test mode, so everyone can write and read in the next 30 days. If you want to have different users I strongly encourage you to read more about rules and authentication in Firebase. Indeed, this can be a topic of another tutorial.

database

This is the final step and after that you will see the endpoint of your database. Initially the database should be empty because we do not have any tables(collections). Cool, we are ready with the setup of the database and can move to the next part.

realtime database

Stock API

As we discussed earlier there are many API-s for stock quotes that can be used. We would recommend the following ones as they have very detailed documentation and high user ratings. 

  • Alpha Vantage
  • Polygon
  • Finnhub

For the purpose of the tutorial we will use Finnhub because it offers many free API calls per minute. In other words, how many quotes or other information can we request per minute from the service. Keep in mind that if you want to have an application with many API calls per minute you have to choose the premium plan. However, Finnhub offers 60 calls per minute which is totally fine for us to lay the foundation of an app and to use it to monitor our portfolio.

monitor portfolio

Structure

The main idea behind the architecture of our application is to split the main components in separate files, which will help us to write shorter components and to have separation of concerns. Of course, the code can be divided even more, but we will try to keep it simple and to help you to follow the logic. Also, it is always a good idea to stick to the well known “Don’t repeat yourself” principle, so we can export such code in separate files and just to import and reuse when needed. After the review of each component you will find a .css file which is used to add styles. As a beginning you can include the following code in index.css file. It will remove all default paddings and margins, apply the border-box model and will handle the fonts. 

body {
    background:white;
}
*,
*:before,
*:after {
    margin: 0px;
    padding: 0px;
    box-sizing: border-box;
    font-family: Arial, Helvetica, sans-serif;
    font-style: normal;
    font-size: 18px;
}

 

Okay, enough talk, let’s see the code. (You will find the full repository here)

components

As you see, we have several major components which are going to be reviewed in much more details in the next sections. Also, some useful resources in the utils folder which will help us to improve the code.

Utils

  • database

In the utils folder we have the constant variable of the database endpoint from Firebase. You can notice that we added /stocks at the end because this will be the name of the table(collection) which we use.

//The endpoint of the Firebase database.  Please, replace it with your endpoint
const DATABASE = 'stock-portfolio-45c87-default-rtdb.firebaseio.com/stocks';
 
export default DATABASE;

 

Later, in Firebase you will find a similar collection to this one. Of course, your entries ID-s will be different because Firebase automatically generates them.

stock

 

  • stockAPI

Here we can find two constants. One for the endpoint of the stocks from Finnhub and one for the token which is provided to us after registration in Finnhub. Keep in mind that your token should be different and you should replace it in the code.

//Finnhub API for stocks
export const STOCK_API = 'https://finnhub.io/api/v1/';
//Token provided by Finnhub after registration. Please, replace it with your personal token
export const TOKEN = 'c45s88aad3idgi81g04g';
 

 

  • stockFetcher

We will come back to this function later, because it will be difficult to understand the logic behind it now. Basically, this is a function which fetches the current price of the stock and updates the application state. As mentioned, we will discuss it later for a better understanding.

App

App component is the main component of our application. It keeps the information for the stocks and helps us move between Portfolio component and PortfolioMonitor component. 

import { useState } from 'react';
import Portfolio from './components/Portfolio/Portfolio';
import PortfolioMonitor from './components/PortfolioMonitor/PortfolioMonitor';
import './App.css';

function App() {
    //State of the stocks
    const [stocks, setStocks] = useState([]);
    //State of Portfolio or PortfolioMonitor component to be shown
    const [isPortfolioReady, setIsPortfolioReady] = useState(false);

    return (
        <div className='App'>
            <header>
                <h1>Welcome to Stock Portfolio</h1>
                {/* This is the logo of our application */}
                <div className='header-logo'>$</div>
            </header>
            <div>
                 {/* If user is ready with portfolio shows PortfolioMonitor */}
                {!isPortfolioReady ? (
                    <div className='portfolio-configuration'>
                        <Portfolio stocks={stocks} setStocks={setStocks} />
                        <div className='portfolio-button-continue-wrapper'>
                            <button
                                className='portfolio-button-continue'
                                onClick={() => setIsPortfolioReady(true)}
                            >
                                <span>Continue</span>
                            </button>
                        </div>
                    </div>
                ) : (
                    <div className='portfolio-monitor'>
                        <div className='portfolio-button-back-wrapper'>
                            <button
                                className='portfolio-button-back'
                                onClick={() => setIsPortfolioReady(false)}
                            >
                                <span>Go back to Portfolio</span>
                            </button>
                        </div>
                        <PortfolioMonitor
                            stocks={stocks}
                            setStocks={setStocks}
                        />
                    </div>
                )}
            </div>
        </div>
    );
}

export default App;

App.css

 
.App {
    min-height: calc(100vh - 76px);
}
 
header {
    height: 70px;
    background: #006e0c;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 15px;
}
 
header h1 {
    color: white;
    margin-right: 15px;
}
 
.header-logo {
    width: 60px;
    height: 60px;
    border-radius: 20px;
    background: white;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 35px;
    margin-right: 15px;
}
 
.tradingview-widget-copyright {
    display: none;
}
 
.portfolio-configuration,
.portfolio-monitor {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-wrap: wrap;
}
 
.portfolio-button-continue-wrapper,
.portfolio-button-back-wrapper {
    width: 100%;
    display: flex;
    justify-content: center;
}
 
.portfolio-button-continue,
.portfolio-button-back {
    width: 200px;
    height: 40px;
    border: none;
    border-radius: 20px;
    background: black;
    color: white;
    margin-top: 15px;
    margin-bottom: 15px;
}
 
.portfolio-button-continue:hover,
.portfolio-button-back:hover {
    cursor: pointer;
}
 
button {
    width: 200px;
    height: 40px;
    margin-top: 15px;
    border: none;
    border-radius: 20px;
    color: white;
}

 

 

Portfolio

import React, { useState, useEffect } from 'react';
import PortfolioForm from '../PortfolioForm/PortfolioForm';
//Importing the database endpoint as string to be used
import DATABASE from '../../utils/database';
import './Portfolio.css';

export default function Portfolio({ stocks, setStocks }) {
    const [inputVisibility, setInputVisibility] = useState(false);

    useEffect(() => {
        //GET request to the database to fetch the stock which are already in our portfolio
        const fetchData = async () => {
            try {
                const response = await fetch(`https://${DATABASE}.json`);
                const data = await response.json();

                //Validates that the database is not empty
                if (data) {
                    //If not empty modifies the data with fetched results and updates state
                    const dataModified = Object.keys(data).map((key) => ({
                        id: key,
                        ticker: data[key]['ticker'],
                        position: data[key]['position'],
                        quantity: data[key]['quantity'],
                        price: data[key]['price'],
                    }));
                    setStocks(dataModified);
                }
            } catch (error) {
                /*The option how to handle the error is totally up to you. 
                Ideally, you can send notification to the user */
                console.log(error);
            }
        };

        fetchData();
    }, [setStocks]);

    //Function that removes the stock from portfolio
    const handleRemoveStock = async (stockId) => {
        try {
            //DELETE request to the database to delete specific stock by id
            await fetch(`https://${DATABASE}/${stockId}.json`, {
                method: 'DELETE',
                'Content-Type': 'application/json',
            });

            //Updates state by removing this stock
            setStocks((stocks) => stocks.filter((s) => s.id !== stockId));
        } catch (error) {
            /*The option how to handle the error is totally up to you. 
            Ideally, you can send notification to the user */
            console.log(error);
        }
    };

    return (
        <div className='portfolio-page'>
            <div className='portfolio-main-row-wrapper'>
                <div className='portfolio-main-row'>Ticker</div>
                <div className='portfolio-main-row'>Position</div>
                <div className='portfolio-main-row'>Quantity</div>
                <div className='portfolio-main-row'>Price</div>
            </div>
            {/* For each stock in database renders a row with info */}
            {stocks.map((s) => {
                return (
                    <div className='portfolio-row-wrapper' key={s.id}>
                        <div className='portfolio-row'>{s.ticker}</div>
                        <div className='portfolio-row'>{s.position}</div>
                        <div className='portfolio-row'>{s.quantity}</div>
                        <div className='portfolio-row'>{s.price}</div>
                        <button
                            className='remove-stock-button'
                            onClick={() => handleRemoveStock(s.id)}
                        >
                            <span>-</span>
                        </button>
                    </div>
                );
            })}
            {/* Form to add new stock to the portfolio */}
            {inputVisibility ? (
                <PortfolioForm
                    setStocks={setStocks}
                    setInputVisibility={setInputVisibility}
                />
            ) : null}
            <button
                className='add-more-button'
                onClick={() => setInputVisibility(!inputVisibility)}
            >
                <span>ADD NEW STOCK</span>
            </button>
        </div>
    );
}

 

This is the component which manages the user portfolio. The user can add a new stock by entering the ticker, position (BUY or SELL), quantity and the price of the position at the time when it was bought or sold. Aslo, by clicking on the right red circle the user can remove already existing stock. Initially, in the useEffect hook the Portfolio component communicates with the database to see if we have already entered some tickers and to update the state which we saw earlier in the App component. Expectedly, the first time when you visit this page your portfolio should be empty. Аfterwards everytime when you visit your portfolio page it will keep the previous positions which were entered.

Portfolio.css

 
.portfolio-page {
    margin: auto;
    padding: 15px;
    border: 3px solid #006e0c;
    border-radius: 20px;
    background-image: url('https://static.vecteezy.com/system/resources/previews/001/220/903/original/stock-market-graph-design-on-white-background-vector.jpg');
    background-position: right;
    background-repeat: no-repeat;
    background-size: cover;
}
 
.portfolio-main-row-wrapper,
.portfolio-row-wrapper {
    display: flex;
    align-items: center;
    width: 100%;
}
 
.portfolio-main-row,
.portfolio-row {
    width: 140px;
    height: 40px;
    display: flex;
    align-items: center;
    font-weight: bold;
}
 
.portfolio-main-row,
.portfolio-row {
    border-bottom: 1px solid #006e0c;
}
 
.remove-stock-button {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    border: none;
    /* margin shorthand: top: 0px, right: 0px, bottom: 0px, left: 5px */
    margin: 0px 0px 0px 5px;
}
 
.remove-stock-button span {
    font-size: 20px;
    color: black;
}
 
.remove-stock-button {
    background: red;
}
 
.remove-stock-button:hover {
    cursor: pointer;
    border: 2px solid black;
}
 
.add-more-button {
    background: #006e0c;
}
 
.add-more-button:hover {
    cursor: pointer;
}
 

PortfolioForm

PortfolioForm is a nested component in Portfolio that has the responsibility to add new additions to our portfolio. For those of you who are not familiar with the specification of Controlled Components in ReactJS it will be very helpful to read the documentation in order to understand why we keep the values of inputs in the state.  

portfolio form

Let’s add a new stock to see how it works. By clicking on the ADD NEW STOCK button we set the inputVisibility state in the Portfolio component to true and the component will be rerendered. The condition for inputVisibility to be true is met and PortfolioForm is visible for the user.

{inputVisibility ? (
                <PortfolioForm
                    setStocks={setStocks}
                    setInputVisibility={setInputVisibility}
                />
            ) : null}

This example is for the popular company Apple which ticker is AAPL and we can enter that we have BUY/LONG position for 10 stocks at a price of 128 dollars. Confirming the parameters by clicking on the green icon with the symbol of + and the stock will be added to the portfolio.

managing portfolio

Managing our portfolio from the app also adds or removes the info from the database. Therefore, next time when we visit the app our portfolio will be exactly the same as we left it because it will fetch the info from the database. Below is the state of the database according to the position in our application.

position

Here is the PortfolioForm component code:

import React, { useState } from 'react';
import DATABASE from '../../utils/database';
import './PortfolioForm.css';

//Initial state of our form
const INITIAL_STATE = {
    ticker: '',
    position: 'BUY',
    quantity: 10,
    price: 50,
};

export default function PortfolioForm({ setStocks, setInputVisibility }) {
    //This is the initial state of our inputs
    const [formValues, setFormValues] = useState(INITIAL_STATE);

    //Function that handles the inputs and their new values
    const handleChange = (event) => {
        setFormValues((formValues) => ({
            ...formValues,
            [event.target.name]: event.target.value,
        }));
    };

    //Function that handles the new stock additions to our portfolio
    const handleSubmitNewStock = async (e) => {
        //Prevents the default behavior of the event to refresh the page
        e.preventDefault();
        try {
            //Basic validation if user entered a ticker, price and quantity above 0
            if (
                formValues.ticker &&
                formValues.price > 0 &&
                formValues.quantity > 0
            ) {
                const newStock = {
                    ticker: formValues.ticker,
                    position: formValues.position,
                    quantity: formValues.quantity,
                    price: formValues.price,
                };

                //POST request to the database to add a new stock
                const response = await fetch(`https://${DATABASE}.json`, {
                    method: 'POST',
                    'Content-Type': 'application/json',
                    body: JSON.stringify(newStock),
                });

                const data = await response.json();

                //Validates the stock is saved
                if (data.name) {
                    //Updates state with the new stock
                    setStocks((stocks) => [
                        ...stocks,
                        { id: data.name, ...newStock },
                    ]);
                    setFormValues(INITIAL_STATE);
                    setInputVisibility(false);
                }
            }
        } catch (error) {
            /*The option how to handle the error is totally up to you. 
            Ideally, you can send notification to the user */
            console.log(error);
        }
    };

    return (
        <div className='add-more-wrapper'>
            <form className='add-more-form'>
                <div className='add-more-row'>
                    <input
                        type='text'
                        name='ticker'
                        value={formValues.ticker}
                        onChange={handleChange}
                    />
                </div>
                <div className='add-more-row'>
                    <select
                        name='position'
                        onChange={handleChange}
                        value={formValues.position}
                    >
                        <option value='BUY'>BUY</option>
                        <option value='SELL'>SELL</option>
                    </select>
                </div>
                <div className='add-more-row'>
                    <input
                        type='number'
                        name='quantity'
                        min='0'
                        value={formValues.quantity}
                        onChange={handleChange}
                    />
                </div>
                <div className='add-more-row'>
                    <input
                        type='number'
                        name='price'
                        min='0'
                        value={formValues.price}
                        onChange={handleChange}
                    />
                </div>
                <button
                    className='add-new-stock-button'
                    onClick={handleSubmitNewStock}
                >
                    <span>+</span>
                </button>
            </form>
        </div>
    );
}

 

PortfolioForm.css

The style of the PortfolioForm has several similarities to the Portfolio and it will be a good idea to combine them under common classes, so you can reduce the css code. However, we preferred to keep them separate in the tutorial, so you can easily distinguish between different components and their html elements.

.add-more-wrapper {
    margin: 20px 0px 5px 0px;
    display: flex;
    align-items: center;
    width: 100%;
}

.add-more-form {
    display: flex;
    justify-content: center;
    align-items: center;
}

.add-more-row {
    width: 140px;
    height: 40px;
    display: flex;
    align-items: center;
    font-weight: bold;
}

.add-more-row input,
select {
    width: 100%;
    height: 100%;
}

textarea:focus,
input:focus,
select:focus {
    outline: none;
}

.add-new-stock-button {
    width: 30px;
    height: 30px;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    border: none;
    margin: 0px 0px 0px 5px;
}

.add-new-stock-button span {
    font-size: 20px;
    color: black;
}

.add-new-stock-button:hover {
    cursor: pointer;
    border: 2px solid black;
}

.add-new-stock-button {
    background: greenyellow;
}

 

stockFetcher (utils)

It is the right moment to go back to this function from the utils folder which we skipped. It receives as parameters the current state of the stocks array – stocks, the function which updates the state – setStocks and profitLossCalculator function which calculates the profit or loss of the position. After the calculation the function calls setStock to update the state with current prices and profit or loss amount. By exporting this function we can reuse it in different components if needed. Of course, to take it out to their first similar parent component is another method that can be used to avoid repeating the code. 

import { STOCK_API } from './stockAPI';
import { TOKEN } from './stockAPI';

//Function which fetches the current prices and updates our state with current prices and profit/loss
const stockFetcher = (stocks, setStocks, profitLossCalculator) => {
    stocks.forEach(async (s) => {
        try {
            const stockName = s.ticker.replace('', '');
            const response = await fetch(
                `${STOCK_API}/quote?symbol=${stockName}&token=${TOKEN}`
            );
            const data = await response.json();

            const profitLoss = profitLossCalculator(
                s.price,
                data.c,
                s.position,
                s.quantity
            );

            const stockWithPrice = {
                ...s,
                currentPrice: data.c.toFixed(2),
                profitLoss,
            };

            const indexOfStock = stocks.indexOf(s);
            setStocks((stocks) => [
                ...stocks.slice(0, indexOfStock),
                stockWithPrice,
                ...stocks.slice(indexOfStock + 1),
            ]);
        } catch (error) {
            /*The option how to handle the error is totally up to you. 
                Ideally, you can send notification to the user */
            console.log(error);
        }
    });
};

export default stockFetcher;

 

PortfolioMonitor

When the user is ready with its portfolio the Continue button should be clicked and the PortfolioMonitor component will be rendered. Here, we also see our positions and information about them but this time we also have the current price, current profit or loss and news option.

portfolio monitor

Just because we use the useEffect hook the stockFetcher is called after the initial rendering and our component receives updated state with prices and profit/loss amount. After the first rendering, the user can update this info by clicking on the Update prices button which will complete the same action. This was the main reason to export stockFetcher to avoid repeating the code.

    const fetchPrices = () => {
        //Fetches prices and updates the state with current prices and profit or loss for the position
        stockFetcher(stocks, setStocks, profitLossCalculator);
    };

As you can see, the total profit/loss position of the portfolio is also visible due to the profitLossTotalCalculator function. Simple operation which iterates over each of the stocks and adds its profit or loss amount to a total amount.

import React, { useEffect, useState } from 'react';
import { ReactComponent as NewsIcon } from '../assets/news.svg';
import StockNews from '../StockNews/StockNews';
import { STOCK_API } from '../../utils/stockAPI';
import { TOKEN } from '../../utils/stockAPI';
import stockFetcher from '../../utils/stockFetcher';
import './PortfolioMonitor.css';

export default function PortfolioMonitor({ stocks, setStocks }) {
    //State of stock news
    const [stockNews, setStocksNews] = useState({});
    //State for news panel
    const [showNews, setShowNews] = useState(false);

    useEffect(() => {
        //Fetches prices and updates the state with current prices and profit or loss for the position
        stockFetcher(stocks, setStocks, profitLossCalculator);
    }, []);

    //Calculates the profit or loss for a single position
    const profitLossCalculator = (price, currentPrice, position, quantity) => {
        let profitLoss = 0;

        if (currentPrice) {
            if (position === 'BUY') {
                profitLoss = (currentPrice - price) * quantity;
            } else {
                profitLoss = (price - currentPrice) * quantity;
            }
        }

        return profitLoss.toFixed(2);
    };

    //Calculates the profit or loss for the whole portfolio
    const profitLossTotalCalculator = (stocks) => {
        let profitLossTotal = 0;

        stocks.forEach((s) => {
            if (!isNaN(Number(s.profitLoss))) {
                profitLossTotal += Number(s.profitLoss);
            }
        });

        return profitLossTotal.toFixed(2);
    };

    const fetchPrices = () => {
        //Fetches prices and updates the state with current prices and profit or loss for the position
        stockFetcher(stocks, setStocks, profitLossCalculator);
    };

    //Fetches daily news for a single stock ticker
    const handleNews = async (stock) => {
        if (stockNews.hasOwnProperty(stock)) {
            setShowNews(false);
            setStocksNews({});
        } else {
            try {
                const stockName = stock.replace('', '');
                //Finds the year, month and day as numbers, so we can use them in the fetch request
                const date = new Date().toISOString().split('T')[0];
                const dateArray = date.split('-');
                const year = dateArray[0];
                const month = dateArray[1];
                const day = dateArray[2];

                const response = await fetch(
                    `${STOCK_API}company-news?symbol=${stockName}&from=${year}-${month}-${day}&to=${year}-${month}-${day}&token=${TOKEN}`
                );
                const data = await response.json();

                setShowNews(true);

                setStocksNews({
                    [stockName]: [data][0],
                });
            } catch (error) {
                /*The option how to handle the error is totally up to you. 
                Ideally, you can send notification to the user */
                console.log(error);
            }
        }
    };

    return (
        <div className='monitor-page'>
            <div className='monitor-main-row-wrapper'>
                <div className='monitor-main-row'>Ticker</div>
                <div className='monitor-main-row'>Position</div>
                <div className='monitor-main-row'>Quantity</div>
                <div className='monitor-main-row'>Price</div>
                <div className='monitor-main-row'>Current Price</div>
                <div className='monitor-main-row'>Profit/Loss</div>
                <div className='monitor-main-row'>News</div>
            </div>
            {/* For each stock in a portfolio prints a row with info */}
            {stocks.map((s) => {
                return (
                    <div key={s.id}>
                        <div className='monitor-row-wrapper'>
                            <div className='monitor-row'>{s.ticker}</div>
                            <div className='monitor-row'>{s.position}</div>
                            <div className='monitor-row'>{s.quantity}</div>
                            <div className='monitor-row'>{s.price}</div>
                            <div className='monitor-row'>
                                {s.currentPrice ? s.currentPrice : null}
                            </div>
                            <div
                                className={`${
                                    s.profitLoss > 0 ? 'profit-row' : 'loss-row'
                                } monitor-row`}
                            >
                                {s.profitLoss ? s.profitLoss : null}
                            </div>
                            <div className='monitor-row'>
                                <NewsIcon
                                    className='monitor-news-icon'
                                    onClick={() => handleNews(s.ticker)}
                                />
                            </div>
                        </div>
                    </div>
                );
            })}
            <div className='monitor-summary-row-wrapper'>
                <div className='monitor-summary-row'>Total:</div>
                <div
                    className={`${
                        profitLossTotalCalculator(stocks)
                            ? 'profit-row'
                            : 'loss-row'
                    } monitor-summary-row`}
                >
                    {profitLossTotalCalculator(stocks)}
                </div>
            </div>
            <button className='monitor-fetch-prices' onClick={fetchPrices}>
                <span>Update prices</span>
            </button>
            {showNews && Object.keys(stockNews)[0] ? (
                <StockNews stockNews={stockNews} />
            ) : null}
        </div>
    );
}

 

PortfolioMonitor.css

.monitor-page {
    width: 1016px;
    padding: 15px;
    margin-bottom: 15px;
    border: 3px solid #006e0c;
    border-radius: 20px;
    background-image: url('https://static.vecteezy.com/system/resources/previews/001/220/903/original/stock-market-graph-design-on-white-background-vector.jpg');
    background-position: right;
    background-repeat: no-repeat;
    background-size: cover;
}

.monitor-main-row-wrapper,
.monitor-row-wrapper {
    display: flex;
    align-items: center;
    width: 100%;
}

.monitor-summary-row-wrapper {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 840px;
}

.monitor-main-row,
.monitor-row,
.monitor-summary-row {
    width: 140px;
    height: 40px;
    display: flex;
    align-items: center;
    font-weight: bold;
}

.monitor-main-row,
.monitor-row {
    border-bottom: 1px solid #006e0c;
}

.monitor-news-icon {
    width: 30px;
    height: 30px;
}

.monitor-news-icon:hover {
    width: 32px;
    height: 32px;
    cursor: pointer;
}

.profit-row {
    color: #006e0c;
}

.loss-row {
    color: red;
}

.monitor-fetch-prices {
    background: black;
}

.monitor-fetch-prices:hover {
    cursor: pointer;
}

 

StockNews 

The last feature of our stock portfolio app will be to fetch the news for a stock. To be informed about recent events for a specific company or the market in general is an important part of portfolio monitoring and management. No doubt, it will be very beneficial for the user if our app has such a feature. Fortunately, Finnhub provides such endpoint and we can directly use it by replacing the date and the stock ticker.

company news

In PortfolioMonitor each row with a single stock has a newspaper icon at the end. The attached onClick function on the icon will trigger the fetching of the news and will render StockNews if it was not visible until that moment. Otherwise, it will hide the news panel.

onClick

The logic behind the StockNews component is simple.It takes the stockNews object which is  passed by its parent as props and renders each of the news. Also, provides the user with the option to click and read the news directly from the source. For example it can be Yahoo Finance or SeekingAlpha.

daily news

import React from 'react';
import './StockNews.css';

export default function StockNews({ stockNews }) {
    //Redirects to the original source
    const handleRedirect = (url) => {
        window.open(url, '_blank');
    };

    return (
        <div className='monitor-news-wrapper'>
            <h2>Daily news for {Object.keys(stockNews)[0]}</h2>
            {Object.values(stockNews)[0].length ? (
                <div className='monitor-news'>
                    {Object.values(stockNews)[0].map((n, i) => {
                        return (
                            <div
                                className='monitor-single-news-wrapper'
                                key={`${n.id}${i}`}
                            >
                                <div className='monitor-source-wrapper'>
                                    {n.image ? (
                                        <img
                                            className='stock-news-image'
                                            src={n.image}
                                            alt={n.id}
                                        />
                                    ) : (
                                        <div className='header-logo'>$</div>
                                    )}
                                    <p>Source: {n.source}</p>
                                </div>
                                <h3>{n.summary}</h3>
                                <button
                                    className='source-redirect'
                                    onClick={() => handleRedirect(n.url)}
                                >
                                    <span>Read from source</span>
                                </button>
                            </div>
                        );
                    })}
                </div>
            ) : (
                <div className='monitor-news-empty'>
                    <p>No news today</p>
                </div>
            )}
        </div>
    );
}

 

StockNews.css

.stock-news-image {
    width: 60px;
    height: 60px;
    border-radius: 20px;
    margin-right: 15px;
}

.source-redirect {
    background: #006e0c;
}

.source-redirect:hover {
    cursor: pointer;
}

.monitor-news-wrapper {
    margin: 25px 0px 25px 0;
}

.monitor-news,
.monitor-news-empty {
    margin-top: 25px;
}

.monitor-single-news-wrapper {
    margin-bottom: 30px;
    padding: 15px;
    border: 1px solid #006e0c;
    border-radius: 20px;
}

.monitor-source-wrapper {
    display: flex;
    align-items: center;
    width: 100%;
    margin-bottom: 15px;
}

 

Bonus. Widget by TradingView

TradingView is a great platform for charts, news and analysis tools. It also provides several amazing widgets which can be directly added to your html code. You can find them at https://www.tradingview.com/widget/

widget

Ticker Tape Widget will be a perfect choice for us if we want to monitor some benchmarks such as S&P 500 or NASDAQ and to follow the general market. We can also add some big companies just to follow their daily results even if they are not part of our portfolio but we know their performance is crucial for the market. However, we can add everything which we want to be monitored and the quotes will be directly updated by TradingView. Below is an example of code which you can add directly to your index.html file in the public folder.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="theme-color" content="#000000" />
        <meta
            name="description"
            content="Web site created using create-react-app"
        />
        <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
        <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
        <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
        <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
        <title>React App</title>
    </head>
    <body>
        <!-- TradingView Widget BEGIN -->
        <div class="tradingview-widget-container">
            <div class="tradingview-widget-container__widget"></div>
            <div class="tradingview-widget-copyright">
                <a
                    href="https://www.tradingview.com"
                    rel="noopener"
                    target="_blank"
                    ><span class="blue-text">Ticker Tape</span></a
                >
                by TradingView
            </div>
            <script
                type="text/javascript"
                src="https://s3.tradingview.com/external-embedding/embed-widget-ticker-tape.js"
                async
            >
                  {
                  "symbols": [
                    {
                      "proName": "FOREXCOM:SPXUSD",
                      "title": "S&P 500"
                    },
                    {
                      "proName": "FOREXCOM:NSXUSD",
                      "title": "Nasdaq 100"
                    },
                    {
                      "description": "Gold",
                      "proName": "AMEX:GLD"
                    },
                    {
                      "description": "Treasury",
                      "proName": "NASDAQ:TLT"
                    },
                    {
                      "description": "Tesla",
                      "proName": "NASDAQ:TSLA"
                    },
                    {
                      "description": "Apple",
                      "proName": "NASDAQ:AAPL"
                    }
                  ],
                  "showSymbolLogo": true,
                  "colorTheme": "light",
                  "isTransparent": true,
                  "displayMode": "adaptive",
                  "locale": "en"
                }
            </script>
        </div>
        <!-- TradingView Widget END -->
        <div id="root"></div>
    </body>
</html>

In conclusion

We made a small application to monitor stock positions performance and the related news.  As a database we used Firebase database and for the stock quotes and news we used Finnhub Stock API. ReactJS was the framework which helped us to create different components and to have local state using useState. If you are interested, you can add many new features such as economic news calendar, fundamental analysis or IPO calendar, as this data is available also on Finnhub.

 

Repository: https://github.com/valentinvachev/Stocks-App
Find more useful tutorials at Motion Software Tech Talks.

 

Valentin is a software developer currently working with MERN stack but eager to learn new technologies. In his spare time he is interested in finance, sports and healthy food.

x

Motion Software starts operating under the name of its parent company Exadel, taking effect on July 15th.

More Info

Motion Software uses cookies to improve site functionality, provide you with a better browsing experience, and to enable our partners to advertise to you. Detailed information on the use of cookies on this Site, and how you can decline them, is provided in our Cookie Policy Learn more about cookies, Opens in new tab. By using this Site or clicking on OK, you consent to the use of cookies.

OK