※本記事のコードや情報は学習・検証目的です。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。記事末尾に関連サービスの紹介(アフィリエイト)があります。
先に結論です。Vibe Codingで自動売買BOTの雛形を作るのは有効です。そのコードを読まずに実弾を入れるのは、ほぼ確実に事故ります。ChatGPTやClaudeは「よくある移動平均クロス」を数秒で出します。よくある実装には、当日終値で判定して同じ足のリターンまで取るlookahead、手数料ゼロ、APIキーの直書きがセットで混ざります。
Andrej Karpathyが2025年2月にXで広めた「Vibe Coding」は、本来「生成されたコードを読まず、動くかどうかだけ見て進める」スタイルです。Martin Fowlerも、使い捨てで閉じた用途向けだと整理しています。資金と証券口座が乗る自動売買は、その対極です。
本記事では、ChatGPT/Claudeへの依頼文、返ってきやすい最小BOT、そしてAIが出したコードを機械的に落とす検査スクリプトまでを一続きで示します。ここが他の「プロンプト集」記事との違いです。プロンプトを良くしても、レビューを自動化しない限り同じバグが残ります。
📘 外部参考:Vibe Coding(Martin Fowler) / yfinance(GitHub) / yfinance(PyPI)
Vibe Codingとは何か(自動売買に使う範囲)
Vibe Codingは、自然言語で「欲しい動き」を伝え、LLMにコードを書かせ、エラーを貼り付けて直させるループです。Karpathy自身の表現は「see stuff, say stuff, run stuff, copy paste stuff」。Simon Willisonが強調したのは、「AI支援コーディング」と「コードを理解せず受け入れるVibe Coding」は別物だという点です。
アルゴトレードは仕様が明確に見えやすい領域です。「ゴールデンクロスで買い、デッドクロスで売る」は一文で書けます。だからAIは得意です。裏を返すと、仕様の穴も一文では書けません。判定足と約定足、休日、調整済み価格、手数料、最大建玉。ここをプロンプトに書かないと、AIは教科書どおりのゼロコスト実装を返します。
向く作業・向かない作業
| 作業 | Vibe Coding | 理由 |
|---|---|---|
| 指標計算の雛形(SMA、RSI) | 向く | 入出力が明確。テストしやすい |
| CSVの整形、ログのパース | 向く | 使い捨てでも実害が小さい |
| バックテストの骨格 | 条件付き | lookahead検査を自分で入れるなら可 |
| 発注API・資金移動 | 向かない | 鍵・約定・規制。一行の誤解が資金に直結 |
| ニュース解釈・板の読み | 向かない | 暗黙知が多く、再現テストが難しい |
「とりあえず動くBOT」は、上表の上3行までです。kabuステーションAPIで実発注する段階は、最小構成BOTの記事側の話として分けてください。Vibe Codingの成果物は、発注モジュールに渡す「シグナル関数」までに止めるのが安全です。
ChatGPTとClaudeへの依頼文(これだけは先に書く)
曖昧な「移動平均クロスのBOTを書いて」は、ほぼ必ず終値判定・終値リターンのリーク実装になります。依頼文に制約を4行入れます。
日本株の日次データで、5日SMAと25日SMAのクロスを検証するPythonを書いてください。
制約:
1. yfinanceは auto_adjust=True, progress=False。MultiIndexなら列を平坦化する
2. シグナルは当日終値で計算し、必ず shift(1) して翌営業日のリターンに掛ける
3. ポジション変更日に片道手数料 0.1% を引く
4. APIキーやWebhook URLはコードに書かず、環境変数から読む
5. 未来参照(shift(-1) や iloc[i+1] で当日判定)を使わない
出力: データ取得、シグナル、コスト込みバックテスト、最新シグナル表示の4関数
この制約を入れても、モデルはたまにshift(-1)を残します。だから次節の検査コードが必要です。プロンプトは入口、検査は出口です。
返ってきやすい「危険な雛形」
制約なしで頼むと、だいたい次のようなコードが返ります。動きます。成績も良く見えます。本番には使えません。
# NG例: AIがよく出すパターン(このまま使わない)
import yfinance as yf
df = yf.download("7203.T", period="2y") # MultiIndexのまま / 進捗バーあり
df["sma5"] = df["Close"].rolling(5).mean()
df["sma25"] = df["Close"].rolling(25).mean()
df["signal"] = (df["sma5"] > df["sma25"]).astype(int)
df["ret"] = df["Close"].pct_change()
df["st"] = df["signal"] * df["ret"] # 当日判定 × 当日リターン = lookahead
print((1 + df["st"].dropna()).prod())
signalをずらしていないので、クロスした当日の終値リターンまで取れます。実運用では、終値が確定した時点ではその足のリターンはもう取れません。この1行で、バックテストの勝率が数ポイント底上げされます。
レビュー済みの最小構成(シグナルまで)
トヨタ(7203.T)で、翌バーリターン・コスト・最新シグナルだけを出す最小構成です。発注はしません。ここまでを「Vibe Codingの合格ライン」にしてください。
import os
from dataclasses import dataclass
import numpy as np
import pandas as pd
import yfinance as yf
@dataclass
class Config:
ticker: str = "7203.T"
fast: int = 5
slow: int = 25
fee_one_way: float = 0.001 # 0.1%
start: str = "2019-01-01"
def fetch(cfg: Config) -> pd.DataFrame:
raw = yf.download(
cfg.ticker,
start=cfg.start,
auto_adjust=True,
progress=False,
threads=False,
)
if raw.empty:
raise RuntimeError(f"no data: {cfg.ticker}")
if isinstance(raw.columns, pd.MultiIndex):
raw.columns = raw.columns.get_level_values(0)
df = raw[["Open", "High", "Low", "Close", "Volume"]].copy()
df.index = pd.to_datetime(df.index).tz_localize(None)
return df.sort_index()
def add_signal(df: pd.DataFrame, cfg: Config) -> pd.DataFrame:
out = df.copy()
out["sma_f"] = out["Close"].rolling(cfg.fast, min_periods=cfg.slow).mean()
out["sma_s"] = out["Close"].rolling(cfg.slow, min_periods=cfg.slow).mean()
raw = (out["sma_f"] > out["sma_s"]).astype(int)
out["signal"] = raw.shift(1) # 翌営業日から有効
out["ret"] = out["Close"].pct_change()
return out
def backtest(df: pd.DataFrame, cfg: Config) -> pd.DataFrame:
out = df.copy()
pos = out["signal"].fillna(0)
turnover = pos.diff().abs().fillna(pos.abs())
out["gross"] = pos * out["ret"]
out["net"] = out["gross"] - turnover * cfg.fee_one_way
out["equity"] = (1 + out["net"].fillna(0)).cumprod()
return out
def latest_action(df: pd.DataFrame) -> str:
last = df.dropna(subset=["signal"]).iloc[-1]
prev = df.dropna(subset=["signal"]).iloc[-2]
if last["signal"] > prev["signal"]:
return "BUY"
if last["signal"] < prev["signal"]:
return "SELL"
return "HOLD"
if __name__ == "__main__":
cfg = Config()
bt = backtest(add_signal(fetch(cfg), cfg), cfg)
print("net", round(float(bt["equity"].iloc[-1] - 1), 4))
print("action", latest_action(bt))
print("webhook set?", bool(os.getenv("DISCORD_WEBHOOK_URL")))
ポイントはraw.shift(1)です。当日終値でクロスを見ても、ポジションは翌営業日です。最新シグナルの表示も「今日の終値が確定したあとの翌日アクション」であり、ザラ場中の即時発注ではありません。日次BOTをザラ場に接続すると、また別のlookaheadが起きます。
AIが出したコードを落とす検査スクリプト
独自の切り口はここです。生成コードを目視する前に、静的チェックと数値チェックを通します。目視は疲れるので、機械が先に赤を出します。
import ast
import pathlib
import re
import pandas as pd
FORBIDDEN = [
r"api_key\s*=\s*['\"]sk-",
r"APP_PASSWORD\s*=",
r"webhook.*/[\w-]{20,}",
r"shift\(\s*-1\s*\)",
]
def scan_source(path: str) -> list[str]:
text = pathlib.Path(path).read_text(encoding="utf-8")
hits = []
for pat in FORBIDDEN:
if re.search(pat, text):
hits.append(f"forbidden pattern: {pat}")
tree = ast.parse(text)
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
if "sk-" in node.value or "xoxb-" in node.value:
hits.append("credential-like string literal")
return hits
def numeric_leak_test(bt: pd.DataFrame) -> dict:
"""同日シグナル×同日リターンと、shift済みを比べる。差が大きければリーク疑い。"""
raw = (bt["sma_f"] > bt["sma_s"]).astype(int)
leak = (raw * bt["ret"]).dropna()
safe = (raw.shift(1) * bt["ret"]).dropna()
return {
"leak_total": round(float((1 + leak).prod() - 1), 4),
"safe_total": round(float((1 + safe).prod() - 1), 4),
"gap": round(float((1 + leak).prod() - (1 + safe).prod()), 4),
}
def cost_sensitivity(bt: pd.DataFrame, fees=(0.0, 0.001, 0.002)) -> pd.DataFrame:
pos = bt["signal"].fillna(0)
turn = pos.diff().abs().fillna(pos.abs())
rows = []
for f in fees:
net = pos * bt["ret"] - turn * f
eq = (1 + net.fillna(0)).cumprod()
rows.append({"fee": f, "total": round(float(eq.iloc[-1] - 1), 4)})
return pd.DataFrame(rows)
gapがプラスに大きく出るほど、lookaheadが成績を水増ししています。手数料0と0.2%で符号が逆転するなら、そのクロスは取引コストに負けています。移動平均クロスは売買が多いので、この逆転は珍しくありません。詳細なクロス検証は移動平均クロスのバックテスト、Backtesting.py入門も参照してください。
検査の合格ライン
| 項目 | 不合格 | 合格の目安 |
|---|---|---|
| ソース | APIキー直書き、shift(-1) | 環境変数のみ。未来シフトなし |
| リーク差 gap | 0.2以上(期間による) | safe側だけを採用。leak側は捨てる |
| コスト | fee=0だけ黒字 | 自分の口座料率でも符号が残る |
| パラメータ | 5/25だけ突出 | 4/20と6/30でも形が残る |
| 発注 | 検査前にAPI接続 | シグナル関数が合格してから接続 |
Vibe Codingの速度は、この表をスキップしたときに最大化します。資金が乗ると、その速度が最大ドローダウンになります。長期で回す前提なら、自作アルゴの長期運用ロードマップの撤退ラインを先に文章化してください。
ChatGPTとClaudeの使い分け
どちらでも雛形は出ます。差が出るのは、制約を守らせる粘りと、長いファイルの修正です。実務では次のように分けると無駄が少ないです。
- ChatGPT:短い関数、正規表現、エラーメッセージの解読。会話を細かく切る。
- Claude:ファイルを複数渡して「lookaheadが残っている関数を列挙して」。レビュー役。
- どちらでもやらない:「勝てる戦略を考えて」。「エッジの有無」はモデルの得意分野ではありません。ルールは自分が決め、コードだけ生成させる。
モデルに戦略を考えさせると、よく知られたクロス・RSI・ボリンジャーの組み合わせが返ります。公開情報の寄せ集めなので、コスト後に残る保証はありません。戦略の仮説は自分の相場観か、書籍の型(株分析を学ぶ本3選)から持ってきてください。
シークレットの扱い
生成AIのチャット欄にAPIキーを貼らないでください。ログに残ります。ローカルの.envと、リポジトリの.gitignoreが最低ラインです。
# .env(gitに入れない)
KABU_API_PASSWORD=...
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
# 読み込み
from dotenv import load_dotenv
load_dotenv()
password = os.environ["KABU_API_PASSWORD"]
チャットに「このエラーを見て」と貼るときも、URLのトークン部分は伏せます。Vibe Codingで事故るパターンの上位は、戦略バグより資格情報の漏洩です。
実弾の前にやること(ペーパー→少額)
検査に通ったシグナル関数でも、直後に本番資金は入れません。順序を固定します。
- ペーパー30営業日。毎日、前日シグナルと当日始値をログに残す。終値判定なのにザラ場で出していないかを見る。
- 少額10〜20万円、最大DD15%で止める。コードのバグと相場の不運を切り分ける金額。
- 約定ログとバックテストを突合。滑りが片道0.1%を超える日が続くなら、日次クロスをザラ場に無理接続している。
- 停止スイッチ。例外で落ちたら注文しない。落ちたことを通知する。動いている前提で放置しない。
AIは「動くコード」は出します。「止めるコード」は、頼まないと出ません。kill switchと通知はプロンプトの制約5行目に入れてください。
def kill_switch(equity: pd.Series, max_dd: float = -0.15) -> bool:
dd = equity / equity.cummax() - 1.0
return bool(dd.iloc[-1] <= max_dd)
def notify(msg: str) -> None:
url = os.getenv("DISCORD_WEBHOOK_URL")
if not url:
print(msg)
return
import requests
requests.post(url, json={"content": msg}, timeout=10)
リークがあると数字がどれだけ盛れるか
抽象論で終わらせないため、同じトヨタ株・同じ5/25クロスで、リークありとshift済みを並べます。期間やデータ源で数値は変わりますが、符号が同じでも水準が違うことさえ分かれば十分です。AIの雛形を採用する前に、この2列を必ず出してください。
def compare_leak(df: pd.DataFrame, fast=5, slow=25) -> pd.DataFrame:
sma_f = df["Close"].rolling(fast).mean()
sma_s = df["Close"].rolling(slow).mean()
raw = (sma_f > sma_s).astype(int)
ret = df["Close"].pct_change()
leak = raw * ret
safe = raw.shift(1) * ret
def stats(x):
x = x.dropna()
eq = (1 + x).cumprod()
dd = eq / eq.cummax() - 1
return {
"total": round(float(eq.iloc[-1] - 1), 4),
"max_dd": round(float(dd.min()), 4),
"ann_vol": round(float(x.std() * (245 ** 0.5)), 4),
}
return pd.DataFrame([
{"rule": "same_bar_leak", **stats(leak)},
{"rule": "next_bar_safe", **stats(safe)},
])
リーク側は「終値が分かった世界線」の成績です。実運用はsafe側にしか住めません。差が小さいなら、その戦略はクロス頻度が低く、判定ズレの影響が小さいだけです。差が大きいなら、AIの雛形をそのまま信じた瞬間に過大評価しています。表をノートに残し、チャットのログより先に数字を信頼してください。
さらに、safe側に片道0.1%を載せると、クロス戦略はよく赤に転じます。ここまで出してから「BOTを24時間回す」話に進んでください。環境構築はPython環境構築ガイド、常時起動は準備カテゴリのVPS記事の役割です。
プロンプトを一度で終わらせない
最初の返答で検査に落ちたら、エラーではなく検査結果の表を貼って再依頼します。「直して」だけだと、モデルは別の場所に同じリークを残します。
- 「numeric_leak_testのgapが0.31でした。raw_signalとretを同じ行で掛けている箇所を特定し、shift(1)に直したパッチだけ出してください」
- 「手数料0.2%でtotalが負です。売買回数を減らす変更はせず、先にコスト計算がturnoverに乗っているか確認してください」
- 「テストを追加してください。signal[t]がret[t]に掛けられていたら失敗するassert」
3回直してもgapが残るなら、そのチャットは捨てて、本記事の最小構成から差分実装した方が速いです。Vibe Codingは短いループが前提で、壊れた文脈を延々と引っ張ると、関係ない関数まで書き換えられます。
よくある失敗
yfinanceのMultiIndexを無視する
最近のyfinanceは単一銘柄でも列がMultiIndexになることがあります。df["Close"]がDataFrameのままだと、rollingの結果がずれます。get_level_values(0)で平坦化する処理は、プロンプトに必ず書いてください。
auto_adjustを指定しない
現行のデフォルトはauto_adjust=True(調整済みOHLC、Adj Close列なし)です。古い記事のAdj Close指定はKeyErrorになります。バックテストのリターン計算なら調整済みで統一します。
「勝率87%のスクショ」を信じる
生成AIブームの体験談には、検証不能な成績が混ざります。自分のコードで、コスト込み・期間分割(前半/後半)が出ない数字は、再現できない宣伝です。
まとめ:雛形はAI、合格判定は自分の検査
Vibe Codingは、自動売買の「最初の200行」を短縮します。短縮してよいのは指標とバックテストの骨格までです。Karpathyの「コードを読まない」定義を、資金口座に適用しないでください。
やることは3つです。制約付きで依頼する。上の最小構成を基準に差分を見る。scan_sourceとnumeric_leak_testを通す。通ってからペーパー、そのあと少額です。
次に手を動かすなら、シグナル関数を移動平均クロスの検証で厚くするか、発注だけkabuステーションAPIの最小構成に分けて接続してください。混ぜると、どちらが壊れているか分からなくなります。

