dVudI6ymyy2jeA_msGyu0md6uI6cA6voV90UfMU-8yw
スポンサーリンク

【python】移動平均線で順張りスクリーニングするプログラムを作ってみるテスト

python
スポンサーリンク
moomoo証券【WEB】

移動平均線とは

株価チャートを見ると、ローソク足の上に何本かの線が引かれていることがあります。
その代表的なものが 移動平均線(Moving Average / MA) です。

移動平均線とは、「過去◯日間の終値の平均をつないだ線」のことです

たとえば 5日移動平均線(5日線、5MA) なら直近5営業日の終値の平均、
25日移動平均線(25日線、25MA) なら1か月分の市場参加者の平均取得コストを表していると考えられます。

「5日線を超える」と上向きの流れが強くなる

下記は任天堂(7974)の日足です。オレンジ矢印は5日線より上の時期、青矢印は5日線より下のゾーンです。

直近12月の下落がわかりやすいですが、陽線でも5日線を超えていない間は下落基調です。

逆に5日線を超えている間は上昇しています。

このように実際にトレードをしていると、

  • 底値っぽい形は出た
  • 下ヒゲや十字線も出た
  • でもその後、下に行ってしまった

という経験を何度もします。

これは、「まだ市場全体が“買い”に合意していなかった」状態でエントリーしていることが多いからです。

そこで私が意識するようになったのが、

「5日移動平均線を明確に上回っているかどうか」

です。

5日線を超えるということは、
直近数日間の参加者の平均コストよりも高い価格で、それでも買われている
という状態です。

つまり、

  • 個人
  • 短期トレーダー
  • アルゴ・機関投資家

を含めて「この価格帯は買い」と判断され始めたサインとも言えます。

底を当てにいくのではなく、
市場の判断に乗る「順張り」 に切り替えたことで、トレードがかなり安定しました。

このプログラムでやりたいこと

この考え方をベースに、

「5日線より上にあり、流れが上向きに変わりつつある銘柄」

を、感覚ではなく 数値でスクリーニング するために作ったのが、今回の Python プログラムです。

このスクリプトでは、全上場銘柄を対象にして、主に次の点をチェックしています。

チェック項目

① 5日移動平均線が上向いているか

短期のトレンドが上に向き始めているかどうか。

② 5日線が「折り返し」ているか

下落基調だった5日線が、直近数営業日で下げ止まり → 上向きに転じたか。

③ 終値が5日線より上にあるか

「市場が買いと判断した価格帯」に乗れているかどうか。

④ 25日移動平均線の向き

中期トレンドが悪すぎないかの確認。
(25日線が下向きの場合はスコアを抑える設計)

⑤ 5日線と25日線の距離

短期線が中期線に近づいているか、上抜け余地があるか。

⑥ 出来高ブレイクの有無(直近5営業日)

出来高が急増している銘柄は、
「参加者が増え、トレンドが出やすい」 ため別枠でチェック。

⑦ 株式分割があった銘柄への対応

分割によって移動平均線が歪まないよう、
過去データを補正した上で判定しています。

出力結果の見方

スクリーニング結果は CSV で出力され、

  • 各条件は
    • ○(条件クリア)
    • ✕(未達)
  • 右端には
    • ○の数を合計したスコア

を表示しています。

スコアが高いほど、

「5日線ベースの順張りに適した形」

になっている銘柄、という見方ができます。

※ 出来高ブレイクは参考情報として表示し、スコア計算には含めていません。

このプログラムの使い方

このプログラムは、

  • 「この銘柄を買え」と教えてくれるもの
  • 未来の株価を予測するもの

ではありません。

あくまで、

「市場が買いだと判断し始めた銘柄候補を、効率よく並べる道具」

です。

実際のエントリーでは、

  • 日足の形
  • 上ヒゲ・下ヒゲ
  • 地合い(指数)
  • 決算・テーマ性

を必ず確認した上で判断するようにしています。

参考書籍

順張りスイングトレードの極意 最強トレーダーの知恵からボラティリティブレイクアウト活用術まで! [ 荻窪 禅 ]価格:1540円
株は順張り!! 〜勝率8割以上の常勝トレーダーになる! 〜【電子書籍】[ 二階堂重人 ]価格:1430円

プログラムコード

作成したプログラムは下記です。利用される場合、必要なモジュールはインポートください。

また、銘柄名として同じフォルダに「nk.csv」が必要です。

