自分AI上司!!できた!!VS Codeで回した総当たり検証、ヒートマップまで出した!!



声量を落としなさい。提出物は“興奮”ではなく“結果”です。何が出た?



移動平均クロスのパラメータを総当たりした。トヨタ(7203.T)で。で、ベストがこれ。
短期20日/長期45日:+35.02%
短期20日/長期45日:+35.02%



35%だぞ!?もう勝ち確じゃん!!俺たち最強のタッグ!!



……では、現実に戻します。
その35.02%は「黄金」ではありません。ほぼ確実に“過学習”です。
その35.02%は「黄金」ではありません。ほぼ確実に“過学習”です。



またそれ!?でも今回はちゃんとプログラムで回したんだよ!?



だから危険なのです。
人間の勘ではなく、計算で“それっぽい答え”が出ると、脳が簡単に騙されます。
まず、あなたが回したルールを言語化しなさい。
人間の勘ではなく、計算で“それっぽい答え”が出ると、脳が簡単に騙されます。
まず、あなたが回したルールを言語化しなさい。
目次
そもそも今回の「ルール」は何か?



えっと……移動平均のクロス。短期と長期を比べて、短期が上なら買い、下なら売り。



正解。もっと正確に言いなさい。
今回のルールはこうです。
今回のルールはこうです。



- 短期SMA(Short_SMA):直近 short_window 日の終値の平均
- 長期SMA(Long_SMA):直近 long_window 日の終値の平均
- Short_SMA > Long_SMA の間だけ買いポジション(Position=1)
- それ以外はノーポジ(Position=0)
つまり「上昇トレンドっぽい間だけ市場に参加する」戦略です。



うん、まさにそれ。見た目はめっちゃ正しそうなんだよな。



“見た目が正しい”ほど危険です。次。コードで何をしているか、要点だけ説明します。
プログラムの流れ(あなたの検証はこう動いている)



まずデータを取る部分。
def fetch_price_data(ticker: str, years: int = 2) -> pd.DataFrame:
end = datetime.today()
start = end - timedelta(days=365 * years)
data = yf.download(ticker, start=start, end=end, interval="1d", auto_adjust=True)
if data.empty:
raise RuntimeError("No data fetched. Check ticker or network access.")
return data


ここでは、トヨタの株価を日足で取得しています。
auto_adjust=True なので株価は調整済み。検証の前提としては妥当です。


次に、戦略の心臓部。
df["Short_SMA"] = prices.rolling(window=short_window).mean()
df["Long_SMA"] = prices.rolling(window=long_window).mean()
df["Position"] = (df["Short_SMA"] > df["Long_SMA"]).astype(int)
df["Return"] = prices.pct_change().shift(-1)
df["Strategy_Return"] = df["Position"] * df["Return"]
df = df.dropna()
cumulative_return = (1 + df["Strategy_Return"]).prod() - 1


重要ポイントは2つ。



1) Positionは“その日”のSMA関係で決まる
ShortがLongより上なら1(持つ)、下なら0(持たない)。
買う/売るの瞬間ではなく、「上にいる間はずっと持つ」設計です。
ShortがLongより上なら1(持つ)、下なら0(持たない)。
買う/売るの瞬間ではなく、「上にいる間はずっと持つ」設計です。



2) Returnが翌日
つまり「今日の判定(Position)で、明日どう動いたか」を利益として計上します。
雑に言えば「判断→翌日反映」の簡易モデルです。
pct_change().shift(-1) によって“翌日の値動き”を現在行にずらしています。つまり「今日の判定(Position)で、明日どう動いたか」を利益として計上します。
雑に言えば「判断→翌日反映」の簡易モデルです。



なるほど。未来見てないようにしてるってことか。



そう。そこは最低限クリアしています。
しかし、あなたが喜んでいるのはそこではない。
あなたは“最適なパラメータ”に酔っています。
しかし、あなたが喜んでいるのはそこではない。
あなたは“最適なパラメータ”に酔っています。
25日と40日の意味(=パラメータは「市場の時間」を表す)



ところでAI上司。
たとえば短期25日、長期40日ってさ。何の意味があるの?ただの数字遊び?
たとえば短期25日、長期40日ってさ。何の意味があるの?ただの数字遊び?



良い質問です。数字遊びにしないために、意味を言語化します。



まず、
25 と 40 は「移動平均を取る期間(日数)」です。
- 短期25日:直近25営業日あたりの平均=だいたい“1か月”の空気
- 長期40日:直近40営業日あたりの平均=だいたい“2か月弱”の空気
つまり“最近の勢い”が“もう少し長い基調”を上回ったかどうかを見ています。



