自動売買でパラボリックSARが最強である理由。

自動売買でパラボリックSARが強い理由とPython戦略設計 Python実装・コード

パラボリックSARは、自動売買と非常に相性がよいテクニカル指標です。理由は、相場の方向判定とトレーリングストップを1つの数値で表現でき、裁量判断を挟まずに「保有継続か、手仕舞いか」を毎日更新できるからです。ただし、どんな相場でも勝てる意味での「最強」ではありません。レンジ相場で反転売買を繰り返すと、手数料と小さな損失が積み上がります。

この記事では、パラボリックSAR(Parabolic Stop and Reverse)の計算をPythonでゼロから実装し、ADXを使ったレンジ回避、翌日始値で約定するバックテスト、資金管理まで一体化します。結論を先に言えば、SARの強みが最も生きるのはエントリー指標として単独使用する場面ではなく、すでに乗ったトレンドから機械的に降りる場面です。

この記事の結論
SARは「上がる銘柄を当てる最強指標」ではありません。利益を伸ばしながら撤退価格を自動更新できる、実装しやすいストップ管理エンジンとして強力です。方向判定には長期移動平均、相場環境の判定にはADXを併用すると、単体の弱点を補えます。

パラボリックSARが自動売買に向いている4つの理由

SARはJ. Welles Wilder Jr.が考案したトレンド追随型の指標です。価格の下にドットがあれば上昇局面、価格の上にあれば下降局面と解釈します。名称に含まれるSARは「Stop and Reverse」の略で、現在のポジションを止め、条件が整えば反対方向へ転換する思想が組み込まれています。

自動売買での利点実装上の意味注意点
判定が明確終値とSARの大小で状態を決められる終値確定前に判定しない
ストップが自動で近づく含み益が伸びるほど撤退水準を引き上げられる窓開け時はSAR価格で約定できない
状態を1列で保持できる買い・売り・非保有をコード化しやすい再起動時の状態復元が必要
パラメータが少ないstepとmax_stepを管理すればよい銘柄ごとの過剰最適化は避ける

第一の強みは、曖昧なチャート解釈を減らせることです。「上昇が弱そう」「そろそろ利確したい」といった感覚はプログラムにできません。一方、「前日の終値が前日のSARを上回り、長期移動平均も上向きなら買い候補」のような条件は、誰が実行しても同じ結果になります。

第二の強みは、含み益を伸ばす設計になっていることです。固定の3%利確では、大きなトレンドが発生しても利益を早く切ってしまいます。SARはトレンド中の最高値または最安値であるEP(Extreme Point)を更新するたび、AF(Acceleration Factor)を増やします。その結果、最初は価格から離れていたストップが徐々に近づきます。

第三は、ポジション管理と決済条件が同じロジックでつながることです。自動売買では、シグナル生成だけでなく「注文が約定したか」「建玉が残っているか」「再起動後に二重注文しないか」が重要です。SARは現在のトレンド状態を明示するので、状態機械として整理しやすい特徴があります。Python自動売買の全体像は自動売買プログラムを自作する方法も参考にしてください。

計算式を理解すると「最強」の正体が見える

上昇トレンド中の翌期SARは、次の式で更新します。

SAR_next = SAR_current + AF × (EP - SAR_current)

EPは上昇トレンドならトレンド開始後の最高値、下降トレンドなら最安値です。標準設定ではAFを0.02から始め、新しいEPを更新するたびに0.02ずつ増やし、0.20で上限に達します。たとえば現在SARが1,000円、EPが1,200円、AFが0.04なら、翌期SARは1,008円です。新高値の更新が続くほどAFが増え、SARが価格へ速く接近します。

TradingViewの解説でも、標準値は開始0.02、増分0.02、上限0.20とされています。同時に、SAR単独ではトレンドの強さを測れないため、Wilder自身のDirectional Movement(DMI/ADX)などとの併用が提案されています。つまり、公式に近い説明も「単独で万能」とはしていません。

「最強」と感じやすい理由は、この加速構造にあります。初期はストップに余裕を持たせ、トレンドが長く続くほど利益を守る方向へ締め上げるためです。固定幅ストップと違い、時間の経過と値動きの両方を反映できます。一方、値動きが横ばいでもSARは価格へ近づくため、レンジでは接触と反転が頻発します。強みと弱みは同じ数式から生まれています。

PythonでパラボリックSARをゼロから実装する

