デイトレーダーがチャートと出来高・株価から、pythonで投資銘柄を選定する方法1(OCRを利用して銘柄一覧を作成する)
はじめに
こんにちは、日本株の個別株投資歴6年目のマチです。デイトレーダーやスイングトレーダーの皆さんは、どのように投資対象の銘柄を選んでいますか。私は「よく投資する銘柄リスト」から時間の許す限りチャートを眺めて決めていました。
本当はちゃんと、出来高と株価などでスクリーニングして、さらにチャートの形状で銘柄を絞り込みたいのですが、2025年7月現在ではチャートと出来高・株価などを組み合わせてスクリーニングできるサイトはあまり見かけないですよね。だから作りました。今回は、私が作ったスクリーニングのプログラムを紹介します。手順のイメージは以下の通りです。手順4の「目視による厳選」では、松井証券のツールを使用します(証券口座の開設が必要です)。
1.環境構築
Dockerでpythonを扱える環境を構築します。手順は、記事「【図解】Windows11でWSL2+DockerによるPython開発環境を構築する手順」を参照ください。Dockerfileなどの資材は以下を使用しました。
#ベースイメージ
FROM jupyter/base-notebook:python-3.10.10
# rootユーザーに切り替え
USER root
# システム依存パッケージのインストール(OCR用)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-jpn \
poppler-utils \
libglib2.0-0 \
libgl1 \
libsm6 \
libxext6 \
libxrender-dev \
fonts-ipafont-gothic \
&& apt-get clean && \
rm -rf /var/lib/apt/lists/*
# ノートブックユーザーに戻す(安全対策)
USER ${NB_UID}
#ローカルのrequirements.txtを、コンテナ内にコピーしてライブラリをインストール
COPY requirements.txt ./
RUN pip3 install --no-cache-dir -r requirements.txt
setuptools
numpy
pandas
openpyxl
xlrd
yfinance
pillow
pytesseract
pdf2image
opencv-python
services:
jupyterlab:
build:
context: .
dockerfile: ./Dockerfile
image: zenn0
container_name: zenn0_c
ports:
- "127.0.0.1:8888:8888" #jupyter用
working_dir: /home/jovyan/work
volumes:
- ./source:/home/jovyan/work #ローカルとコンテナ環境をマウントする
environment:
TZ: "Asia/Tokyo"
tty: true
restart: always
ここまでの進捗
2.データ収集(手動)
残念ながら、データ収集の一部は手動です。スクレイピングやクローリングも検討しましたが、サーバーに対する負荷等の都合で、Webページの運営側が規約などで禁止するケースが増えている(今は言及されていなくても今後禁止される可能性が高い)ことから、不採用としました。
2-1.銘柄の一覧を取得
以下のいずれかの方法で銘柄の一覧を取得します。前者の方がプログラムで処理するデータ量が少なくなり、実行時間を抑えられるのでおすすめです。
-
SBI証券でスクリーニングした結果をCSVダウンロードする
SBI証券のスクリーニング機能(使用には証券口座の開設が必要)であれば、株価で絞り込んだ銘柄の一覧をCSVダウンロードできます。2025年7月現在は出来高での絞り込みはできなさそうなので、後述のプログラムで実行します。 -
全銘柄のデータをダウンロードする
日本取引所のサイトからxlsファイルをダウンロードします。月1の頻度で更新されます。
2-2.チャートのスクショを取得
チャートの形状でスクリーニングできるサイトから、得意なチャートパターンのwebページ全体のスクショを取得します。サイトは好きなものを使ってください。私は、株マップ.comを使用しています。後述のプログラムも、株マップ.comのwebページをベースにしています。スクショを撮る際は、Chromeの全画面スクショ機能を使うのがおすすめです。
ここまでの進捗
3.プログラム作成・実行(スクリーニング)
プログラムは以下の通り、大きく3つのステップで構成されます。
フォルダ構成は以下の通りです。「input」フォルダ配下に、収集した銘柄一覧(screener_sbi.csv または data_j.xls)とスクショを配置します。個人的な好みにつき、スクショはチャートの形状パターンごとにフォルダ(フォルダ名に「long」または「short」の文字列を含む)を分けています。
work
│ docker-compose.yaml
│ Dockerfile
│ requirements.txt
│
└─source
│ make_master.ipynb
│ screening.ipynb
│
├─input
│ │ data_j.xls
│ │ screener_sbi.csv
│ │
│ └─charts
│ ├─long_ボックス下限
│ │ ├─XXX.png
│ │ ├─...
│ ├─...
│ └─short_高値反落
│ ├─XXX.png
│ ├─...
└─output
3-1.出来高・株価でスクリーニング
make_master.ipynbを作成します。このプログラムでは、銘柄一覧とyfinanceを使って出来高・株価でスクリーニングした結果を、「output」フォルダ配下にmaster.csvとして出力します。銘柄一覧のデータ量やスクリーニングの条件に依ると思いますが、実行時間は30分程度でした。
make_master.ipynbのコード
import pandas as pd
from datetime import datetime
import yfinance as yf
from tqdm import tqdm
# # 使用するデータに合わせて、以下のいずれかを実行する------------------------
# SBI証券でスクリーニングした結果を読み込む
df = pd.read_csv("./input/screener_sbi.csv", encoding="utf_8") #投資金額:10万~60万
# 東証上場銘柄一覧を読み込む
# df = pd.read_excel("./input/data_j.xls")
# df = df[df["市場・商品区分"].str.contains("内国株式", na=False)]
# ----------------------------------------------------------------------------
# 使用する列に絞る
df = df[["コード", "銘柄名"]]
df["yf_code"] = df["コード"].astype(str).str.zfill(4) + ".T"
# yfinance で情報取得(30min)
results = []
for _, row in tqdm(df.iterrows(), total=len(df), desc="処理中"):
code = row["yf_code"]
name = row["銘柄名"]
try:
ticker = yf.Ticker(code)
info = ticker.info
# 最新日付の株価(1日のデータ)
hist = ticker.history(period="1d")
result = {
"コード": code,
"銘柄名": name,
"始値": hist["Open"].iloc[-1] if not hist.empty else None,
"高値": hist["High"].iloc[-1] if not hist.empty else None,
"安値": hist["Low"].iloc[-1] if not hist.empty else None,
"終値": hist["Close"].iloc[-1] if not hist.empty else None,
"出来高": hist["Volume"].iloc[-1] if not hist.empty else None,
"カテゴリ": info.get("industry"),
"時価総額": info.get("marketCap"),
"決算日": info.get("nextEarningsDate"),
"配当金額": info.get("dividendRate"),
"権利付き最終日": info.get("exDividendDate"),
}
results.append(result)
except Exception as e:
print(f"{code} 取得失敗: {e}")
# 結果をデータフレームに変換して、投資候補の銘柄に絞る
# 投資金額:10万~60万、出来高:10万以上
result_df = pd.DataFrame(results)
result_df = result_df[
(result_df["安値"] >= 1000) &
(result_df["高値"] <= 6000) &
(result_df["出来高"] >= 100000)
]
# UNIXタイムスタンプを日付に変換
result_df["権利付き最終日"] = pd.to_datetime(
result_df["権利付き最終日"],
unit='s',
errors='coerce' # 無効な値は NaT に
)
# 日付のフォーマットを文字列にする
result_df["権利付き最終日"] = result_df["権利付き最終日"].dt.strftime('%Y-%m-%d')
# 表示と保存
result_df.to_csv("./output/master.csv", index=False)
print(len(result_df))
display(result_df.head())
3-2.チャートを取得した銘柄をOCRで一覧にする
screening.ipynbの前半を作成します。まずは、スクショに対して二値化などの前処理を行います。今回のように二値化した結果が、黒い背景に白字になる場合はOCRの精度が下がることがあるので、白黒反転させます。次に、チャートが表示される場所(座標)が固定されていることに留意して、チャート上部のヘッダー(銘柄コードと銘柄名の部分)を切り取ってノイズを除去します。その後、OCRで銘柄コードを読み取ります。銘柄コードを読み取る際は銘柄名はノイズとなるので、OCRを実行する際に数値のみ認識するよう設定します。
screening.ipynbの前半のコード
import os
import re
from tqdm import tqdm
import numpy as np
import pandas as pd
pd.set_option('display.max_rows', 100)
from datetime import date, timedelta
import yfinance as yf
import cv2
import pytesseract
from pathlib import Path
from PIL import Image, ImageEnhance, ImageFilter
# 関数を定義-----------------------------------------------
def rename_pngs(folder: Path):
"""
folder配下のpngを、input_{i}.pngにrenameする
"""
#不要なファイルがあれば削除
del_files = sorted(folder.glob("*Zone.Identifier"))
list(map(lambda f: f.unlink(), del_files))
# ファイル名をrename
png_files = sorted(folder.glob("*.png"))
list(map(
lambda pair: pair[0].rename(folder / f"input_{pair[1]}.png"),
zip(png_files, range(len(png_files)))
))
def trimming_pngs(target_folders: list[str]):
"""
folder配下の「long」「short」を含むフォルダ配下のpngを、白黒処理・トリミング処理をして、processed1_{i}.pngにrenameする
"""
# トリミング領域リスト(left, upper, right, lower)
crop_boxes = [
(25, 450, 325, 485), (355, 450, 645, 485), (675, 450, 975, 485),
(25, 680, 325, 715), (355, 680, 645, 715), (675, 680, 975, 715),
(25, 910, 325, 945), (355, 910, 645, 945), (675, 910, 975, 945),
(25, 1140, 325, 1175)
]
for folder in target_folders:
print(f"処理対象フォルダ: {folder.name}")
# input_*.png ファイルを順に処理
input_files = sorted(folder.glob("input_*.png"))
for input_path in tqdm(sorted(input_files), desc=f"{folder.name} の処理", unit="ファイル"):
stem = input_path.stem # 例: input_0
# 1. PIL → numpy配列 → グレースケール変換
im_pillow = Image.open(input_path).convert("RGB")
im_np = np.array(im_pillow)
gray = cv2.cvtColor(im_np, cv2.COLOR_RGB2GRAY)
# 2. Otsuによる白黒化
_, thresh_img = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# 3. 白黒反転
inverted = cv2.bitwise_not(thresh_img)
# 4. 一時的に保存(Pillowで開くため)
temp_path = folder / f"{stem}_inverted_temp.png"
cv2.imwrite(str(temp_path), inverted)
# 5. PILで開いてトリミング
image = Image.open(temp_path)
for i, crop_box in enumerate(crop_boxes):
cropped = image.crop(crop_box)
output_path = folder / f"{stem.replace('input_', 'processed1_')}_{str(i).zfill(3)}.png"
cropped.save(output_path)
# 一時ファイル削除
temp_path.unlink()
def extract_ocr_results_with_names(target_folders):
"""
OCRを実行し、フォルダ名とOCR結果リストをタプルで返す。
Parameters: target_folders (list[Path]): 処理対象フォルダ一覧
Returns: list[tuple[str, list[str]]]: フォルダ名とOCR結果のリスト
"""
# OCR設定:数字のみに限定
custom_config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789'
results = [] # [(フォルダ名, [ocr結果...]), ...]
for folder in target_folders:
png_files = sorted(folder.glob("processed1_*.png"))
result = [] # このフォルダ内のOCR結果(先頭4文字を格納用)
for png_path in tqdm(png_files, desc=f"{folder.name} の処理", unit="ファイル"):
stem = png_path.stem
# 画像読み込みと前処理
image = Image.open(png_path)
upscale_factor = 5
image = image.resize((image.width * upscale_factor, image.height * upscale_factor), Image.LANCZOS) # 解像度調整
image = image.filter(ImageFilter.MedianFilter(size=5)) # ノイズ除去
image = ImageEnhance.Contrast(image).enhance(3) # コントラスト強調
image = image.point(lambda x: 255 if x > 180 else 0).convert('1') # 二値化
# 補正済み画像の保存
processed_path = folder / f"{stem.replace('processed1_', 'processed2_')}.png"
image.save(processed_path)
# OCR実行・補正
text = pytesseract.image_to_string(image, config=custom_config)
text = text.replace('「', '').replace('」', '').replace('|', '').replace('[|]', '').replace(']', '')
text = re.sub(r'[ ]', '', text).strip()
# 最大4文字格納
result.append(text[:4] if len(text) >= 4 else text)
# このフォルダの結果を (フォルダ名, OCRリスト) で格納
result = [x for x in result if x.strip() != ""]
result = [x + ".T" for x in result]
results.append((folder.name, result))
return results
def display_result(i:int, results:list[tuple[str, list[str]]]):
"""
フォルダ名とOCR結果リストを表示する。
"""
folder_name, ocr_list = results[i]
master_df = pd.read_csv("./output/master.csv", dtype=str, encoding="utf_8") #投資金額:10万~60万・出来高10万以上
filtered_df = master_df[master_df["コード"].isin(ocr_list)].sort_values(by="出来高", ascending=False)
# 1. 欠損・空文字を "0" にし、"." 以降を削除 → カンマ削除 → 整数変換
filtered_df["出来高"] = (
filtered_df["出来高"]
.fillna("0") # None → "0"
.replace("", "0") # 空文字 → "0"
.str.split(".").str[0] # 小数点以降を削除
.str.replace(",", "", regex=False) # カンマ除去
.astype(int) # 整数に変換
)
# 2. 出来高の降順でソート
filtered_df = filtered_df.sort_values(by="出来高", ascending=False)
if len(filtered_df)!=0:
filtered_df.to_csv(f"./output/chart1_{folder_name}.csv", index=False)
print(f"\n{folder_name} :{len(filtered_df)}件")
display(filtered_df)
# -----------------------------------------------------------
# 「./input」配下の「long」または「short」を含むフォルダを取得
base_dir = Path("./input/charts")
target_folders = list(filter(lambda d: d.is_dir() and ('long' in d.name or 'short' in d.name), base_dir.iterdir()))
# input_{i}.pngにrenameする
list(map(rename_pngs, target_folders))
print("ファイル名の変更処理が完了しました。")
# input_{i}.pngに対して、白黒処理・トリミング処理をして、processed1_{i}.pngにrenameする
trimming_pngs(target_folders)
print("トリミング処理が完了しました。")
# processed1_{i}.pngに対して、OCRを実行
results = extract_ocr_results_with_names(target_folders)
print("OCR処理が完了しました。")
# OCRの結果を表示
for folder_name, ocr_list in results:
print(f"\n{folder_name} ({len(ocr_list)}件):\n{ocr_list}")
print("すべての処理が完了しました。")
3-3.3-1と3-2のデータを突合させる
screening.ipynbの後半を作成します。3-1と3-2のデータを突合させて両方に含まれる銘柄コードのみ抜粋します。ついでに、直近2週間のチャートの上ヒゲと下ヒゲが長すぎる銘柄を(値動きに安定感がないため)、投資候補から外します。
screening.ipynbの後半のコード
# 関数を定義-----------------------------------------------
def display_extract_code(i:int, ocr_results:list[tuple[str, list[str]]]):
"""
髭の長さで銘柄を絞り込み、フォルダ名とOCR結果リストを表示する。
"""
folder_name, ocr_list = ocr_results[i]
master_df = pd.read_csv("./output/master.csv", dtype=str, encoding="utf_8") #投資金額:10万~60万・出来高10万以上
filtered_df = master_df[master_df["コード"].isin(ocr_list)].sort_values(by="出来高", ascending=False)
if filtered_df.empty:
print(f"[{folder_name}] 該当する銘柄が master.csv に存在しません。スキップします。")
else:
# yfinance で情報取得
yf_results = []
for _, row in tqdm(filtered_df.iterrows(), total=len(filtered_df), desc="処理中"):
code = row["コード"]
name = row["銘柄名"]
try:
ticker = yf.Ticker(code)
info = ticker.info
# 直近2週間分の株価
today_date = date.today()
two_weeks_ago_date = today_date - timedelta(days=14)
today = today_date.strftime("%Y-%m-%d")
two_weeks_ago = two_weeks_ago_date.strftime("%Y-%m-%d")
hist = yf.download(tickers=code, start=two_weeks_ago, end=today, auto_adjust=True)
if isinstance(hist.columns, pd.MultiIndex):
hist.columns = hist.columns.get_level_values(0)
if hist.empty:
continue
hist = hist.reset_index() # 日付を列に
hist["コード"] = code
hist["銘柄名"] = name
hist["カテゴリ"] = info.get("industry")
hist["時価総額"] = info.get("marketCap")
hist["決算日"] = info.get("nextEarningsDate")
hist["配当金額"] = info.get("dividendRate")
hist["権利付き最終日"] = info.get("exDividendDate")
yf_results.append(hist)
except Exception as e:
print(f"{code} 取得失敗: {e}")
# 全ての履歴を結合して整形
df = pd.concat(yf_results, ignore_index=True)
df = df.rename(columns={
"Date": "日付",
"Open": "始値",
"High": "高値",
"Low": "安値",
"Close": "終値",
"Volume": "出来高"
})
df = df[['コード', '銘柄名', '日付', '高値', '安値', '始値', '終値', '出来高', 'カテゴリ', '時価総額', '決算日', '配当金額', '権利付き最終日']]
df['始値'] = pd.to_numeric(df['始値'], errors='coerce')
df['終値'] = pd.to_numeric(df['終値'], errors='coerce')
df['高値'] = pd.to_numeric(df['高値'], errors='coerce')
df['安値'] = pd.to_numeric(df['高値'], errors='coerce')
# 上ヒゲの長さ: 高値 - max(始値, 終値)
df['上ヒゲ'] = df['高値'] - df[['始値', '終値']].max(axis=1)
# 下ヒゲの長さ: min(始値, 終値) - 安値
df['下ヒゲ'] = df[['始値', '終値']].min(axis=1) - df['安値']
# 終値と始値の差の絶対値
df['実体'] = (df['終値'] - df['始値']).abs()
# 投資対象か否か判定する材料
df['投資flg'] = df.apply(
lambda row: 0 if (row['上ヒゲ'] - row['実体'] > 0) and (row['下ヒゲ'] - row['実体'] > 0) else 1,
axis=1
)
# 投資対象か否か判定する
# 上下のひげが「実体」より短いものが6割以上ある銘柄を投資対象とする
counts = df.groupby('コード')['投資flg'].sum().reset_index()
valid_codes = counts[counts['投資flg'] >= 6]['コード']
df = df[df['コード'].isin(valid_codes)]
# 欠損・空文字を "0" にし、"." 以降を削除 → カンマ削除 → 整数変換
df["出来高"] = (
df["出来高"]
.fillna("0") # None → "0"
.replace("", "0") # 空文字 → "0"
.astype(str) # 念のため文字列化
.str.replace(",", "", regex=False) # カンマ削除
.str.replace(r"\..*$", "", regex=True) # 小数点以降を削除(例:123.45 → 123)
.astype(int) # 整数に変換
)
# UNIXタイムスタンプを日付に変換
df["権利付き最終日"] = pd.to_datetime(
df["権利付き最終日"],
unit='s',
errors='coerce' # 無効な値は NaT に
)
df["権利付き最終日"] = df["権利付き最終日"].dt.strftime('%Y-%m-%d')
# 最新の日付の情報のみ抽出して、出来高の降順でソート
df = df[df["日付"] == df["日付"].max()]
df = df.sort_values(by="出来高", ascending=False)
df = df.drop(columns=['上ヒゲ', '下ヒゲ', '実体', '投資flg'])
# 出力と表示
if len(df)!=0:
df.to_csv(f"./output/chart2_{folder_name}.csv", index=False)
# 「コード」列だけをコピーして加工(元のdfは変更しない)
# 加工後のコード列のみを出力(ヘッダーなし)
code_series = df['コード'].str.replace(r"\.T$", "", regex=True)
code_series.to_frame().to_csv(f"./output/MATSUI_chart2_{folder_name}.csv", index=False, header=False)
print(f"\n{folder_name} :{len(df)}件")
display(df)
# -----------------------------------------------------------
display_extract_code(0, results)
display_extract_code(1, results)
# ...チャートのスクショを格納しているフォルダの数だけ繰り返す
ここまでの進捗
4.目視による厳選
松井証券のツールを使用すると、データをインポートして、複数のチャートを少ないクリック数で見れます。また、日足・週足などの時間軸を変えられる点も良いです。ツールの使用には証券口座の開設が必要です。
4-1.スクリーニングデータの読み込み
松井証券のツールに、スクリーニングしたデータを読み込ませます。
4-2.チャート形状を確認
損するリスクが低そうな銘柄をチャートの形状をもとに、厳選します。
4-3.当日朝の板情報を確認
当日朝の板情報から売り優勢か買い優勢か見極めて、投資する銘柄をさらに絞り込みます。
選定完了!
おわりに
このプログラムのおかげで、数時間かけていた銘柄の選定作業が、45分程度(=スクショ取得15分 + チャート形状の目視による絞り込み15分 + 当日朝の板情報による絞り込み15分;プログラミング実行中の待ち時間は含まない)まで抑えることができました。今のところ選定した銘柄で利益を出すことができています。今回した記事が、皆さんの銘柄選定作業の一助となれば幸いです。(プログラムなどの紹介内容の利用に伴う責任は、こちらでは一切負いません。投資は自己責任でお願いします。)
Discussion