株自動売買プログラミング SBI証券編【Python自作】

株自動売買プログラミングSBI証券編 Python自作とIFDOCO活用 準備・環境構築

SBI証券を使った日本株の自動売買プログラムは、Pythonで「分析・シグナル・発注数量・注文票」まで自作し、実際の注文はHYPER SBI 2のIFDOCO注文へ入力する半自動構成から始めるのが安全です。重要な事実として、SBI証券は国内現物株を個人がPythonから自由に発注できる公開REST APIの仕様書を提供していません。先物・オプションAPIはありますが、国内現物株の公開APIとは別物です。

そのため、この記事では存在しないAPIエンドポイントや認証トークンを捏造しません。SeleniumやPyAutoGUIでログイン画面を自動操作する方法も、規約・セキュリティ・誤発注のリスクが高いため採用しません。代わりに、Pythonで再現可能な売買判断を作り、注文内容をCSVと通知へ出力し、SBI証券の公式注文機能で執行する実用的な構成を作ります。

先に結論:SBI証券編で自作できるのは、株価取得、シグナル判定、資金管理、注文票生成、約定履歴の照合、運用ログです。国内株の発注ボタンをPythonから直接押す部分は、公式公開APIが確認できない限り実装しないのが正解です。HYPER SBI 2のIFDOCOを使えば、買付・利益確定・損切りの3条件は公式機能内で自動化できます。

SBI証券でPython自動売買を作る前に確認すること

インターネット上には「HYPER SBI 2 API」「localhost:18080」というサンプルが見つかります。しかし、localhost:18080は三菱UFJ eスマート証券のkabuステーションAPIで使われる代表的な接続先です。SBI証券の公式サイトで国内現物株向けPython REST APIのエンドポイント、認証方法、注文仕様が公開されていない以上、そのコードをSBI証券用として実行してはいけません。

機能SBI証券での扱いPython側の方針
国内株の市場データHYPER SBI 2で閲覧可能検証用はyfinance等、利用条件を確認して取得
国内株の公開REST発注API一般向け仕様書を確認できない架空APIを実装しない
先物・オプションAPI接続可能な外部ツールを公式案内国内現物株と混同しない
IFD・OCO・IFDOCOHYPER SBI 2等の公式注文機能Pythonで価格を計算し、画面で確認して入力
取引履歴CSV等で確認可能pandasで日次照合する

SBI証券の先物・オプションAPIは、注文、残高照会などに対応する外部ツール連携です。しかし、先物・オプションは証拠金取引であり、国内現物株のプログラムとは商品性もリスクも異なります。本記事の対象は現物の日本株です。レバレッジを使う信用取引へそのまま流用しないでください。

HYPER SBI 2は通常注文に加え、逆指値、IFD、OCO、IFDOCOに対応しています。IFDOCOでは買い注文が約定した後、利益確定の指値と損切りの逆指値を待機させ、一方が成立すればもう一方を取り消せます。Pythonで計算した3つの価格を入力すれば、日中ずっとPCを監視しなくても出口を証券会社側に置けます。

安全な全体構成:判断はPython、執行は公式画面

プログラムは5つの部品に分けます。価格取得と戦略だけを1つの巨大なスクリプトへ詰め込むと、途中で失敗した際に二重生成やログ欠落が起きます。入力、判断、注文案、照合、監視を分離するとテストしやすくなります。

  1. Data:日足OHLCVを取得し、欠損・重複・株式分割を確認する
  2. Strategy:確定した前日データだけで売買シグナルを作る
  3. Risk:損切り幅から100株単位の注文数量を計算する
  4. Ticket:HYPER SBI 2へ入力するIFDOCO注文票をCSVへ出す
  5. Reconcile:SBI証券の約定履歴とローカル台帳を照合する

この構成は「完全自動ではないから劣る」のではありません。実資金を扱う初期段階では、発注前に銘柄・数量・価格を人が確認できることが安全装置になります。シグナルが安定しない段階で注文だけ自動化しても、損失の速度が上がるだけです。まずは分析から注文票までを毎日同じ手順で生成できる状態を目指します。

開発環境とフォルダを準備する

WindowsでPython 3.11以降を使う例です。仮想環境を作り、必要なライブラリをプロジェクト内へ固定します。HYPER SBI 2のログイン情報や取引パスワードはPythonファイルへ書きません。

mkdir sbi-trading
cd sbi-trading
python -m venv .venv
.venv\Scripts\activate
python -m pip install --upgrade pip
pip install pandas numpy yfinance pydantic
sbi-trading/
├─ data/
│  ├─ prices/
│  └─ executions/
├─ output/
│  ├─ order_tickets/
│  └─ logs/
├─ config.json
├─ strategy.py
├─ make_ticket.py
└─ reconcile.py