この2つを比べる意味は単純です。
- 短期が上:最近の価格が上向き → 上昇トレンドっぽい
- 短期が下:最近の価格が弱い → 下落/停滞っぽい
ただし、移動平均は遅い。遅いものは「上がってから買い、下がってから売る」傾向になります。



つまり25/40って“時間の切り取り方”なんだな。市場をどう見るか、みたいな。



その通り。パラメータは“世界の見方”です。
だから、適当にいじると“世界そのものがあなたに都合よく歪みます”。
それが過学習です。
だから、適当にいじると“世界そのものがあなたに都合よく歪みます”。
それが過学習です。
歓喜:ベストが出た瞬間、人間は簡単に壊れる



でもさ、現にベストが出たわけじゃん。
短期20/長期45で +35.02%。
ヒートマップでもその辺が強かった。
これって、意味ある“傾向”なんじゃないの?
短期20/長期45で +35.02%。
ヒートマップでもその辺が強かった。
これって、意味ある“傾向”なんじゃないの?





傾向ではなく、当選番号です。



当選番号!?ひど。



あなたは「良いパラメータを見つけた」と思っている。
実態は「試した範囲の中で一番よく見えるものを拾った」だけです。
拾った瞬間、あなたの脳はこう囁く。
“これが答えだ”と。
実態は「試した範囲の中で一番よく見えるものを拾った」だけです。
拾った瞬間、あなたの脳はこう囁く。
“これが答えだ”と。



……囁いた。めっちゃ囁いた。退職代行までチラついた。



人間は数字に弱い。特に、都合のいい数字に。
解説:過学習(オーバーフィッティング)とは何か



でも結局、過学習って何がダメなの?
勝ってるなら良くない?
勝ってるなら良くない?



過学習とは、
過去データの“偶然の形”まで学習してしまい、未来に通用しない状態です。
過去データの“偶然の形”まで学習してしまい、未来に通用しない状態です。



例え話。
あなたが「過去2年分の天気図」を完璧に暗記したとします。
では、明日の天気は当てられますか?
あなたが「過去2年分の天気図」を完璧に暗記したとします。
では、明日の天気は当てられますか?



当たらない。過去を覚えても未来は変わる。



そう。
ところがあなたは今、こう言っているのと同じです。
「天気図の覚え方を総当たりして、一番当たった暗記法を見つけた。これで未来も当たるはず!」
ところがあなたは今、こう言っているのと同じです。
「天気図の覚え方を総当たりして、一番当たった暗記法を見つけた。これで未来も当たるはず!」



うわ、急に俺がバカみたいに見える……。



それが正しい感覚です。



シストレで過学習が最悪なのは、
“うまくいっているように見える”ことです。
ヒートマップが綺麗。最高成績が派手。
人間はそこで脳汁を出し、検証を止めます。
そして未来で破裂します。
“うまくいっているように見える”ことです。
ヒートマップが綺麗。最高成績が派手。
人間はそこで脳汁を出し、検証を止めます。
そして未来で破裂します。
結び:絶望する部下、締めるAI上司



じゃあ、この+35.02%は……結局、何の意味もないの?
俺、またメッキ拾っただけ?
俺、またメッキ拾っただけ?



意味はあります。



あなたは今日、最重要の教訓を得ました。
「数字は嘘をつかないが、詐欺師のように振る舞う」
「数字は嘘をつかないが、詐欺師のように振る舞う」



悔しいのに名言っぽくて腹立つ……。



次回。
“黄金の見つけ方”ではなく、黄金かどうかの検査方法を教えます。
データを分ける。期間をずらす。銘柄を増やす。コストも入れる。
あなたがやるのは、勝ち探しではなく、嘘の排除です。
“黄金の見つけ方”ではなく、黄金かどうかの検査方法を教えます。
データを分ける。期間をずらす。銘柄を増やす。コストも入れる。
あなたがやるのは、勝ち探しではなく、嘘の排除です。



うわ……また俺の夢が削られる回だ。



当然です。あなたの夢はノイズです。
次回:「本当に使えるルールの探し方」――過去に勝つな、未来に耐えろ。
付録