TA-Libを使えばtalib.SAR(high, low, acceleration=0.02, maximum=0.2)で計算できます。ただし、自動売買へ組み込む前に、反転処理と直近2本の高値・安値による制約を理解しておくべきです。以下は学習用として見通しを優先した実装です。

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

def load_price(ticker: str, start: str = "2015-01-01") -> pd.DataFrame:
  df = yf.download(
    ticker,
    start=start,
    auto_adjust=True,
    progress=False
  )
  # yfinance 0.2系の複数階層カラムに対応
  if isinstance(df.columns, pd.MultiIndex):
    df.columns = df.columns.get_level_values(0)
  return df[["Open", "High", "Low", "Close", "Volume"]].dropna()

def parabolic_sar(
  high: pd.Series,
  low: pd.Series,
  step: float = 0.02,
  max_step: float = 0.20
) -> pd.DataFrame:
  if len(high) < 3:
    raise ValueError("3本以上の価格データが必要です")

  h = high.astype(float).to_numpy()
  l = low.astype(float).to_numpy()
  sar = np.full(len(h), np.nan)
  trend = np.ones(len(h), dtype=int)

  # 初期方向は最初の2本の中心値で判定
  up = (h[1] + l[1]) >= (h[0] + l[0])
  ep = h[0] if up else l[0]
  af = step
  sar[1] = l[0] if up else h[0]
  trend[:2] = 1 if up else -1

  for i in range(2, len(h)):
    candidate = sar[i - 1] + af * (ep - sar[i - 1])

    if up:
      # 上昇中のSARは直近2本の安値を超えない
      candidate = min(candidate, l[i - 1], l[i - 2])
      if l[i] <= candidate:
        # 反転時は前トレンドのEPから開始
        up = False
        sar[i] = ep
        ep = l[i]
        af = step
      else:
        sar[i] = candidate
        if h[i] > ep:
          ep = h[i]
          af = min(af + step, max_step)
    else:
      # 下降中のSARは直近2本の高値を下回らない
      candidate = max(candidate, h[i - 1], h[i - 2])
      if h[i] >= candidate:
        up = True
        sar[i] = ep
        ep = h[i]
        af = step
      else:
        sar[i] = candidate
        if l[i] < ep:
          ep = l[i]
          af = min(af + step, max_step)

    trend[i] = 1 if up else -1

  return pd.DataFrame(
    {"SAR": sar, "Trend": trend},
    index=high.index
  )

df = load_price("7203.T")
psar = parabolic_sar(df["High"], df["Low"])
df = df.join(psar)
print(df.tail())

重要なのは、SARが単純な移動平均ではなく、前日のSAR・EP・AF・現在の方向を引き継ぐ状態依存の指標であることです。途中の1日だけを独立して計算できません。本番運用では十分な過去データから再計算するか、状態をデータベースへ保存します。

自作値はTA-Libと完全一致するとは限りません。初期トレンドの決定方法や最初のSAR値が実装ごとに異なるためです。本番でTA-Libを採用するなら、自作関数との誤差を確認し、バックテストと本番で同じライブラリ・同じバージョンを固定してください。

SAR単体が負けるレンジ相場をADXで除外する

パラボリックSARは常に上昇か下降のどちらかを示します。「トレンドなし」という第3の状態がありません。これがレンジで致命的になります。価格が狭い範囲を往復するたびに買い、すぐ売り、また買うというウィップソーが発生するからです。

相場環境SARの挙動推奨する扱い
明確な上昇トレンド価格の下から追随ロングの撤退線として利用
明確な下降トレンド価格の上から追随新規買いを停止、保有を手仕舞い
狭いレンジ上下反転を繰り返す新規エントリーを停止
決算後の窓開けSARを飛び越えて始まる実際の始値と滑りで損益計算

ADXは方向ではなくトレンドの強さを見る指標です。ただし、「ADXが25以上なら必ず勝てる」という意味ではありません。25は慣習的な目安にすぎず、銘柄・時間軸・コストを含めて検証が必要です。ここではpandas_taを使い、ADXと長期移動平均をフィルターにします。

import pandas_ta as ta

adx = ta.adx(df["High"], df["Low"], df["Close"], length=14)
df["ADX"] = adx["ADX_14"]
df["SMA200"] = df["Close"].rolling(200).mean()
df["SMA200_Slope"] = df["SMA200"].diff(20)