yfinanceはバックテストや学習に便利ですが、証券取引所やSBI証券の公式発注用データフィードではありません。遅延、欠損、仕様変更があり得ます。本番の注文価格はHYPER SBI 2で確認し、前日終値ベースの低頻度戦略に限定します。秒単位のデイトレードには適しません。

移動平均とATRで売買シグナルを作る

例として、20日移動平均が60日移動平均を上抜き、200日移動平均の上にあるときだけ買い候補にします。損切りは14日ATRの2倍、利益確定はリスクの2倍です。単純な戦略ですが、入口・損失・利益目標を数式で説明できます。

# strategy.py
from __future__ import annotations

import numpy as np
import pandas as pd
import yfinance as yf

def download_daily(ticker: str, start: str = "2018-01-01") -> pd.DataFrame:
  df = yf.download(
    ticker,
    start=start,
    auto_adjust=True,
    progress=False
  )
  if isinstance(df.columns, pd.MultiIndex):
    df.columns = df.columns.get_level_values(0)
  required = ["Open", "High", "Low", "Close", "Volume"]
  return df[required].dropna().sort_index()

def add_atr(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
  out = df.copy()
  prev_close = out["Close"].shift(1)
  tr = pd.concat([
    out["High"] - out["Low"],
    (out["High"] - prev_close).abs(),
    (out["Low"] - prev_close).abs()
  ], axis=1).max(axis=1)
  # Wilder方式の平滑化
  out["ATR"] = tr.ewm(
    alpha=1 / length,
    adjust=False,
    min_periods=length
  ).mean()
  return out

def make_signal(df: pd.DataFrame) -> pd.DataFrame:
  out = add_atr(df)
  out["SMA20"] = out["Close"].rolling(20).mean()
  out["SMA60"] = out["Close"].rolling(60).mean()
  out["SMA200"] = out["Close"].rolling(200).mean()

  cross_up = (
    (out["SMA20"] > out["SMA60"])
    & (out["SMA20"].shift(1) <= out["SMA60"].shift(1))
  )
  trend_ok = (
    (out["Close"] > out["SMA200"])
    & (out["SMA200"].diff(20) > 0)
  )
  liquid = out["Volume"].rolling(20).median() >= 100_000
  out["BUY"] = cross_up & trend_ok & liquid
  return out

if __name__ == "__main__":
  frame = make_signal(download_daily("7203.T"))
  print(frame.tail(3)[
    ["Close", "SMA20", "SMA60", "SMA200", "ATR", "BUY"]
  ])

auto_adjust=Trueにしているため、株式分割や配当を調整した価格で長期検証します。ただし、注文票へ使う実際の価格は未調整の現在値と一致することを確認してください。過去の分析値と当日の発注価格を混同しないことが重要です。また、シグナルは大引け後に確定するため、翌営業日用の注文案として扱います。

損失額から注文数量を計算する

自動売買で最も危険なのは「良さそうだから500株」のように数量を感覚で決めることです。1回の許容損失を運用資金の0.5〜1%に固定し、買値と損切り値の差から株数を逆算します。日本株は通常100株単位なので、必ず下方向へ丸めます。

from dataclasses import dataclass
import math

@dataclass(frozen=True)
class RiskPlan:
  entry: float
  stop: float
  target: float
  quantity: int
  planned_loss: float

def position_size(
  cash: float,
  entry: float,
  atr: float,
  risk_pct: float = 0.0075,
  max_position_pct: float = 0.25,
  lot_size: int = 100
) -> RiskPlan:
  if min(cash, entry, atr) <= 0:
    raise ValueError("cash, entry, atrは正数が必要です")

  stop = entry - 2.0 * atr
  risk_per_share = entry - stop
  risk_budget = cash * risk_pct
  qty_by_risk = math.floor(risk_budget / risk_per_share)
  qty_by_value = math.floor((cash * max_position_pct) / entry)
  raw_qty = min(qty_by_risk, qty_by_value)
  quantity = (raw_qty // lot_size) * lot_size

  if quantity < lot_size:
    raise ValueError("許容リスク内で単元株を購入できません")

  target = entry + 2.0 * risk_per_share
  return RiskPlan(
    entry=round(entry, 1),
    stop=round(stop, 1),
    target=round(target, 1),
    quantity=quantity,
    planned_loss=round(risk_per_share * quantity, 0)
  )

たとえば運用資金100万円、候補価格2,000円、ATRが50円なら、損切り候補は1,900円です。リスク率0.75%なら許容損失は7,500円で、リスク基準では75株しか買えません。100株単位では見送りになります。「買える銘柄を探す」のではなく、ルールに収まらない取引を見送るのが資金管理です。

S株は単元未満で数量調整しやすい一方、SBI証券の公式FAQではS株とPTSはIFDOCO注文の対象外とされています。したがって、IFDOCOを使う本記事の構成では100株単位を前提にします。サービス仕様は変更される可能性があるため、発注前に注文画面と公式FAQを再確認してください。

HYPER SBI 2用のIFDOCO注文票をCSV出力する

次に、監視銘柄を順番に評価し、条件を満たした銘柄だけ注文票へ出します。成行価格を正確に予測することはできないため、前日終値を仮の入口として数量を計算し、実際の寄付前に再計算・確認します。

# make_ticket.py
from __future__ import annotations

from datetime import datetime
from pathlib import Path
import json
import pandas as pd

from strategy import download_daily, make_signal
from risk import position_size

WATCHLIST = {
  "7203.T": "トヨタ自動車",
  "8306.T": "三菱UFJフィナンシャル・グループ",
  "9432.T": "日本電信電話"
}

def build_tickets(cash: float) -> pd.DataFrame:
  tickets = []
  for ticker, name in WATCHLIST.items():
    df = make_signal(download_daily(ticker))
    latest = df.iloc[-1]

    if not bool(latest["BUY"]):
      continue

    try:
      plan = position_size(
        cash=cash,
        entry=float(latest["Close"]),
        atr=float(latest["ATR"])
      )
    except ValueError as exc:
      print(f"SKIP {ticker}: {exc}")
      continue

    tickets.append({
      "signal_date": df.index[-1].date().isoformat(),
      "ticker": ticker.removesuffix(".T"),
      "name": name,
      "order_type": "IFDOCO",
      "side": "現物買",
      "entry_reference": plan.entry,
      "quantity": plan.quantity,
      "take_profit": plan.target,
      "stop_trigger": plan.stop,
      "planned_loss": plan.planned_loss,
      "status": "REVIEW_REQUIRED"
    })
  return pd.DataFrame(tickets)

def main() -> None:
  config = json.loads(Path("config.json").read_text(encoding="utf-8"))
  tickets = build_tickets(float(config["trading_cash"]))
  Path("output/order_tickets").mkdir(parents=True, exist_ok=True)
  stamp = datetime.now().strftime("%Y%m%d")
  path = Path("output/order_tickets") / f"orders_{stamp}.csv"
  tickets.to_csv(path, index=False, encoding="utf-8-sig")
  print(tickets.to_string(index=False) if not tickets.empty else "注文候補なし")
  print("saved:", path)

if __name__ == "__main__":
  main()
{
  "trading_cash": 1000000,
  "risk_per_trade": 0.0075,
  "max_positions": 4
}

CSVのstatusを最初からREVIEW_REQUIREDにしているのがポイントです。プログラムが出した数字をそのまま発注済みにしません。HYPER SBI 2を開き、銘柄コード、売買区分、数量、買付条件、利益確定、損切りを1項目ずつ照合した後に注文します。IFDOCOでは親注文が約定すると子注文が有効になるため、子注文の値幅・有効期限も確認します。

バックテストは翌日始値とコストで評価する

移動平均クロスが当日の終値で確定した後、同じ終値で買えたことにすると未来参照です。シグナルを1日ずらし、翌日の始値で約定した前提にします。SBI証券の「ゼロ革命」で条件を満たせば国内株の通常売買手数料は無料ですが、売買手数料がゼロでもスプレッド、価格変動、税金、信用取引の金利・貸株料はゼロになりません。

def simple_backtest(
  df: pd.DataFrame,
  slippage: float = 0.001
) -> pd.DataFrame:
  out = make_signal(df)
  out["EXIT"] = (
    (out["SMA20"] < out["SMA60"])
    & (out["SMA20"].shift(1) >= out["SMA60"].shift(1))
  )

  state = pd.Series(np.nan, index=out.index)
  state[out["BUY"]] = 1.0
  state[out["EXIT"]] = 0.0
  state = state.ffill().fillna(0.0)

  # 前日大引け後に決定し、翌日寄付から保有
  position = state.shift(1).fillna(0.0)
  open_to_open = out["Open"].shift(-1) / out["Open"] - 1
  turnover = position.diff().abs().fillna(position.abs())
  out["strategy_return"] = (
    position * open_to_open - turnover * slippage
  )
  out["equity"] = (1 + out["strategy_return"].fillna(0)).cumprod()
  out["drawdown"] = out["equity"] / out["equity"].cummax() - 1
  return out

tested = simple_backtest(download_daily("7203.T"))
ret = tested["strategy_return"].dropna()
print("累積:", round(tested["equity"].iloc[-2] - 1, 3))
print("最大DD:", round(tested["drawdown"].min(), 3))
print("Sharpe:", round(ret.mean() / ret.std() * np.sqrt(252), 2))

この簡易版はATRストップとIFDOCOの約定順序を再現していません。日中に利益確定価格と損切り価格の両方へ触れた日は、日足だけではどちらが先だったか分かりません。都合のよい方を採用せず、保守的に損切りを先とするか、分足データで順序を確認します。バックテストの評価方法はPythonバックテスト入門も参照してください。

約定履歴を照合してローカル台帳を正しくする

注文したつもりと、実際の建玉は同じではありません。指値未約定、部分約定、注文取消、入力ミスが起こります。SBI証券から取得した約定履歴CSVを読み、注文票と照合します。列名はダウンロード画面や時期で変わる可能性があるため、実ファイルに合わせてマッピングしてください。

# reconcile.py
from pathlib import Path
import pandas as pd

def normalize_code(value: object) -> str:
  text = str(value).strip()
  return text.split(".")[0].zfill(4)

def reconcile(ticket_path: str, execution_path: str) -> pd.DataFrame:
  tickets = pd.read_csv(ticket_path, dtype={"ticker": str})
  fills = pd.read_csv(execution_path, encoding="cp932", dtype=str)

  # 実際のCSV列名に合わせて変更する
  fills = fills.rename(columns={
    "銘柄コード": "ticker",
    "約定数量": "filled_quantity",
    "約定単価": "filled_price",
    "約定日": "filled_date"
  })
  tickets["ticker"] = tickets["ticker"].map(normalize_code)
  fills["ticker"] = fills["ticker"].map(normalize_code)

  for col in ("filled_quantity", "filled_price"):
    fills[col] = pd.to_numeric(
      fills[col].str.replace(",", "", regex=False),
      errors="coerce"
    )

  summary = fills.groupby("ticker", as_index=False).agg(
    filled_quantity=("filled_quantity", "sum"),
    avg_filled_price=("filled_price", "mean"),
    last_filled_date=("filled_date", "max")
  )
  result = tickets.merge(summary, on="ticker", how="left")
  result["reconcile_status"] = result.apply(
    lambda row: "FILLED"
    if row["filled_quantity"] == row["quantity"]
    else ("PARTIAL_OR_NONE" if pd.notna(row["filled_quantity"]) else "NOT_FOUND"),
    axis=1
  )
  return result

if __name__ == "__main__":
  result = reconcile(
    "output/order_tickets/orders_YYYYMMDD.csv",
    "data/executions/sbi_execution.csv"
  )
  print(result[[
    "ticker", "quantity", "filled_quantity",
    "avg_filled_price", "reconcile_status"
  ]].to_string(index=False))

約定履歴CSVの読み込みについてはSBI証券CSVをPythonで集計する方法でも解説しています。文字コードがUTF-8とは限らないため、文字化けしたらcp932を確認します。

本番移行前のチェックリスト

  • シグナル計算に当日未確定の終値を使っていない
  • 株式分割・上場廃止銘柄を含む複数銘柄で検証した
  • スリッページを0.1〜0.3%へ増やしても成績が崩れない
  • 1回の想定損失と全建玉の合計リスクに上限がある
  • 決算発表日の前後は新規注文を停止する
  • IFDOCOの対象商品、有効期限、値幅制限を公式画面で確認した
  • 注文票と約定履歴を毎営業日照合する
  • 最初の1か月は発注せず、注文候補と仮想損益だけ記録する

運用開始後も、プログラムが動いたことではなく、入力データが正しいことを監視します。前日比が異常、出来高がゼロ、最終日付が古い、同じ行が重複している場合は注文票を生成しないフェイルクローズ設計にします。エラー時に前回の注文票を再利用する仕様は危険です。

SBI証券でのPython自作は、発注APIの有無をごまかさず、公式機能と自作部分の境界を明確にするほど安全になります。価格取得から注文票までを自動化し、HYPER SBI 2のIFDOCOで出口を預け、約定履歴をPythonで照合する。この半自動構成だけでも、感情による銘柄変更、数量の過大化、損切り忘れを大きく減らせます。

公式情報・参考資料

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