Scriptone Scriptone

[Stock Analysis with Python] Detecting Moving Averages and Golden Cross Using Modern Libraries

blog-image

Overview

Using modern Python libraries like Polars, Plotly, marimo, and kand, we’ll detect moving averages and golden cross patterns.

Motivation

I receive part of my salary in the form of RSU (Restricted Stock Units). RSUs are attractive as an income source when the stock is performing well, but it’s like carrying all your eggs in one basket—if the company’s performance declines, losses can be significant. Therefore, it’s important to diversify your eggs into multiple baskets. Holding only cash or only one stock as RSU is not ideal; proper diversification is essential for risk management. While it’s easy to quickly diversify into index funds (investment trusts) or gold products, individual stocks can be enjoyable for actively taking risks or enjoying dividends and shareholder benefits (after all, RSUs are essentially individual stocks). So, while keeping in mind the need to deepen my understanding of the RSUs I own, I’ll be analyzing individual stocks.

Python has mature and abundant libraries like Pandas, Ta-Lib-Python, Matplotlib, yfinance, and Jupyter, which are very convenient due to their ease of integration. However, since the 2020s, many modern libraries have emerged that offer additional benefits like addressing pain points and speed advantages that traditional libraries don’t have. While older libraries are mature with abundant information, I want to enjoy analysis while learning new libraries simultaneously. Although these methods can also be used for systematic trading, I prefer technical analysis to identify trading candidates and timing while ensuring humans can easily manage and understand the situation, and also verifying there are no fundamental issues.

Libraries Used

The libraries are as follows:

LibraryRelated LibraryDescription
polarspandasA library for high-speed data frame operations
plotlymatplotlibA visualization library that allows users to interact with graphs
marimojupyterA notebook that runs in .py format and allows interactive code execution
kandta-lib-pythonA Rust-based technical analysis library that can be used in Python and Web through PYO3 and Wasm
yfinance-plyfinanceA Python library wrapping yfinance-rs that allows handling Yahoo Finance data with Polars

Notebook and Code

The HTML version of the marimo notebook is available here. Here’s also a link to the Python code on GitHub.

Explanation

Fetching Stock Price Data

Stock price data is fetched using yfinance-pl. By appending .T to the stock code, you can retrieve data in Polars DataFrame format from yfinance-pl. For example, Toyota is 7203.T, SoftBank is 9984.T, etc. I hold shares of San-in Godo Bank (Gogin) 8381.T, a regional bank that I believe has stable management, for dividends and shareholder benefits, so I’ll use 8381.T as an example.

import yfinance_pl as yf

ticker = yf.Ticker("8381.T")
hist = ticker.history(period="1y")

For the period parameter, enter a duration like 1y or 1m. Since yfinance_pl specifies Literals, you can use code completion in Visual Studio Code or PyCharm. Here’s the hist data in TSV format (showing the latest 5 entries):

open.amount	open.currency	high.amount	high.currency	low.amount	low.currency	close.amount	close.currency	close_unadj.amount	close_unadj.currency	volume	date
Decimal('1374.0000000000')	JPY	Decimal('1404.0000000000')	JPY	Decimal('1372.0000000000')	JPY	Decimal('1400.0000000000')	JPY	Decimal('1400.0000000000')	JPY	400000	2025-11-21 00:00:00
Decimal('1405.0000000000')	JPY	Decimal('1413.0000000000')	JPY	Decimal('1384.0000000000')	JPY	Decimal('1393.0000000000')	JPY	Decimal('1393.0000000000')	JPY	317800	2025-11-25 00:00:00
Decimal('1409.0000000000')	JPY	Decimal('1412.0000000000')	JPY	Decimal('1399.0000000000')	JPY	Decimal('1407.0000000000')	JPY	Decimal('1407.0000000000')	JPY	387300	2025-11-26 00:00:00
Decimal('1420.0000000000')	JPY	Decimal('1454.0000000000')	JPY	Decimal('1420.0000000000')	JPY	Decimal('1442.0000000000')	JPY	Decimal('1442.0000000000')	JPY	547300	2025-11-27 00:00:00
Decimal('1450.0000000000')	JPY	Decimal('1469.0000000000')	JPY	Decimal('1449.0000000000')	JPY	Decimal('1453.0000000000')	JPY	Decimal('1453.0000000000')	JPY	428800	2025-11-28 00:00:00

Calculating Moving Averages

Moving averages are calculated by sliding N days of data including the current day for each date. For short-term averages, use the 5-day moving average, and for monthly trends, use the 25-day moving average. While stock prices fluctuate daily, averaging helps understand trends while reducing daily variance. For this calculation, I use the Rust-based kand instead of Ta-Lib-Python, but there’s almost no difference and it’s easy to calculate.

import kand as ka
import polars as pl
import yfinance_pl as yf