日本取引所(https://www.jpx.co.jp/markets/statistics-equities/misc/01.html)からダウンロードし、csvで保存ください(ETFなどの余計な銘柄もあるので、加工して保存した方が良いです)。

import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

import datetime
import pandas as pd
import yfinance as yf
from tqdm import tqdm
import time
import os

# ======================
# 設定
# ======================
CACHE_DIR = "yf_ma_cache"
BATCH_SIZE = 300
WAIT_TIME = 8

start = "2022-10-01"
end = datetime.datetime.today().strftime('%Y-%m-%d')

# ======================
# True / False → ○×
# ======================
def arrow(val):
    return "○" if val else "✕"

# ======================
# 株式分割補正
# ======================
def adjust_for_stock_split(df):
    df = df.copy()

    # 前日比
    ratio = df["Close"] / df["Close"].shift(1)

    # 分割らしきポイント検出
    split_points = ratio[(ratio < 0.45) | (ratio > 2.2)]
    if split_points.empty:
        return df, False, None

    split_date = split_points.index[0]

    # 前日と当日の終値を正しく取得
    prev_close = df["Close"].shift(1).loc[split_date]
    curr_close = df.loc[split_date, "Close"]

    if pd.isna(prev_close) or curr_close == 0:
        return df, False, None

    split_ratio = round(prev_close / curr_close)

    if split_ratio <= 1:
        return df, False, None

    # 分割日以前を補正
    price_cols = ["Open", "High", "Low", "Close"]
    df.loc[:split_date, price_cols] /= split_ratio

    return df, True, split_date


# ======================
# キャッシュ初期化
# ======================
os.makedirs(CACHE_DIR, exist_ok=True)
for f in os.listdir(CACHE_DIR):
    path = os.path.join(CACHE_DIR, f)
    if os.path.isfile(path):
        os.remove(path)

# ======================
# 銘柄リスト
# ======================
df_nk = pd.read_csv("nk.csv", dtype=str)

tickers = [c.strip() + ".T" for c in df_nk["コード"]]
names = df_nk["銘柄名"].tolist()
markets = df_nk["市場・商品区分"].tolist()

results = []

# ======================
# メイン処理
# ======================
for i in range(0, len(tickers), BATCH_SIZE):
    batch_tickers = tickers[i:i+BATCH_SIZE]
    batch_names = names[i:i+BATCH_SIZE]
    batch_markets = markets[i:i+BATCH_SIZE]

    cache_path = os.path.join(CACHE_DIR, f"batch_{i}.csv")

    if os.path.exists(cache_path):
        data = pd.read_csv(cache_path, header=[0,1], index_col=0, parse_dates=True)
    else:
        data = yf.download(
            batch_tickers,
            start=start,
            end=end,
            group_by="ticker",
            threads=True,
            progress=False
        )
        data.to_csv(cache_path)
        time.sleep(WAIT_TIME)

    for t, raw_name, raw_market in tqdm(
        zip(batch_tickers, batch_names, batch_markets),
        total=len(batch_tickers)
    ):

        if t not in data.columns.get_level_values(0):
            continue

        df = data[t].copy()
        df = df[df["Close"].notna()].sort_index()

        if len(df) < 30:
            continue

        # ===== 株式分割補正 =====
        df, split_adjusted, split_date = adjust_for_stock_split(df)

        # ======================
        # 移動平均
        # ======================
        df["MA5"] = df["Close"].rolling(5).mean()
        df["MA25"] = df["Close"].rolling(25).mean()
        df["VolMA5"] = df["Volume"].rolling(5).mean()

        df.dropna(subset=["MA5", "MA25"], inplace=True)
        if len(df) < 6:
            continue

        # ======================
        # 最新営業日
        # ======================
        latest = df.iloc[-1]
        prev5 = df.iloc[-6]

        close = latest["Close"]
        latest_volume = int(latest["Volume"])
        ma5 = latest["MA5"]
        ma25 = latest["MA25"]

        if latest_volume < 100000:
            continue

        # ======================
        # 判定ロジック
        # ======================
        ma5_up = ma5 > df["MA5"].iloc[-2]

        ma5_turn = False
        for j in range(2, 6):
            if df["MA5"].iloc[-j] < df["MA5"].iloc[-j-1] and ma5 > df["MA5"].iloc[-2]:
                ma5_turn = True
                break

        ma25_up = ma25 > df["MA25"].iloc[-2]
        close_over_ma5 = close >= ma5

        dist_now = abs(ma5 - ma25)
        dist_prev = abs(prev5["MA5"] - prev5["MA25"])
        ma_converging = dist_now < dist_prev

        ma5_over_ma25 = ma5 > ma25
        ma_diff_pct = (ma5 - ma25) / ma25 * 100

        # ======================
        # 出来高ブレイク(表示専用)
        # ======================
        vol_break = ""
        for j in range(1, 6):
            if df["Volume"].iloc[-j] > df["VolMA5"].iloc[-j] * 1.5:
                vol_break = "○"
                break

        # ======================
        # ↑スコア(出来高除外)
        # ======================
        score = sum([
            ma5_up,
            ma5_turn,
            ma25_up,
            close_over_ma5,
            ma_converging,
            ma5_over_ma25
        ])

        # ===== 分割直後スコア調整 =====
        if split_adjusted:
            days_from_split = (df.index[-1] - split_date).days
            if days_from_split <= 30:
                score = max(score - 1, 0)

        # ======================
        # name / market 成形
        # ======================
        code = t.replace(".T", "")
        name = raw_name.replace("ホールディングス","HD").replace("グループ","G").replace("株式会社","").strip()
        market = raw_market.replace("(内国株式)","").replace("内国株式","").replace("プライム","P").replace("スタンダード","S").replace("グロース","G").strip()

        # ======================
        # 結果格納
        # ======================
        results.append([
            code, name, market,
            round(close,2),
            latest_volume,
            round(ma5,2),
            round(ma25,2),
            arrow(ma5_up),
            arrow(ma5_turn),
            arrow(ma25_up),
            arrow(close_over_ma5),
            arrow(ma_converging),
            arrow(ma5_over_ma25),
            round(ma_diff_pct,2),
            vol_break,
            "○" if split_adjusted else "",
            score
        ])

# ======================
# 出力
# ======================
df_out = pd.DataFrame(results, columns=[
    "コード","銘柄名","市場",
    "終値","直近出来高",
    "MA5","MA25",
    "5日線向き",
    "5日線折返",
    "25日線向き",
    "終値≧5日線",
    "MA接近",
    "5日線≧25日線",
    "5日線乖離率(%)",
    "出来高ブレイク",
    "分割補正",
    "スコア"
])

today = datetime.datetime.today().strftime("%Y%m%d")
df_out.to_csv(f"{today}_ma_screen.csv", index=False, encoding="utf-8-sig")

print(f"✅ 完了:{len(df_out)} 銘柄抽出")

実行結果の例(25/12/26)

  • プライムのみ
  • 5日線上向き
  • 終値が5日線より上
  • 25日線より5日線が下

※リストはコード順です

コード銘柄名市場終値MA5MA25スコア
1885東亜建設工業P27592752.82781.924
1887日本国土開発P532527.4530.175
1893五洋建設P1559.51548.41595.524
1911住友林業P16141608.41608.85
2353日本駐車場開発P269261.82644
2371カカクコムP23552239.82276.264
2461ファンコミュニケーションズP531529.4532.65
2533オエノンHDP516513519.484
2670エービーシー・マートP27062683.62702.064
2802味の素P33743357.83455.884
2931ユーグレナP403400.2405.044
3103ユニチカP277266282.324
3197すかいらーくHDP35273489.83529.324
3387クリエイト・レストランツ・HDP776762.8773.844
3397トリドールHDP42774220.84311.084
3903gumiP356348.2368.524
3993PKSHA TechnologyP3455337333735
4151協和キリンP25702541.12569.744
4180Appier GroupP11091096.21116.844
4369トリケミカル研究所P276327322814.965
4483JMDCP380037693929.84
4506住友ファーマP238423322419.584
4523エーザイP47034601.64688.525
4544H.U.GHDP347233893417.844
4927ポーラ・オルビスHDP13431332.11332.35
4974タカラバイオP791789.4799.684
5541大平洋金属P22552173.62253.484
5602栗本鐵工所P168516701693.84
5715古河機械金属P378037553868.65
5726大阪チタニウムテクノロジーズP18851855.41954.524
6047GunosyP533530.4541.564
6055ジャパンマテリアルP157015641619.764
6141DMG森精機P2663.526532662.45
6240ヤマシンフィルタP597591.6606.524
6383ダイフクP49254921.44964.684
6417SANKYOP25542550.22585.084
6472NTNP360.5359.24366.554
6526ソシオネクストP2200.521982217.585
6544ジャパンエレベーターサービスHDP1780.51762.81815.984
6740ジャパンディスプレイP2120.220.245
6754アンリツP22662253.62276.245
6758ソニーGP40454020.64241.724
6952カシオ計算機P1261.51260.61266.65
6966三井ハイテックP747742.2782.124
6981村田製作所P32073188.43247.964
7071アンビスHDP457448.2459.244
7095Macbee PlanetP15551498.41647.24
7731ニコンP1756.51735.11788.985
7867タカラトミーP27502733.327734
8113ユニ・チャームP901.8896902.254
8697日本取引所GP1697.51684.31720.164
8801三井不動産P17851779.41789.245
9007小田急電鉄P1731.51703.51716.424
9023東京地下鉄P16051580.71591.045
9042阪急阪神HDP397039043908.485
9064ヤマトHDP22152194.22204.24
9229サンウェルズP359348.2384.564
9418U-NEXT HOLDINGSP20021968.21991.125
9434ソフトバンクP217.1215.42218.384
9468KADOKAWAP31723116.83180.284
9517イーレックスP626619.2624.324
9552M&A総研HDP11671133.81158.244
9684スクウェア・エニックス・HDP2903.52869.52955.984
9697カプコンP36743599.43668.524
9766コナミGP215602143722502.24
9861吉野家HDP30993057.63080.164

アウトプット例:大阪チタニウムテクノロジーズ(5726)

https://finance.yahoo.co.jp/quote/5726.T/chart?styl=cndl&frm=dly&scl=stndrd&trm=6m&evnts=&ovrIndctr=sma%2Cmma&addIndctr=

おわりに

「底を当てる」よりも、

「市場の流れに乗り、取れるところを取る」

という考え方に変えてから、
トレードのストレスがかなり減りました。

このスクリーニングも、
そのための 補助輪 のような位置づけです。

同じように、

  • 逆張りで疲れている方
  • 感覚トレードから抜けたい方

のヒントになればうれしいです。

コメント

タイトルとURLをコピーしました