AIツクリラボ
・AI日本株自動売買実験・21分で読めます・🔵 実装記録

AI自動売買の成績が落ちた原因は「判断のブレ」だった。2回連続の確認で発注する仕組みを入れた話

この記事で分かること
LLMの判断を1日に何度も繰り返すと、同じ入力でも答えが揺れ、そのうち1回でも売買が出れば執行される構造が損失につながっていた。その分析と、2回連続で同じ判断のときだけ発注する確認・同日中の売買の反転を拒否するルールの実装
実際に使った環境
Python / Groq API(openai/gpt-oss-120b、temperature=0)/ kabuステーションAPI。2026-09-25に本番の自動売買システムへ導入
筆者の結論
AIの判断そのものは変えず、判断を執行する前の機械的な確認だけを足した。ノイズの売買は減るはずだが、勝ちトレードも遅れるため、成績が良くなるかはまだ分からない

AIに30万円を渡して日本株を自動売買させる実験で、2026年9月17日のピーク(総資産309,355円)から、9月25日には303,030円まで下がりました。下落幅は-2.0%で、元本比ではまだ+1.0%ですが、9月16日以降に買った銘柄の決済済みトレード8件のうち、勝ちは1件だけでした。

「ここ最近成績が悪い」という問題を、相場のせいにせず記録から調べ直したところ、原因は相場ではなく、判断を執行する仕組みのほうにありました。この記事では、その分析と、9月25日に追加した対策を実装に沿って解説します。

🛠 実装してみて分かったこと

「AIの判断が悪かった」のではなく、「AIに何度もサイコロを振らせて、1回でも当たりが出たら執行していた」のが実態でした。1回ごとの判断を見るとそれなりに筋が通った理由が書いてあるので、記事や通知で1件ずつ読んでいる間は気づけませんでした。同じ銘柄の判断を時系列に並べて初めて、答えが揺れていることが見えました。

何が起きていたか

数字で見る成績の悪化

項目値
総資産のピーク(9/17)309,355円
9/25の総資産303,030円(ピーク比-2.0%)
9/16以降に買った銘柄の決済済みトレード8件中、勝ちは1件
勝率の推移9/18時点の78%(7勝2敗)→ 9/25時点で57%(8勝6敗)

9月24〜25日に相場全体が戻す中で、比較のために記録している日経平均・TOPIX(元本を一括で投資していた場合の仮定の資産額)にも上回られました。

同じ日に売って、買い戻していた

売買の記録を銘柄ごとに並べると、同じ日のうちに売買が反転しているケースが見つかりました。

  • NTT(9/25): 9:00に174.9円で売り、1時間後に175.65円で買い戻し
  • LINEヤフー(9/25): 511.1円で買い、同じ日に509.35円で売り
  • パーソルホールディングス(9/25): 10:00に売った直後、10:30以降に「買い」の判断が並んでいた

どれも1回あたりの損失は小さいものの、材料が何も変わっていないのに往復して、そのたびに少しずつ損をしている状態でした。

原因: 判断のブレを、そのまま執行していた

1. AIに渡している情報に、判断を変える根拠がほとんど無かった

AIに渡していたのは、今の株価、本日の始値・高値・安値・前日終値、年次の財務データ、投資予算だけでした。財務データは何日経っても同じ値で、ニュースは取得していません。数日間の値動きも、自分自身が直近に何を売買したかも渡していませんでした。

つまりAIは、毎回ほぼ同じ情報を受け取り、まっさらな状態で判断し直していたことになります。

2. 1日約10回、ほぼ同じ入力で判断し直していた

このシステムは、東証の立会時間中に1時間おき(と大引け直前)に全銘柄を判断します。1銘柄あたり1日約10回です。

AIのパラメータはtemperature=0(出力のランダム性を最小にする設定)にしています。それでも、ほぼ同じ入力に対して判断は揺れていました。NTTの9月25日の判断を時系列に並べると、次のとおりです。

SELL → HOLD → BUY → HOLD → HOLD → BUY → HOLD …

3. 「10回のうち1回でも売買が出たら執行」の構造

そして、これまでの仕組みでは、AIの判断が「BUY」または「SELL」なら、そのままリスクチェックへ進んで発注していました。10回の判断のうちHOLDが8回でも、1回SELL、1回BUYが出れば、その両方が執行されます。

これは言い換えると、判断のブレの最大値を毎日執行していることになります。NTTの往復売買は、まさにこの構造から生まれていました。直近の負けトレード(NTTの売りと買い戻し、パーソルの売り、LINEヤフーの売り)は、どれも「直前の判断は別の方向だった、単発のシグナル」から執行されたものでした。

対策: AIの判断は変えず、執行の前に機械的な確認を足す

このシステムには、「AIの判断をどう信用しないか」を優先する設計方針があります。AIの判断オブジェクトはリスクルールエンジンに渡さず、エンジンは銘柄・数量・価格といった注文情報だけを見て機械的に判定します。今回の対策もこの方針に合わせ、AIの判断ロジックには手を入れず、判断を執行する前の確認を2つ追加しました。