確かにcodexはすごかった。以下のプロンプトの後、エラーをそのまま貼るのを数回で動くコードが完成してしまった。
# Pythonを使用して、日本株の移動平均線クロス戦略のパラメーターを総当たり(ブルートフォース)で検証するコードを書いてください。
# 仕様:
1. `yfinance`を使用して、トヨタ自動車(7203.T)の過去2年間の日足データを取得する。
2. 短期移動平均線(Short_SMA)と長期移動平均線(Long_SMA)の組み合わせをすべて試す。
- Short_SMAの範囲:2日から20日まで(1日刻み)
- Long_SMAの範囲:21日から60日まで(1日刻み)
3. 各組み合わせについて、以下のシミュレーションを行う。
- Short_SMA > Long_SMA ならば「買い保持(1)」
- それ以外は「ノーポジ(0)」
- 翌日のリターンを計算し、累積リターンを算出する。
4. すべての結果をリストに格納し、最終的な累積リターンが「最も高かった組み合わせ」と「最も低かった組み合わせ」をコンソールに表示する。
5. 最後に、全パターンのリターンをヒートマップ(seabornを使用)で可視化するコードも含めてください。


以下、プログラムの全体
matplotlib, pandas, seaborn, yfinanceはpip installする必要があった。
import itertools
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import yfinance as yf
def fetch_price_data(ticker: str, years: int = 2) -> pd.DataFrame:
end = datetime.today()
start = end - timedelta(days=365 * years)
data = yf.download(ticker, start=start, end=end, interval="1d", auto_adjust=True)
if data.empty:
raise RuntimeError("No data fetched. Check ticker or network access.")
return data
def extract_close_series(data: pd.DataFrame, ticker: str) -> pd.Series:
if isinstance(data, pd.Series):
return data.rename("Close")
if isinstance(data.columns, pd.MultiIndex):
level0 = data.columns.get_level_values(0)
level1 = data.columns.get_level_values(1)
if "Close" in level0:
close_df = data["Close"]
elif "Close" in level1:
close_df = data.xs("Close", axis=1, level=1)
else:
close_df = None
if close_df is not None:
if isinstance(close_df, pd.DataFrame):
if ticker in close_df.columns:
return close_df[ticker].copy().rename("Close")
return close_df.iloc[:, 0].copy().rename("Close")
return close_df.copy().rename("Close")
if "Close" in data.columns:
return data["Close"].copy()
if data.shape[1] == 1:
return data.iloc[:, 0].copy().rename("Close")
raise RuntimeError("Could not locate Close column in downloaded data.")
def simulate_sma_cross(
data: pd.DataFrame, short_window: int, long_window: int, ticker: str
) -> float:
prices = extract_close_series(data, ticker)
df = pd.DataFrame({"Close": prices})
df["Short_SMA"] = prices.rolling(window=short_window).mean()
df["Long_SMA"] = prices.rolling(window=long_window).mean()
df["Position"] = (df["Short_SMA"] > df["Long_SMA"]).astype(int)
df["Return"] = prices.pct_change().shift(-1)
df["Strategy_Return"] = df["Position"] * df["Return"]
df = df.dropna()
cumulative_return = (1 + df["Strategy_Return"]).prod() - 1
return cumulative_return
def main() -> None:
ticker = "7203.T"
data = fetch_price_data(ticker)
output_csv = "ma_cross_results.csv"
output_png = "ma_cross_heatmap.png"
short_range = range(2, 21)
long_range = range(21, 61)
results = []
for short_window, long_window in itertools.product(short_range, long_range):
if short_window >= long_window:
continue
cum_ret = simulate_sma_cross(data, short_window, long_window, ticker)
results.append(
{
"Short_SMA": short_window,
"Long_SMA": long_window,
"Cumulative_Return": cum_ret,
}
)
results_df = pd.DataFrame(results)
results_df.to_csv(output_csv, index=False)
best_row = results_df.loc[results_df["Cumulative_Return"].idxmax()]
worst_row = results_df.loc[results_df["Cumulative_Return"].idxmin()]
print("Best combination:")
print(
f"Short_SMA={int(best_row['Short_SMA'])}, Long_SMA={int(best_row['Long_SMA'])}, "
f"Cumulative_Return={best_row['Cumulative_Return']:.2%}"
)
print("Worst combination:")
print(
f"Short_SMA={int(worst_row['Short_SMA'])}, Long_SMA={int(worst_row['Long_SMA'])}, "
f"Cumulative_Return={worst_row['Cumulative_Return']:.2%}"
)
pivot = results_df.pivot(index="Short_SMA", columns="Long_SMA", values="Cumulative_Return")
plt.figure(figsize=(14, 8))
sns.heatmap(pivot, annot=False, cmap="RdYlGn", center=0)
plt.title(f"SMA Cross Strategy Cumulative Returns: {ticker}")
plt.xlabel("Long_SMA")
plt.ylabel("Short_SMA")
plt.tight_layout()
plt.savefig(output_png, dpi=150)
if __name__ == "__main__":
main()






コメント