ticker = yf.Ticker("8381.T")
hist = ticker.history(period="1y")
close = hist["close.amount"].to_numpy().astype("float64")
hist_with_ma = hist.with_columns(
    ma5=pl.Series(ka.sma(close, period=5)),
    ma25=pl.Series(ka.sma(close, period=25)),
)

SMA stands for Simple Moving Average, and by changing the period value, you can calculate trends for longer-term investments such as 90 days or 200 days.

Here are the first 30 rows of hist_with_ma in TSV format:

open.amount	open.currency	high.amount	high.currency	low.amount	low.currency	close.amount	close.currency	close_unadj.amount	close_unadj.currency	volume	date	ma5	ma25
Decimal('1147.0000000000')	JPY	Decimal('1155.0000000000')	JPY	Decimal('1142.0000000000')	JPY	Decimal('1151.0000000000')	JPY	Decimal('1195.0000000000')	JPY	212000	2024-11-28 00:00:00	NaN	NaN
Decimal('1151.0000000000')	JPY	Decimal('1169.0000000000')	JPY	Decimal('1145.0000000000')	JPY	Decimal('1163.0000000000')	JPY	Decimal('1207.0000000000')	JPY	222500	2024-11-29 00:00:00	NaN	NaN
Decimal('1173.0000000000')	JPY	Decimal('1213.0000000000')	JPY	Decimal('1169.0000000000')	JPY	Decimal('1211.0000000000')	JPY	Decimal('1257.0000000000')	JPY	493800	2024-12-02 00:00:00	NaN	NaN
Decimal('1214.0000000000')	JPY	Decimal('1235.0000000000')	JPY	Decimal('1208.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1272.0000000000')	JPY	440100	2024-12-03 00:00:00	NaN	NaN
Decimal('1220.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1235.0000000000')	JPY	279400	2024-12-04 00:00:00	1188.2	NaN
Decimal('1200.0000000000')	JPY	Decimal('1213.0000000000')	JPY	Decimal('1198.0000000000')	JPY	Decimal('1209.0000000000')	JPY	Decimal('1255.0000000000')	JPY	241000	2024-12-05 00:00:00	1199.8	NaN
Decimal('1210.0000000000')	JPY	Decimal('1216.0000000000')	JPY	Decimal('1198.0000000000')	JPY	Decimal('1202.0000000000')	JPY	Decimal('1248.0000000000')	JPY	130400	2024-12-06 00:00:00	1207.6	NaN
Decimal('1207.0000000000')	JPY	Decimal('1225.0000000000')	JPY	Decimal('1196.0000000000')	JPY	Decimal('1216.0000000000')	JPY	Decimal('1262.0000000000')	JPY	309500	2024-12-09 00:00:00	1208.6	NaN
Decimal('1232.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1209.0000000000')	JPY	Decimal('1215.0000000000')	JPY	Decimal('1261.0000000000')	JPY	300100	2024-12-10 00:00:00	1206.4	NaN
Decimal('1216.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1204.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1272.0000000000')	JPY	256900	2024-12-11 00:00:00	1213.6	NaN
Decimal('1238.0000000000')	JPY	Decimal('1242.0000000000')	JPY	Decimal('1230.0000000000')	JPY	Decimal('1233.0000000000')	JPY	Decimal('1280.0000000000')	JPY	307600	2024-12-12 00:00:00	1218.4	NaN
Decimal('1223.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1210.0000000000')	JPY	Decimal('1216.0000000000')	JPY	Decimal('1262.0000000000')	JPY	348500	2024-12-13 00:00:00	1221.2	NaN
Decimal('1218.0000000000')	JPY	Decimal('1221.0000000000')	JPY	Decimal('1194.0000000000')	JPY	Decimal('1201.0000000000')	JPY	Decimal('1247.0000000000')	JPY	252900	2024-12-16 00:00:00	1218.2	NaN
Decimal('1205.0000000000')	JPY	Decimal('1215.0000000000')	JPY	Decimal('1193.0000000000')	JPY	Decimal('1195.0000000000')	JPY	Decimal('1240.0000000000')	JPY	215500	2024-12-17 00:00:00	1214.2	NaN
Decimal('1193.0000000000')	JPY	Decimal('1195.0000000000')	JPY	Decimal('1182.0000000000')	JPY	Decimal('1193.0000000000')	JPY	Decimal('1238.0000000000')	JPY	189600	2024-12-18 00:00:00	1207.6	NaN
Decimal('1175.0000000000')	JPY	Decimal('1195.0000000000')	JPY	Decimal('1169.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1235.0000000000')	JPY	363000	2024-12-19 00:00:00	1199	NaN
Decimal('1197.0000000000')	JPY	Decimal('1197.0000000000')	JPY	Decimal('1168.0000000000')	JPY	Decimal('1168.0000000000')	JPY	Decimal('1212.0000000000')	JPY	511500	2024-12-20 00:00:00	1189.4	NaN
Decimal('1171.0000000000')	JPY	Decimal('1204.0000000000')	JPY	Decimal('1171.0000000000')	JPY	Decimal('1204.0000000000')	JPY	Decimal('1250.0000000000')	JPY	403500	2024-12-23 00:00:00	1190	NaN
Decimal('1216.0000000000')	JPY	Decimal('1226.0000000000')	JPY	Decimal('1209.0000000000')	JPY	Decimal('1222.0000000000')	JPY	Decimal('1268.0000000000')	JPY	320300	2024-12-24 00:00:00	1195.4	NaN
Decimal('1217.0000000000')	JPY	Decimal('1217.0000000000')	JPY	Decimal('1194.0000000000')	JPY	Decimal('1205.0000000000')	JPY	Decimal('1251.0000000000')	JPY	207300	2024-12-25 00:00:00	1197.8	NaN
Decimal('1204.0000000000')	JPY	Decimal('1208.0000000000')	JPY	Decimal('1193.0000000000')	JPY	Decimal('1208.0000000000')	JPY	Decimal('1254.0000000000')	JPY	230600	2024-12-26 00:00:00	1201.4	NaN
Decimal('1209.0000000000')	JPY	Decimal('1220.0000000000')	JPY	Decimal('1205.0000000000')	JPY	Decimal('1220.0000000000')	JPY	Decimal('1266.0000000000')	JPY	252400	2024-12-27 00:00:00	1211.8	NaN
Decimal('1224.0000000000')	JPY	Decimal('1230.0000000000')	JPY	Decimal('1214.0000000000')	JPY	Decimal('1219.0000000000')	JPY	Decimal('1265.0000000000')	JPY	159400	2024-12-30 00:00:00	1214.8	NaN
Decimal('1228.0000000000')	JPY	Decimal('1228.0000000000')	JPY	Decimal('1211.0000000000')	JPY	Decimal('1225.0000000000')	JPY	Decimal('1271.0000000000')	JPY	443500	2025-01-06 00:00:00	1215.4	NaN
Decimal('1231.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1217.0000000000')	JPY	Decimal('1222.0000000000')	JPY	Decimal('1268.0000000000')	JPY	403500	2025-01-07 00:00:00	1218.8	1205.2
Decimal('1216.0000000000')	JPY	Decimal('1244.0000000000')	JPY	Decimal('1215.0000000000')	JPY	Decimal('1237.0000000000')	JPY	Decimal('1284.0000000000')	JPY	469100	2025-01-08 00:00:00	1224.6	1208.64
Decimal('1232.0000000000')	JPY	Decimal('1232.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1253.0000000000')	JPY	421100	2025-01-09 00:00:00	1222	1210.4
Decimal('1204.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1190.0000000000')	JPY	Decimal('1196.0000000000')	JPY	Decimal('1241.0000000000')	JPY	328400	2025-01-10 00:00:00	1217.4	1209.8
Decimal('1190.0000000000')	JPY	Decimal('1194.0000000000')	JPY	Decimal('1164.0000000000')	JPY	Decimal('1173.0000000000')	JPY	Decimal('1217.0000000000')	JPY	394900	2025-01-14 00:00:00	1207	1207.68
Decimal('1182.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1178.0000000000')	JPY	Decimal('1207.0000000000')	JPY	Decimal('1253.0000000000')	JPY	362200	2025-01-15 00:00:00	1204	1208.36