対策1: 直前の判断も同じ方向のときだけ発注する

BUY/SELLは、同じ銘柄の直前の判断も同じ方向だったときだけ発注します。1回だけ出た判断は見送り、次の判断でも同じ答えが出たら、そこで初めて発注します。

判定ロジックは、独立したモジュール(signal_confirmation.py)にまとめました。

@dataclass(frozen=True)
class PreviousSignal:
    """同じ銘柄の直前のAI判断。"""
 
    action: str
    decided_at: dt.datetime
    led_to_order: bool
    """その判断がすでに実際の注文につながったか。すでに執行された判断を
    「確認」として再利用すると、同じシグナルで買い増し・売り増しが
    重なってしまうため、確認には使わない。"""
 
 
def is_signal_confirmed(
    action: str,
    previous: PreviousSignal | None,
    now: dt.datetime,
    max_age_hours: float,
) -> tuple[bool, str]:
    """`action`(BUY/SELL)が直前の判断で確認できているかを返す。
 
    戻り値は(確認できたか, 記録用の説明文)。"""
    if previous is None:
        return False, f"{action}は初回のシグナルのため、次回も同じ判断が出たときに発注(1回だけの判断は見送り)"
    age = _as_utc(now) - _as_utc(previous.decided_at)
    if age > dt.timedelta(hours=max_age_hours):
        return False, f"直前の判断が{age.total_seconds() / 3600:.0f}時間前で古いため、{action}は次回の判断で再確認"
    if previous.action != action:
        return False, f"直前の判断は{previous.action}で{action}と一致しないため、{action}は次回の判断で再確認"
    if previous.led_to_order:
        return False, f"直前の{action}はすでに発注済みのため、同じシグナルの重複発注は見送り"
    return True, f"直前の判断も{action}で一致(確認済み)"

細かいところで、いくつか判断が必要でした。

  • 「直前」の範囲は26時間以内: 翌営業日の寄付(9:00)の判断が、前営業日の引け前(14:55)の判断を「直前」として使えるようにしつつ、連休をまたいだ古い判断は使わないための値です
  • すでに注文につながった判断は、確認に使わない: 「BUY(発注済み)→BUY」を確認済みとすると、同じシグナルで2回買うことになります。確認に使えるのは、まだ執行されていない判断だけです
  • 戻り値に説明文を持たせる: 見送った理由は、他のリスクチェックと同じくrisk_checksテーブルにsignal_confirmationとして記録します。あとから「なぜこの判断は発注されなかったのか」を追えるようにするためです

この確認は、リスクルールエンジンの中ではなく、日次パイプラインの発注直前に置いています。「直前の判断が何だったか」はAIの判断そのものに関わる情報なので、エンジンに持ち込むと「エンジンはAIの判断を見ない」という分離が崩れるためです。

if risk_limits.require_signal_confirmation:
    confirmed, confirmation_detail = is_signal_confirmed(
        analysis.action.value,
        previous_signal,
        dt.datetime.now(dt.timezone.utc),
        risk_limits.signal_confirmation_max_age_hours,
    )
    append_record(
        session,
        RiskCheck,
        {
            "decision_id": decision_row.id,
            "rule_name": "signal_confirmation",
            "result": "PASS" if confirmed else "REJECT",
            "detail": confirmation_detail,
            "checked_at": dt.datetime.now(dt.timezone.utc),
        },
    )
    if not confirmed:
        logger.info("%s: %s", symbol, confirmation_detail)
        continue

previous_signalは、今回の判断をDBに記録する前に取得しています。記録した後に「直近の判断」を引くと、今回の判断自体が返ってきてしまうためです。

対策2: 同じ日に売買を反転させない

もう1つは、リスクルールエンジンへの新しいルールです。同じ銘柄について、当日すでに反対方向の注文を出していたら、後からの注文を拒否します(SELL後のBUY、BUY後のSELL)。

def _check_same_day_reversal(self, order: OrderRequest, portfolio: PortfolioState) -> RuleResult:
    if not self.limits.block_same_day_reversal:
        return RuleResult("same_day_reversal", True, "ルール無効")
    opposite = Side.SELL if order.side == Side.BUY else Side.BUY
    if opposite in portfolio.todays_order_sides.get(order.symbol, frozenset()):
        return RuleResult(
            "same_day_reversal",
            False,
            f"{order.symbol}は本日すでに{opposite.value}済みのため、同日中の{order.side.value}(売買の反転)は見送り",
        )
    return RuleResult("same_day_reversal", True, "OK")

こちらは注文情報(銘柄と売買方向)だけで判定できるので、既存のリスクルールと同じ場所に置けます。当日の売買方向は、二重注文防止のためにもともと持っていた注文キー({日付}:{銘柄}:{売買}という形式)から取り出しているため、新しく状態を持つ必要はありませんでした。

