madnight/wallstreet

View on GitHub
index.html

Summary

Maintainability
Test Coverage
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Stock Data</title>

        <script src="https://code.highcharts.com/stock/highstock.js"></script>
        <script src="https://code.highcharts.com/stock/modules/exporting.js"></script>
        <script src="https://code.highcharts.com/stock/indicators/indicators-all.js"></script>
        <script src="https://code.highcharts.com/stock/modules/accessibility.js"></script>

        <style>
            body {
                font-family: "Arial", sans-serif;
                background: #f4f7f6;
                margin: 0;
                padding: 20px;
                color: #333;
            }

            .high-pe {
                color: red;
            }

            .low-pe {
                color: green;
            }

            h2 {
                color: #2c3e50;
            }

            table {
                width: 100%;
                border-collapse: collapse;
                margin: 25px 0;
                font-size: 0.9em;
                min-width: 400px;
                box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
            }

            thead tr {
                background-color: #009879;
                color: #ffffff;
                text-align: left;
            }

            th,
            td {
                padding: 8px 10px;
            }

            tbody tr:nth-of-type(even) {
                background-color: #1f1f1f;
            }

            tbody tr:hover {
                background-color: #111111;
            }

            tbody tr.active-row {
                font-weight: bold;
                color: #009879;
            }
            .spinner {
                border: 5px solid #f3f3f3;
                border-top: 5px solid #3498db;
                border-radius: 50%;
                width: 50px;
                height: 50px;
                animation: spin 2s linear infinite;
                margin: 50px auto;
            }

            @keyframes spin {
                0% {
                    transform: rotate(0deg);
                }
                100% {
                    transform: rotate(360deg);
                }
            }
        </style>
    </head>
    <body>
        <!-- <div class="main-wrapper"> -->
        <!--     <div class="selectors-container"> -->
        <!--         <div class="col"> -->
        <!--             <label for="overlays">Overlays:</label> -->
        <!--             <select class="left-select" id="overlays"> -->
        <!--                 <option value="abands">Acceleration Bands</option> -->
        <!--                 <option value="bb" selected="selected"> -->
        <!--                     Bollinger Bands -->
        <!--                 </option> -->
        <!--                 <option value="dema"> -->
        <!--                     DEMA (Double Exponential Moving Average) -->
        <!--                 </option> -->
        <!--                 <option value="ema"> -->
        <!--                     EMA (Exponential Moving Average) -->
        <!--                 </option> -->
        <!--                 <option value="ikh">Ichimoku Kinko Hyo</option> -->
        <!--                 <option value="keltnerchannels"> -->
        <!--                     Keltner Channels -->
        <!--                 </option> -->
        <!--                 <option value="linearRegression"> -->
        <!--                     Linear Regression -->
        <!--                 </option> -->
        <!--                 <option value="pivotpoints">Pivot Points</option> -->
        <!--                 <option value="pc">Price Channel</option> -->
        <!--                 <option value="priceenvelopes">Price Envelopes</option> -->
        <!--                 <option value="psar">PSAR (Parabolic SAR)</option> -->
        <!--                 <option value="sma">SMA (Simple Moving Average)</option> -->
        <!--                 <option value="supertrend">Super Trend</option> -->
        <!--                 <option value="tema"> -->
        <!--                     TEMA (Triple Exponential Moving Average) -->
        <!--                 </option> -->
        <!--                 <option value="vbp">VbP (Volume by Price)</option> -->
        <!--                 <option value="vwap"> -->
        <!--                     WMA (Weighted Moving Average) -->
        <!--                 </option> -->
        <!--                 <option value="wma"> -->
        <!--                     VWAP (Volume Weighted Average Price) -->
        <!--                 </option> -->
        <!--                 <option value="zigzag">Zig Zag</option> -->
        <!--             </select> -->
        <!--         </div> -->
        <!--         <div class="col"> -->
        <!--             <label for="oscillators">Oscillators:</label> -->
        <!--             <select class="right-select" id="oscillators"> -->
        <!--                 <option value="apo">Absolute price indicator</option> -->
        <!--                 <option value="ad"> -->
        <!--                     A/D (Accumulation/Distribution) -->
        <!--                 </option> -->
        <!--                 <option value="aroon">Aroon</option> -->
        <!--                 <option value="aroonoscillator"> -->
        <!--                     Aroon oscillator -->
        <!--                 </option> -->
        <!--                 <option value="atr">ATR (Average True Range)</option> -->
        <!--                 <option value="ao">Awesome oscillator</option> -->
        <!--                 <option value="cci"> -->
        <!--                     CCI (Commodity Channel Index) -->
        <!--                 </option> -->
        <!--                 <option value="chaikin">Chaikin</option> -->
        <!--                 <option value="cmf">CMF (Chaikin Money Flow)</option> -->
        <!--                 <option value="disparityindex">Disparity Index</option> -->
        <!--                 <option value="cmo"> -->
        <!--                     CMO (Chande Momentum Oscillator) -->
        <!--                 </option> -->
        <!--                 <option value="dmi"> -->
        <!--                     DMI (Directional Movement Index) -->
        <!--                 </option> -->
        <!--                 <option value="dpo">Detrended price</option> -->
        <!--                 <option value="linearRegressionAngle"> -->
        <!--                     Linear Regression Angle -->
        <!--                 </option> -->
        <!--                 <option value="linearRegressionIntercept"> -->
        <!--                     Linear Regression Intercept -->
        <!--                 </option> -->
        <!--                 <option value="linearRegressionSlope"> -->
        <!--                     Linear Regression Slope -->
        <!--                 </option> -->
        <!--                 <option value="klinger">Klinger Oscillator</option> -->
        <!--                 <option value="macd"> -->
        <!--                     MACD (Moving Average Convergence Divergence) -->
        <!--                 </option> -->
        <!--                 <option value="mfi">MFI (Money Flow Index)</option> -->
        <!--                 <option value="momentum">Momentum</option> -->
        <!--                 <option value="natr"> -->
        <!--                     NATR (Normalized Average True Range) -->
        <!--                 </option> -->
        <!--                 <option value="obv">OBV (On-Balance Volume)</option> -->
        <!--                 <option value="ppo">Percentage Price oscillator</option> -->
        <!--                 <option value="roc">RoC (Rate of Change)</option> -->
        <!--                 <option value="rsi" selected="selected"> -->
        <!--                     RSI (Relative Strength Index) -->
        <!--                 </option> -->
        <!--                 <option value="slowstochastic">Slow Stochastic</option> -->
        <!--                 <option value="stochastic">Stochastic</option> -->
        <!--                 <option value="trix">TRIX</option> -->
        <!--                 <option value="williamsr">Williams %R</option> -->
        <!--             </select> -->
        <!--         </div> -->
        <!--     </div> -->
        <!--     <div id="container"></div> -->
        <!-- </div> -->

        <h2>NDX Nasdaq-100</h2>
        <table id="stockTable">
            <thead>
                <tr>
                    <th>No.</th>
                    <th>Ticker</th>
                    <th>Name</th>
                    <th>Market<br />Cap</th>
                    <th>P/E</th>
                    <th>P/FCF</th>
                    <th>P/S</th>
                    <th>P/B</th>
                    <th>PEG</th>
                    <th>Beta</th>
                    <th>
                        Analyst <br />
                        Recom.
                    </th>
                    <th>RSI</th>
                    <th>CAGR <br />Perf5y</th>
                    <th>CAGR <br />Perf10y</th>
                    <th>CAGR <br />Perf15y</th>
                    <th>CAGR <br />Sales5y</th>
                    <th>CAGR <br />EPS5y</th>
                    <th>Short %</th>
                    <th>Employees</th>
                    <th>Weight</th>
                </tr>
            </thead>
            <tbody></tbody>
        </table>

        <div class="spinner" id="loadingSpinner"></div>
        <div id="averagePe"></div>

        <script>
            function formatNumber(num) {
                if (num >= 1e9) {
                    return Math.round(num / 1e9) + "B";
                }
                if (num >= 1e3) {
                    return Math.round(num / 1e3) + "k";
                }
                return num.toString();
            }

            function roundAndConvertUnit(str) {
                const numericPart = parseFloat(str.slice(0, -1));
                const unit = str.slice(-1).toUpperCase();
                let roundedNumber;
                let newUnit = unit;
                if (unit === "B" && numericPart >= 1000) {
                    roundedNumber = (numericPart / 1000).toFixed(1);
                    newUnit = "T";
                } else if (unit === "B") {
                    roundedNumber = Math.round(numericPart);
                } else if (unit === "T") {
                    roundedNumber = numericPart.toFixed(1);
                } else {
                    roundedNumber = Math.round(numericPart);
                }
                return `${roundedNumber}${newUnit}`;
            }

            function classifyCellByValue(type, content, cell) {
                const value = parseFloat(content);

                let thresholdHigh, thresholdLow, classHigh, classLow;

                switch (type) {
                    case "pe":
                        thresholdHigh = 40;
                        thresholdLow = 10;
                        break;
                    case "Recom":
                        thresholdHigh = 2.5;
                        thresholdLow = 1.5;
                        break;
                    case "rsi":
                        thresholdHigh = 70;
                        thresholdLow = 30;
                        break;
                    default:
                        return;
                }

                if (value > thresholdHigh) {
                    cell.classList.add("high-pe");
                } else if (value < thresholdLow) {
                    cell.classList.add("low-pe");
                }
            }

            function addCell(content, type = "") {
                const table = document.getElementById("stockTable");
                const lastRow = table.rows[table.rows.length - 1];
                const cell = lastRow.insertCell(lastRow.cells.length);

                if (type === "beta" && parseFloat(content) > 2) {
                    const span = document.createElement("span");
                    span.style.color = "red";
                    span.textContent = content;
                    cell.appendChild(span);
                } else {
                    cell.textContent = content;
                }

                classifyCellByValue(type, content, cell);
            }

            function checkNaN(value, formatter = (val) => val, suffix = "") {
                if (isNaN(value)) {
                    return "-";
                } else {
                    return formatter(value) + suffix;
                }
            }

            let weightedPeSum = 0;
            let totalWeight = 0;
            let weightedPeValues = [];

            function addCells(stock, index) {
                const tableBody = document
                    .getElementById("stockTable")
                    .getElementsByTagName("tbody")[0];
                const row = tableBody.insertRow();
                const keys = [
                    "symbol",
                    "short_name",
                    "market_cap",
                    "price_earnings",
                    "price_free_cash_flow",
                    "price_sales",
                    "price_book",
                    "peg",
                    "beta",
                    "recommendation",
                    "rsi",
                    "cagr_5y",
                    "cagr_10y",
                    "cagr_15y",
                    "sales_5y",
                    "eps_5y",
                    "short",
                    "employees",
                    "weight",
                ];
                const formatters = {
                    symbol: (val) => val,
                    shortName: (val) => val,
                    market_cap: (val) => formatLargeNumber(val),
                    price_earnings: (val) => checkNaN(val, Math.round),
                    price_book: (val) => checkNaN(val, Math.round),
                    "P/FCF": (val) => checkNaN(val, Math.round),
                    "P/S": (val) => checkNaN(val, Math.round),
                    priceToBook: (val) => checkNaN(val, Math.round),
                    pegRatio: (val) => checkNaN(val, (val) => val.toFixed(1)),
                    beta: (val) => checkNaN(val, (val) => val.toFixed(1)),
                    Recom: (val) => checkNaN(val, (val) => val),
                    rsi: (val) => checkNaN(val, Math.round),
                    cagr_5y: (val) => (val ? val.toFixed(1) + "%" : "-"),
                    cagr_10y: (val) => (val ? val.toFixed(1) + "%" : "-"),
                    cagr_15y: (val) => (val ? val.toFixed(1) + "%" : "-"),
                    sales_5y: (val) =>
                        val == null ? "-" : `${Math.round(parseFloat(val))}%`,
                    eps_5y: (val) =>
                        val == "-" ? "-" : `${Math.round(parseFloat(val))}%`,
                    short: (val) =>
                        checkNaN(val * 100, (val) => val.toFixed(2), "%"),
                    employees: (val) => (val ? formatNumber(val) : "-"),
                    weight: (val) => (val ? val + "%" : "-"),
                };
                const specialTypes = {
                    "pe": "pe",
                    "price_free_cash_flow": "pe",
                    beta: "beta",
                    "rsi": "rsi",
                    "recommandation": "Recom",
                };

                addCell(index + 1);

                keys.forEach((key) => {
                    let content = stock[key];
                    if (formatters.hasOwnProperty(key)) {
                        content = formatters[key](stock[key]);
                    }
                    const type = specialTypes[key] || "";
                    addCell(content, type);
                });
            }

            function formatLargeNumber(number) {
                // Trillions
                if (number >= 1e12) {
                    return (number / 1e12).toFixed(2) + "T";
                }
                // Billions
                else if (number >= 1e9) {
                    return (number / 1e9).toFixed(0) + "B";
                }
                // Millions
                else if (number >= 1e6) {
                    return (number / 1e6).toFixed(2) + "M";
                }
                // Thousands
                else if (number >= 1e3) {
                    return (number / 1e3).toFixed(2) + "K";
                } else {
                    return number.toString();
                }
            }

            // Function to fetch stock data and populate the table
            function getQQQHoldings() {
                const url = "https://finance.beuke.org/ndx";
                fetch(url)
                    .then((response) => response.json())
                    .then((data) => {
                        const tableBody = document
                            .getElementById("stockTable")
                            .getElementsByTagName("tbody")[0];
                        const spinner =
                            document.getElementById("loadingSpinner");

                        data.sort((a, b) => {
                            return b.market_cap - a.market_cap;
                        });
                        data.forEach((stock, index) => {
                            const row = tableBody.insertRow();

                            const pe = stock["P/E"];
                            const weight = parseFloat(stock["weight"]);

                            if (!isNaN(pe) && !isNaN(weight)) {
                                for (let i = 0; i < weight; i++) {
                                    weightedPeValues.push(parseFloat(pe));
                                }
                            }
                            addCells(stock, index);
                        });
                        // Hide spinner and show table when data is loaded
                        spinner.style.display = "none";
                        stockTable.style.display = "table";

                        if (weightedPeValues.length > 0) {
                            const sortedWeightedPeValues =
                                weightedPeValues.sort((a, b) => a - b);
                            const middleIndex = Math.floor(
                                sortedWeightedPeValues.length / 2
                            );
                            const weightedMedianPe =
                                sortedWeightedPeValues.length % 2 !== 0
                                    ? sortedWeightedPeValues[middleIndex]
                                    : (
                                          (sortedWeightedPeValues[
                                              middleIndex - 1
                                          ] +
                                              sortedWeightedPeValues[
                                                  middleIndex
                                              ]) /
                                          2
                                      ).toFixed(2);
                             document.getElementById("averagePe").textContent =
                                `Weighted Median P/E: ${weightedMedianPe}`;
                        } else {
                        }
                    })
                    .catch((error) => {
                        console.error("Error fetching data:", error);
                        // Hide spinner if there is an error
                        document.getElementById(
                            "loadingSpinner"
                        ).style.display = "none";
                    });
            }

            // Call the function to fetch and display the data

            // check if site has a query parameter
            const urlParams = new URLSearchParams(window.location.search);
            const tickers = urlParams.get("quotes");

            if (tickers) {
                // Split the tickers into an array, even if there's only one ticker without commas
                const tickerArray = tickers
                    .split(",")
                    .map((ticker) => ticker.trim()); // Trim whitespace from each ticker

                // Loop through each ticker and make a request to the finance API
                let index = 0;
                tickerArray.forEach((ticker) => {
                    const url = "https://finance.beuke.org/quote/" + ticker;
                    fetch(url)
                        .then((response) => response.json())
                        .then((data) => {
                            const tableBody = document
                                .getElementById("stockTable")
                                .getElementsByTagName("tbody")[0];
                            const spinner =
                                document.getElementById("loadingSpinner");
                            console.log(data);
                            addCells(data, index); // You need to define this function to add data to the table
                            index = index + 1;
                            if (index === tickerArray.length) {
                                // Hide spinner and show table when data is loaded
                                spinner.style.display = "none";
                                stockTable.style.display = "table";
                            }
                        })
                        .catch((error) => {
                            console.error(
                                "Error fetching data for ticker:",
                                ticker,
                                error
                            );
                        });
                });
            } else {
                getQQQHoldings();
            }

            (async () => {
                const data = await fetch("http://127.0.0.1:5000/ndx_data").then(
                    (response) => response.json()
                );

                // split the data set into ohlc and volume
                let ohlc = [],
                    volume = [],
                    dataLength = data.length;

                for (let i = 0; i < dataLength; i += 1) {
                    ohlc.push([
                        data[i]["Date"], // the date
                        parseFloat(data[i]["Open"].toFixed(1)), // open
                        parseFloat(data[i]["High"].toFixed(1)), // high
                        parseFloat(data[i]["Low"].toFixed(1)), // low
                        parseFloat(data[i]["Close"].toFixed(1)), // close
                    ]);

                    volume.push([
                        data[i]["Date"], // the date
                        data[i]["Volume"], // the volume
                    ]);
                }

                // filter 9 out of 10 data points from volume
                // filter 9 out of 10 data points from  ohlc

                // create the chart
                Highcharts.stockChart(
                    "container",
                    {
                        chart: {
                            height: 800,
                        },
                        title: {
                            text: "NDX Historical",
                        },
                        subtitle: {
                            text: "All indicators",
                        },
                        accessibility: {
                            series: {
                                descriptionFormat: "{seriesDescription}.",
                            },
                            description:
                                "Use the dropdown menus above to display different indicator series on the chart.",
                            screenReaderSection: {
                                beforeChartFormat:
                                    "<{headingTagName}>{chartTitle}</{headingTagName}><div>{typeDescription}</div><div>{chartSubtitle}</div><div>{chartLongdesc}</div>",
                            },
                        },
                        legend: {
                            enabled: true,
                        },
                        rangeSelector: {
                            allButtonsEnabled: true,
                            buttons: [
                                {
                                    type: "days",
                                    count: 3,
                                    text: "Day",
                                    dataGrouping: {
                                        forced: true,
                                        units: [["day", [1]]],
                                    },
                                },
                                {
                                    type: "year",
                                    count: 1,
                                    text: "Week",
                                    dataGrouping: {
                                        forced: true,
                                        units: [["week", [1]]],
                                    },
                                },
                                {
                                    type: "all",
                                    text: "Month",
                                    dataGrouping: {
                                        forced: true,
                                        units: [["month", [1]]],
                                    },
                                },
                            ],
                            buttonTheme: {
                                width: 60,
                            },
                            selected: 2,
                        },

                        yAxis: [
                            {
                                height: "60%",
                            },
                            {
                                top: "60%",
                                height: "20%",
                            },
                            {
                                top: "80%",
                                height: "20%",
                            },
                        ],
                        plotOptions: {
                            spline: {
                                marker: {
                                    enabled: false,
                                },
                            },
                            series: {
                                showInLegend: true,
                                accessibility: {
                                    exposeAsGroupOnly: true,
                                },
                            },
                        },

                        series: [
                            {
                                type: "candlestick",
                                id: "ndx",
                                name: "NDX",
                                data: ohlc,
                                dataGrouping: {
                                    groupPixelWidth: 30, // Quantity of points to group
                                },
                            },
                            {
                                type: "column",
                                id: "volume",
                                name: "Volume",
                                data: volume,
                                yAxis: 1,
                            },
                            {
                                type: "sma",
                                linkedTo: "ndx",
                                marker: {
                                    enabled: false,
                                },
                                params: {
                                    period: 50,
                                },
                            },
                            {
                                type: "rsi",
                                id: "oscillator",
                                linkedTo: "ndx",
                                yAxis: 2,
                                marker: {
                                    enabled: false,
                                },
                            },
                        ],
                    },
                    function (chart) {
                        document
                            .getElementById("overlays")
                            .addEventListener("change", function (e) {
                                const series = chart.get("overlay");

                                if (series) {
                                    series.remove(false);
                                    chart.addSeries({
                                        type: e.target.value,
                                        linkedTo: "aapl",
                                        id: "overlay",
                                    });
                                }
                            });

                        document
                            .getElementById("oscillators")
                            .addEventListener("change", function (e) {
                                const series = chart.get("oscillator");

                                if (series) {
                                    series.remove(false);
                                    chart.addSeries({
                                        type: e.target.value,
                                        linkedTo: "aapl",
                                        id: "oscillator",
                                        yAxis: 2,
                                    });
                                }
                            });
                    }
                );
            })();
        </script>
    </body>
</html>