The 5-day moving average cannot be calculated without 5 days of data including the current day, so the first 4 rows are NaN. Similarly, the 25-day moving average requires past data, so 24 rows are NaN.

Chart Visualization

Using Plotly’s Candlestick, you can easily display charts. Candlesticks are represented by rectangles (body) and lines (wicks), showing upper wick, lower wick, open, and close prices. They are colored in red and green based on the relationship between open and close prices, showing price increases/decreases and daily price volatility. You can easily display the chart by passing these 4 pieces of information along with the color and date data on the horizontal axis.

# omitted

df_plot = hist_with_ma.with_columns(
    [
        pl.col("open.amount").cast(pl.Float64),
        pl.col("high.amount").cast(pl.Float64),
        pl.col("low.amount").cast(pl.Float64),
        pl.col("close.amount").cast(pl.Float64),
    ]
)
dates = df_plot["date"].to_list()

ma_data = [
    go.Candlestick(
        yaxis="y1",
        x=dates,
        open=df_plot["open.amount"],
        high=df_plot["high.amount"],
        low=df_plot["low.amount"],
        close=df_plot["close.amount"],
        increasing_line_color="red",
        decreasing_line_color="green",
        name=f"{company_name} Stock Price",
    ),
    # omitted
]

ma_fig = go.Figure(data=ma_data, layout=go.Layout(ma_layout))