対策1があれば往復売買の多くは防げますが、確認を通過したシグナル同士でも同日中に反転する可能性は残ります。対策2は、その最後の歯止めです。

あわせて: AIに「自分の直近の行動」を渡す

判断のブレを減らすため、AIへの入力にも2つの情報を足しました。

  • 直近5営業日の日次価格: 各営業日の、最後の判断時点の株価
  • この銘柄についての、自分の直近3件の売買: 日時・売買・株数

どちらも新しく取得したデータではなく、このシステム自身が判断のたびに記録してきた実データです。使っている株価データのJ-Quants無料プランは約90日遅延しているため、直近の日足を外部から取ってくる手段がありません。そこで、判断のたびにDBへ保存している入力データ(その時点の株価)から、営業日ごとの最後の値を取り出して使っています。

プロンプトには、次の一文を追加しました。

市場データに「当システムの直近の売買」「直近の日次価格」がある場合は参考にしてください。
株価が数%動いたなど新しい材料が無いのに、直近に売った銘柄をすぐ買い戻す・
直近に買った銘柄をすぐ売る、といった短期間での判断の反転は避けてください。

プロンプトのバージョン番号もv1からv2に上げました。すべての判断に使ったプロンプトのバージョンを記録しているので、あとから「対策前後の判断」を分けて比較できます。

見送った対策

分析の中で、損益がほぼ0%付近での売却を禁止するルール(いわゆる「ノイズ帯」での売買禁止)も検討しました。しかし、過去の記録で該当するのは2件・合計約-365円しかなく、効果が小さいわりに「何%をノイズとみなすか」の線引きが恣意的になるため、見送りました。

建値から-8%の損切り、+20%の利確といった機械的なルールも、これまでの方針どおり接続していません。損切り・利確のタイミングはAIの裁量に任せ、AIの自律的な判断そのものを検証するという実験の目的を優先しています。

この対策の代償: 勝ちトレードも遅れる

過去に実際に執行された35件の注文を、対策1の確認にかけ直してみると、26件が見送りの対象になりました。ただし、この数字には注意が必要です。運用初期は1日の判断回数が少なく、「26時間以内の直前の判断」が存在しなかった分が多く含まれているため、今後の見送り率はこれより低くなるはずです。

もう1つの注意点は、見送りは「取り消し」ではなく「1回分の遅れ」だということです。次の判断でも同じ答えが出れば、1時間後に発注されます。そしてこの遅れは、負けトレードだけでなく勝ちトレードにも同じようにかかります。実際、過去にNTNで+1,660円の利益が出たトレードも、この確認を入れていれば1回分遅れて執行されていた計算です。

つまりこの対策は、「成績を良くする」ことが保証されたものではありません。判断のブレから生まれる無駄な往復を減らすことが狙いで、それが遅れのコストを上回るかどうかは、運用を続けて確かめるしかありません。

よくある質問

Q. temperature=0にすれば、AIの判断は毎回同じになるのではないですか? A. この実験では、temperature=0でも、ほぼ同じ入力に対してHOLD・SELL・BUYの判断が入れ替わることを実際の記録で確認しました。例えばNTTの2026年9月25日の判断は、SELL→HOLD→BUY→HOLD→HOLD→BUY→HOLDと推移しています。

Q. なぜAIのプロンプトを直すだけにしなかったのですか? A. プロンプトの調整もあわせて行っていますが、それだけではAIが指示を守るかどうかに依存します。このシステムは「AIの判断を信用しない安全装置」をコードで持つ方針なので、判断のブレを抑える仕組みも、AIの外側に機械的な確認として置きました。

Q. この対策で成績は良くなりましたか? A. まだ分かりません。導入したのは2026年9月25日の引け後で、この記事の時点では効果を判断できるだけのデータがありません。確認による見送りは発注を1回分遅らせるため、勝ちトレードも同じように遅れます。効果は今後の運用記録で確認し、このブログで報告します。

まとめ

成績が悪化した原因は、AIの判断の質そのものより、「ほぼ同じ入力で1日10回判断させ、1回でも売買が出れば執行する」という執行の構造にありました。LLMの出力はtemperature=0でも揺れるという前提に立ち、2回連続で同じ判断が出たときだけ発注する確認と、同日中の売買の反転を拒否するルールを追加しました。

効果はまだ検証できていません。日々の結果は運用状況ページと毎日のトレード日誌で公開しているので、対策の前後で何が変わったかも、そのまま記録していきます。

免責事項

この記事は投資助言ではありません。特定の銘柄や取引手法を推奨するものではなく、AIに実際のお金を運用させる実験の実装内容を記録したものです。売買の判断や結果を、他の方の投資判断の根拠として参照しないようご注意ください。投資は自己責任で行ってください。

この記事を書いた人

ラボ管理人

AIツール・ノーコード・Next.jsで実際に手を動かしながら、AIツクリラボを運営しています。

運営者情報を見る →