# 前日終値で確定するシグナル
df["EntrySignal"] = (
  (df["Trend"] == 1)
  & (df["Trend"].shift(1) == -1)
  & (df["ADX"] >= 25)
  & (df["Close"] > df["SMA200"])
  & (df["SMA200_Slope"] > 0)
)

# SAR反転は決済には使う。新規売り建てはしない
df["ExitSignal"] = (
  (df["Trend"] == -1)
  & (df["Trend"].shift(1) == 1)
)

この設計では、SARにすべてを任せません。200日移動平均で大きな方向を選び、ADXでトレンドの強さを確認し、SARはタイミングと撤退を担当します。役割を分けると、どの条件が成績へ影響したか追跡しやすくなります。ADXを使わない場合は、ボリンジャーバンド幅の過去252日パーセンタイルや、ATRを価格で割った正規化ボラティリティでも環境を分類できます。

未来参照を避けたバックテストを実装する

日足の終値・高値・安値から当日のSARを計算する場合、そのシグナルを知れるのは大引け後です。当日の終値で約定したことにすると未来参照になります。ここでは前日確定シグナルを使い、翌営業日の始値で売買します。さらに売買手数料・スリッページを片道0.10%として差し引きます。

def backtest_sar(
  df: pd.DataFrame,
  initial_cash: float = 1_000_000,
  risk_per_trade: float = 0.01,
  max_position_pct: float = 0.30,
  one_way_cost: float = 0.001
) -> tuple[pd.DataFrame, pd.DataFrame]:
  cash = initial_cash
  shares = 0
  entry_price = np.nan
  records = []
  trades = []

  for i in range(1, len(df)):
    today = df.iloc[i]
    yesterday = df.iloc[i - 1]
    open_price = float(today["Open"])

    # 前日シグナルを当日始値で執行
    if shares == 0 and bool(yesterday["EntrySignal"]):
      sar_stop = float(yesterday["SAR"])
      risk_per_share = max(
        float(yesterday["Close"]) - sar_stop,
        float(yesterday["Close"]) * 0.01
      )
      risk_budget = cash * risk_per_trade
      by_risk = int(risk_budget / risk_per_share)
      by_value = int((cash * max_position_pct) / open_price)
      # 日本株の単元株を想定して100株単位に丸める
      qty = min(by_risk, by_value)
      qty = (qty // 100) * 100

      if qty > 0:
        buy_price = open_price * (1 + one_way_cost)
        cash -= qty * buy_price
        shares = qty
        entry_price = buy_price
        entry_date = df.index[i]

    elif shares > 0 and bool(yesterday["ExitSignal"]):
      sell_price = open_price * (1 - one_way_cost)
      cash += shares * sell_price
      trades.append({
        "entry_date": entry_date,
        "exit_date": df.index[i],
        "shares": shares,
        "entry": entry_price,
        "exit": sell_price,
        "return": sell_price / entry_price - 1
      })
      shares = 0
      entry_price = np.nan

    equity = cash + shares * float(today["Close"])
    records.append({"date": df.index[i], "equity": equity})

  equity = pd.DataFrame(records).set_index("date")
  trades = pd.DataFrame(trades)
  return equity, trades

equity, trades = backtest_sar(df)
daily = equity["equity"].pct_change().fillna(0)
drawdown = equity["equity"] / equity["equity"].cummax() - 1

years = len(equity) / 252
cagr = (equity["equity"].iloc[-1] / equity["equity"].iloc[0]) ** (1 / years) - 1
sharpe = daily.mean() / daily.std() * np.sqrt(252)

print({
  "CAGR": round(cagr * 100, 2),
  "最大DD": round(drawdown.min() * 100, 2),
  "Sharpe": round(sharpe, 2),
  "取引回数": len(trades),
  "勝率": round((trades["return"] > 0).mean() * 100, 1)
})

バックテストでは、勝率だけを見ないでください。トレンドフォローは、小さな損失を何度も受け入れ、少数の大きな利益で回収する傾向があります。勝率40%でも平均利益が平均損失の2倍以上なら成立し得ます。反対に、勝率70%でも一度の窓開け損失が大きければ資産は減ります。最大ドローダウン、プロフィットファクター、期待値、年間取引回数を一緒に確認します。

また、このコードは説明用の最小構成です。現実には配当、信用金利、注文失効、値幅制限、ストップ高・ストップ安、部分約定を扱う必要があります。バックテストの基本と評価指標はPythonバックテストの実装で詳しく解説しています。

パラメータ最適化でやってはいけないこと

stepを大きくするとSARは価格へ速く近づき、利益を早く守れる一方で、小さな押し目でも反転します。stepを小さくするとダマシは減りますが、撤退が遅くなります。max_stepにも同じトレードオフがあります。そこで、1銘柄の過去データに最も合う組み合わせを探したくなりますが、それだけでは過剰最適化です。

  • 時系列分割:前半で候補を選び、後半の未使用期間で検証する
  • 複数銘柄:大型株、景気敏感株、ディフェンシブ株で再現するか確認する
  • パラメータ面:一点だけでなく、近い設定でも成績が安定しているか見る
  • コスト感応度:想定コストを2倍にしても優位性が残るか確認する
  • ウォークフォワード:過去だけで更新した設定を次期間へ適用する
def parameter_candidates() -> list[tuple[float, float]]:
  rows = []
  for step in (0.01, 0.015, 0.02, 0.025, 0.03):
    for max_step in (0.10, 0.15, 0.20, 0.25):
      if max_step > step:
        rows.append((step, max_step))
  return rows

# 全期間で最高値を選ばず、学習期間と検証期間を分離する
split = int(len(df) * 0.7)
train = df.iloc[:split].copy()
test = df.iloc[split:].copy()
print("候補数:", len(parameter_candidates()))
print("学習:", train.index.min(), train.index.max())
print("検証:", test.index.min(), test.index.max())

良い設定がstep=0.017の一点だけに現れ、0.016と0.018で崩れるなら、その数字は偶然を拾った可能性が高いと考えます。多少パラメータを動かしてもプラスが残る「台地」を探すほうが実運用向きです。ウォークフォワード検証については過学習を防ぐWalk-Forward Validationも確認してください。

実運用では注文価格ではなく状態管理が重要

バックテストが良くても、注文処理が不安定なら自動売買は成立しません。プログラムを1分ごとに起動したとき、同じ買いシグナルで何度も注文を送る事故を防ぐ必要があります。最低限、注文前に証券会社側の建玉と未約定注文を取得し、ローカル記録と照合します。

保存する状態用途事故防止
signal_dateシグナルが発生した日同日シグナルの重複処理を防ぐ
client_order_id注文を一意に識別タイムアウト後の二重発注を防ぐ
broker_order_id証券会社側の注文番号照会・取消とひも付ける
filled_quantity約定済み数量部分約定を正しく反映する
last_sar直近SAR値監査と異常検知に使う

SARを逆指値として発注できる環境でも、毎日値を更新する際は「新しいストップが前日より不利な方向へ動いていないか」を検査します。ロングなら撤退線を下げない、ショートなら上げないという単調性を守ると、計算バグでリスクが拡大する事故を防げます。また、決算発表前後はギャップでSARを飛び越える可能性があるため、決算日フィルターやポジション縮小を別ルールで設けます。

パラボリックSARを「最強」に近づける実践ルール

ここまでの内容を、運用ルールとして整理します。最も重要なのは、SARを予言装置としてではなく、担当範囲が明確な部品として使うことです。

  1. 方向は長期移動平均で選ぶ:200日線の上で上向きならロングのみを検討する
  2. 環境はADXで選ぶ:トレンドが弱い期間は新規エントリーを止める
  3. タイミングはSAR反転で取る:前日確定値を使い、翌営業日に執行する
  4. 撤退はSARを優先する:利益目標を固定せず、伸びるトレンドを追う
  5. 数量は損失額から逆算する:1回の想定損失を運用資金の0.5〜1%程度に抑える
  6. 決算と流動性を別管理する:SARだけではギャップと約定リスクを測れない

「パラボリックSARが最強」という表現を正確に言い換えるなら、トレンドを予測する指標として最強なのではなく、トレンドに乗った後の撤退判断を自動化する道具として完成度が高い、となります。式が明確で、パラメータが少なく、ストップが利益方向へ加速する。この3点は自動売買に大きな価値があります。

最初は実資金を使わず、複数銘柄・複数期間でバックテストし、その後にペーパートレードで注文時刻と約定差を記録してください。単体SARの派手な反転シグナルを追うより、ADXと長期トレンドで取引回数を絞り、撤退ルールとしてSARを徹底するほうが、再現性のある仕組みに近づきます。

参考資料

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