# omitted

ma_fig  # displays the chart in the notebook

Golden Cross and Dead Cross

When long-term and short-term moving averages intersect, it serves as an indicator of trend reversal (or trading signal). When the short-term moving average crosses above the long-term moving average, it’s called a “Golden Cross”; when it crosses below, it’s called a “Dead Cross”.

These can be detected by calculating the difference between short-term and long-term moving averages and checking whether the sign of that difference changes between the previous day and the current day.

(1) If the short-term moving average was below the long-term moving average yesterday, and today the short-term moving average crosses above the long-term moving average, it’s a Golden Cross (2) If the short-term moving average was above the long-term moving average yesterday, and today the short-term moving average crosses below the long-term moving average, it’s a Dead Cross

# Calculate the difference between SMA5 and SMA25 for previous and current day
diff = SMA5 - SMA25
prev_diff = diff.shift(1)

# Golden Cross: previous day SMA5 < SMA25, current day SMA5 > SMA25
golden_cross = (prev_diff < 0) & (diff > 0)

# Dead Cross: previous day SMA5 > SMA25, current day SMA5 < SMA25
dead_cross = (prev_diff > 0) & (diff < 0)

When golden_cross is True, it indicates a Golden Cross; when dead_cross is True, it indicates a Dead Cross.

You can also display Golden Cross and Dead Cross on Plotly.

# Calculate the difference between SMA5 and SMA25
df_cross = (
    hist_with_ma.with_columns(
        diff=(pl.col("ma5") - pl.col("ma25")),
    )
    .with_columns(
        prev_diff=pl.col("diff").shift(1),
    )
    .with_columns(
        # Golden Cross: previous day is negative (SMA5 < SMA25), current day is positive (SMA5 > SMA25)
        golden_cross=(pl.col("prev_diff") < 0) & (pl.col("diff") > 0),
        # Dead Cross: previous day is positive (SMA5 > SMA25), current day is negative (SMA5 < SMA25)
        dead_cross=(pl.col("prev_diff") > 0) & (pl.col("diff") < 0),
    )
)

# Extract Golden Cross dates
golden_crosses = df_cross.filter(pl.col("golden_cross")).select(
    pl.col("date"),
    pl.col("close.amount").alias("price"),
    pl.lit("Golden Cross").alias("signal"),
)

# Extract Dead Cross dates
dead_crosses = df_cross.filter(pl.col("dead_cross")).select(
    pl.col("date"),
    pl.col("close.amount").alias("price"),
    pl.lit("Dead Cross").alias("signal"),
)

# Combine and sort Golden Cross and Dead Cross
signals = pl.concat([golden_crosses, dead_crosses]).sort("date")

# Separate Golden Cross and Dead Cross data
_golden = signals.filter(pl.col("signal") == "Golden Cross")
_dead = signals.filter(pl.col("signal") == "Dead Cross")

signal_data = [
    go.Candlestick(
        yaxis="y1",
        x=_dates,
        open=_df_plot["open.amount"],
        high=_df_plot["high.amount"],
        low=_df_plot["low.amount"],
        close=_df_plot["close.amount"],
        increasing_line_color="red",
        decreasing_line_color="green",
        name=f"{_company_name} Stock Price",
    ),
    # omitted
    # Golden Cross markers
    go.Scatter(
        yaxis="y1",
        x=_golden["date"].to_list(),
        y=_golden["price"].cast(pl.Float64).to_list(),
        mode="markers",
        name="Golden Cross",
        marker={
            "symbol": "triangle-up",
            "size": 15,
            "color": "lime",
            "line": {"color": "darkgreen", "width": 2},
        },
    ),
    # Dead Cross markers
    go.Scatter(
        yaxis="y1",
        x=_dead["date"].to_list(),
        y=_dead["price"].cast(pl.Float64).to_list(),
        mode="markers",
        name="Dead Cross",
        marker={
            "symbol": "triangle-down",
            "size": 15,
            "color": "red",
            "line": {"color": "darkred", "width": 2},
        },
    ),
]

Summary

In this article, I’ve introduced how to fetch stock prices, display them in candlestick charts, calculate moving averages, and detect Golden Cross and Dead Cross patterns. While using Pandas has been convenient for a long time, as you can see from the code, you can perform everything from fetching stock prices to visualization using only Polars without going through Pandas. While Golden Cross and Dead Cross alone may not be sufficient, we’ve managed to create basic and understandable indicators, taking the first step in analysis. I’ll continue to perform other indicators and more advanced analysis through modern libraries.

Reference book: Practical Stock Investment Analysis with Python (Hikaru Katafuchi (Author), Yoshihiro Yamada (Supervisor))

comment

Loading comments...