「バックテストで勝率70%」。この数字だけを見て安心していないだろうか。
Pineスクリプトのストラテジー機能を使えば、自分のトレード手法を過去データで検証できる。strategy()で宣言し、strategy.entry()でエントリーし、ストラテジーテスターで成績を確認する。手順自体は難しくない。
問題は「その数字が信頼できるかどうか」だ。
バックテストの勝率70%は、パラメータを過去データに合わせて調整した結果かもしれない。スプレッドやスリッページを考慮していないかもしれない。特定の相場環境でたまたまハマっただけかもしれない。こうした落とし穴を知らずにリアル運用に移ると、「バックテストでは勝てたのに実際は負ける」という典型的な失敗パターンにはまる。
この記事では、Pineスクリプトでストラテジーを作成する方法を、「コードの書き方」だけでなく「検証結果の読み方」「数字の信頼性の判断方法」まで含めて解説する。コピペで動くサンプルコードを複数掲載しているので、実際にTradingViewで動かしながら読んでほしい。
ストラテジーの基本構造
Pineスクリプトのストラテジーは、indicator()の代わりにstrategy()で宣言する。これだけで、TradingViewのストラテジーテスターが有効になり、バックテスト結果を確認できるようになる。
最小構成のストラテジーはこうなる。
//@version=6
strategy("最小ストラテジー", overlay=true)
// エントリー条件
if ta.crossover(ta.ema(close, 12), ta.ema(close, 26))
strategy.entry("Long", strategy.long)
// エグジット条件
if ta.crossunder(ta.ema(close, 12), ta.ema(close, 26))
strategy.close("Long")
たった8行。12EMAが26EMAを上抜けたらロング、下抜けたらクローズ。これをチャートに追加すると、過去データ全体で売買シミュレーションが実行され、ストラテジーテスターに成績が表示される。
ストラテジーのコードは、大きく4つのパートで構成される。
①宣言部: strategy()で名前、初期資金、ポジションサイズ、手数料などを設定する。
②計算部: テクニカル指標の計算、条件判定、フィルターの適用を行う。
③注文部: strategy.entry()でエントリー、strategy.close()でクローズ、strategy.exit()でTP/SLを設定する。
④描画部: plot()やplotshape()でインジケーターやシグナルを可視化する。
strategy()の設定 — ここで結果が大きく変わる
strategy()の引数は、バックテスト結果に直接影響する。適当に設定すると、結果の信頼性が大きく下がる。
//@version=6
strategy("EMAクロス",
overlay=true,
initial_capital=1000000, // 初期資金100万円
default_qty_type=strategy.percent_of_equity,
default_qty_value=10, // 資金の10%でエントリー
commission_type=strategy.commission.percent,
commission_value=0.1, // 往復0.1%の手数料
slippage=2, // 2ティックのスリッページ
process_orders_on_close=false, // バーの終値で注文を処理しない
calc_on_every_tick=false) // ティックごとの計算を無効化
特に重要な設定を説明する。
initial_capital(初期資金)。 デフォルトは$1,000,000だが、自分の実際のトレード資金に近い値を設定する。資金量によってポジションサイズが変わり、成績も変わる。
commission_value(手数料)。 デフォルトは0(手数料なし)。手数料なしのバックテストは現実離れしている。FXなら0.01〜0.05%、暗号資産なら0.05〜0.1%が目安。スプレッドを含めたい場合は、手数料をやや大きめに設定するか、slippageで調整する。
slippage(スリッページ)。 実際の約定では、指定価格と約定価格にずれが生じる。1〜3ティックを設定しておくと現実に近い結果が得られる。
process_orders_on_close。 trueにすると、バーの終値確定時点で注文が処理される。false(デフォルト)だと次のバーの始値で約定する。trueは「未来の情報を使っている」ことになりやすいので、基本はfalseのままにする。
エントリーとエグジットの書き方
ストラテジーの注文関数は3つある。
strategy.entry() — 新規エントリー。同じIDで複数回呼ぶと、最新の注文で上書きされる。
strategy.close() — 指定IDのポジションを成行でクローズ。
strategy.exit() — TP(利確)/SL(損切)/トレーリングストップを設定。エントリー後に自動で機能する。
この3つの使い分けを、実践的なストラテジーで見てみよう。
サンプル①: EMAクロス+RSIフィルター+固定TP/SL
最も基本的で応用しやすい構成。トレンドフォロー系のストラテジーだ。
//@version=6
strategy("EMAクロス+RSI", overlay=true,
initial_capital=1000000,
default_qty_type=strategy.percent_of_equity,
default_qty_value=10,
commission_value=0.05,
slippage=2)
// === パラメータ ===
emaFast = input.int(12, "短期EMA", minval=1, group="EMA設定")
emaSlow = input.int(26, "長期EMA", minval=1, group="EMA設定")
rsiLen = input.int(14, "RSI期間", minval=1, group="フィルター")
rsiUpper = input.int(70, "RSI上限", group="フィルター")
rsiLower = input.int(30, "RSI下限", group="フィルター")
tpPips = input.float(100, "利確(pips)", group="決済")
slPips = input.float(50, "損切(pips)", group="決済")
// === 計算 ===
fast = ta.ema(close, emaFast)
slow = ta.ema(close, emaSlow)
rsi = ta.rsi(close, rsiLen)
// === pips → 価格変換 ===
pipValue = syminfo.mintick * 10
tp = tpPips * pipValue
sl = slPips * pipValue
// === エントリー条件 ===
longCond = ta.crossover(fast, slow) and rsi < rsiUpper
shortCond = ta.crossunder(fast, slow) and rsi > rsiLower
// === 注文 ===
if longCond
strategy.entry("Long", strategy.long)
strategy.exit("Long TP/SL", "Long", profit=tp/syminfo.mintick, loss=sl/syminfo.mintick)
if shortCond
strategy.entry("Short", strategy.short)
strategy.exit("Short TP/SL", "Short", profit=tp/syminfo.mintick, loss=sl/syminfo.mintick)
// === 描画 ===
plot(fast, "短期EMA", color.blue)
plot(slow, "長期EMA", color.red)
このストラテジーのポイントを解説する。
RSIフィルター。 EMAクロスだけでは、レンジ相場でダマシのシグナルが乱発する。RSIが買われすぎ圏(70超)でのロングを除外し、売られすぎ圏(30未満)でのショートを除外することで、無駄なエントリーを減らす。
strategy.exit()のprofit/loss。 profitとlossは「ティック数」で指定する。pips単位で入力したい場合は、上記のようにpipValueで変換する必要がある。FXの場合、1pip = 10 mintick(5桁ブローカー)が一般的。
ロングとショートの両方向。 トレンドフォロー系は、売りと買いの両方向をテストすることで、ストラテジーの堅牢性を確認できる。片方向だけの検証は、相場の方向性にバイアスがかかる。
サンプル②: ブレイクアウト+ATRベースTP/SL
固定pipsのTP/SLは、ボラティリティが変動する相場では機能しにくい。ATR(Average True Range)でTP/SLを動的に調整するストラテジーを紹介する。
//@version=6
strategy("ATRブレイクアウト", overlay=true,
initial_capital=1000000,
default_qty_type=strategy.percent_of_equity,
default_qty_value=10,
commission_value=0.05,
slippage=2)
// === パラメータ ===
lookback = input.int(20, "ブレイクアウト期間", minval=5, group="エントリー")
atrLen = input.int(14, "ATR期間", minval=1, group="ATR")
atrTPmul = input.float(2.0, "ATR × TP倍率", step=0.1, group="決済")
atrSLmul = input.float(1.0, "ATR × SL倍率", step=0.1, group="決済")
// === 計算 ===
highestHigh = ta.highest(high, lookback)
lowestLow = ta.lowest(low, lookback)
atr = ta.atr(atrLen)
// === エントリー条件 ===
longBreak = close > highestHigh[1]
shortBreak = close < lowestLow[1]
// === 注文 ===
if longBreak
strategy.entry("Long", strategy.long)
strategy.exit("Long Exit", "Long",
profit=atr * atrTPmul / syminfo.mintick,
loss=atr * atrSLmul / syminfo.mintick)
if shortBreak
strategy.entry("Short", strategy.short)
strategy.exit("Short Exit", "Short",
profit=atr * atrTPmul / syminfo.mintick,
loss=atr * atrSLmul / syminfo.mintick)
// === 描画 ===
plot(highestHigh, "高値ライン", color.green, style=plot.style_stepline)
plot(lowestLow, "安値ライン", color.red, style=plot.style_stepline)
ATRベースの利点。 ボラティリティが高い相場ではTP/SLが広がり、低い相場では狭まる。相場環境に自動適応するため、固定pipsよりも堅牢性が高い。
ATR倍率の目安。 TP=ATR×2倍、SL=ATR×1倍が一般的な起点。リスクリワード比2:1になる。ここからパラメータを調整していく。
サンプル③: 時間帯フィルター付きストラテジー
FXやゴールドは、東京・ロンドン・ニューヨークの各セッションで値動きの性質が異なる。時間帯フィルターを入れることで、有効な時間帯だけにエントリーを限定できる。
//@version=6
strategy("セッション限定EMAクロス", overlay=true,
initial_capital=1000000,
default_qty_type=strategy.percent_of_equity,
default_qty_value=10,
commission_value=0.05,
slippage=2)
// === パラメータ ===
emaFast = input.int(12, "短期EMA", group="EMA設定")
emaSlow = input.int(26, "長期EMA", group="EMA設定")
useSession = input.bool(true, "セッション限定", group="時間帯")
sessionStr = input.session("0800-1600", "許可時間帯(UTC)", group="時間帯")
// === 計算 ===
fast = ta.ema(close, emaFast)
slow = ta.ema(close, emaSlow)
// === 時間帯フィルター ===
inSession = useSession ? not na(time(timeframe.period, sessionStr)) : true
// === エントリー条件 ===
longCond = ta.crossover(fast, slow) and inSession
shortCond = ta.crossunder(fast, slow) and inSession
// === 注文 ===
if longCond
strategy.entry("Long", strategy.long)
if shortCond
strategy.entry("Short", strategy.short)
// セッション外で全ポジションクローズ
if useSession and not inSession
strategy.close_all("セッション終了")
// === 描画 ===
plot(fast, "短期EMA", color.blue)
plot(slow, "長期EMA", color.red)
bgcolor(inSession ? color.new(color.blue, 95) : na)
input.session()。 時間帯を「HHMM-HHMM」形式で設定パネルから指定できる。UTC基準なので、東京時間(JST)の場合は-9時間で計算する。ロンドン時間08:00-16:00(UTC)が「0800-1600」。
strategy.close_all()。 セッション外に入ったタイミングで全ポジションをクローズする。ポジションを持ち越さないデイトレードスタイルのテストに使う。
bgcolor()で時間帯を可視化。 許可されている時間帯の背景色を薄く塗ることで、エントリーが制限されている時間帯を目視で確認できる。
ストラテジーテスターの読み方
ストラテジーをチャートに追加すると、画面下部の「ストラテジーテスター」タブに結果が表示される。数字の意味を正しく理解しないと、判断を誤る。
純利益(Net Profit)。 全トレードの損益合計。プラスならトータルで勝ち、マイナスなら負け。ただし、この数字だけでは判断できない。初期資金に対する割合(%)で見ることが重要。
勝率(Percent Profitable)。 勝ちトレード数 ÷ 全トレード数。勝率50%でもプロフィットファクターが高ければ収益性はある。勝率だけに注目しないこと。
プロフィットファクター(Profit Factor)。 総利益 ÷ 総損失。1.0なら損益トントン、1.5以上なら優秀、2.0以上はかなり良い。ただし、トレード数が少ないと偶然で2.0以上になることもある。
最大ドローダウン(Max Drawdown)。 資産がピークからどれだけ落ち込んだかの最大値。これが許容範囲を超えるストラテジーは、リアル運用で精神的に耐えられない。一般的に資金の20%以内が目安。
トレード数(Total Trades)。 バックテストの信頼性を左右する。30回未満のトレードでは統計的に有意ではない。最低100回以上のトレード数を目指す。
平均トレード(Avg Trade)。 1回のトレードあたりの平均損益。これが手数料+スリッページより大きくなければ、実運用では負ける。
Pineスクリプトの基礎をもっと詳しく知りたい方はこちら。
→ Pineスクリプト(Pine Script)とは?できること・始め方を完全解説
バックテストの落とし穴 — 数字を鵜呑みにしない
バックテストの数字が良くても、リアル運用で勝てるとは限らない。よくある落とし穴を知っておこう。
カーブフィッティング(過剰最適化)。 パラメータを微調整して、過去データに完璧にフィットさせること。EMA期間を12→13に変えたら利益が20%増えた。さらに14にしたら25%。こうやって過去データに最適化したパラメータは、将来の相場では機能しない。
対策は「パラメータの安定領域を探す」こと。EMA期間10〜16のどれでもプロフィットファクターが1.3以上あるなら、そのストラテジーは堅牢だ。特定の1つの値でだけ良い結果が出るなら、それはカーブフィッティングの可能性が高い。
手数料・スリッページの無視。 デフォルト設定(手数料0、スリッページ0)でバックテストすると、現実よりもかなり良い数字が出る。特にスキャルピング系のストラテジーは、手数料を入れた途端に収益がマイナスに転じることがある。
バーの終値ベースの約定。 TradingViewのストラテジーテスターは、基本的にバーの終値(または次のバーの始値)で約定を計算する。バー内の値動き(ヒゲ部分)は考慮されない。リアルタイムでは、バーの途中で条件が成立してエントリーするため、バックテスト通りの価格で約定しないことがある。
生存者バイアス。 上場廃止になった銘柄はTradingViewのチャートから消えている。生き残った銘柄だけでバックテストすると、結果が良く見える傾向がある。
堅牢性を確かめる方法
カーブフィッティングを避け、リアル運用でも通用するストラテジーかどうかを確認する方法。
複数銘柄テスト。 USDJPY、EURUSD、GBPUSD、XAUUSD(ゴールド)など、複数の銘柄で同じストラテジーを走らせる。1つの銘柄でしか機能しないストラテジーは汎用性が低い。
複数時間足テスト。 15分足、1時間足、4時間足、日足で同じストラテジーをテストする。特定の時間足でだけ良い結果が出る場合は、その時間足のデータ特性に依存している可能性がある。
分割テスト(イン/アウトサンプル)。 過去データを前半と後半に分ける。前半でパラメータを決め、後半でそのパラメータのまま検証する。前半は良いが後半は悪い、というパターンならカーブフィッティングの兆候だ。
Pineスクリプトでは、time関数を使ってテスト期間を分割できる。
//@version=6
strategy("分割テスト", overlay=true)
// テスト期間の分割
cutoff = timestamp(2024, 1, 1)
isInSample = time < cutoff // 2024年以前 = パラメータ調整用
isOutSample = time >= cutoff // 2024年以降 = 検証用
// 背景色でイン/アウトサンプルを可視化
bgcolor(isInSample ? color.new(color.blue, 95) : color.new(color.green, 95))
パラメータ感度分析。 EMA期間を10→12→14→16と変えたとき、プロフィットファクターが1.5→1.8→0.5→1.6と不安定に変動するなら、そのストラテジーはパラメータに敏感すぎる。1.3→1.4→1.5→1.4のように安定的に推移するなら、堅牢性が高い。
strategy.exit()の応用 — 決済ロジックを磨く
多くの初心者は、エントリーロジックに時間をかけすぎて、決済ロジックを軽視する。実際には、「いつ出るか」の方が収益に大きく影響する。
トレーリングストップ。 利益が伸びたら損切りラインを切り上げる。
strategy.exit("Trail", "Long",
trail_points=50, // 50ティック利益が出たらトレーリング開始
trail_offset=20) // 高値から20ティック逆行で決済
TP/SLとトレーリングの併用。 利確ラインに達する前にトレーリングで利益を確保する。
strategy.exit("複合決済", "Long",
profit=200, // 200ティックで利確
loss=100, // 100ティックで損切
trail_points=80, // 80ティック利益でトレーリング開始
trail_offset=30) // 高値から30ティック逆行で決済
時間ベースの決済。 一定時間(バー数)が経過してもTP/SLに到達しない場合、ポジションを閉じる。含み損が膨らむ前に撤退するロジック。
//@version=6
strategy("時間決済", overlay=true)
var int entryBar = 0
if ta.crossover(ta.ema(close, 12), ta.ema(close, 26))
strategy.entry("Long", strategy.long)
entryBar := bar_index
// エントリーから10バー経過で強制クローズ
if strategy.position_size > 0 and bar_index - entryBar >= 10
strategy.close("Long", comment="時間切れ")
ストラテジーからインジケーターへの変換
バックテストで有効性を確認したストラテジーは、実際のトレードではインジケーターとして使う。strategy()をindicator()に変え、strategy.entry()をplotshape()に、strategy.exit()をalertcondition()に置き換える。
//@version=6
// strategy("EMAクロス", overlay=true) // ← コメントアウト
indicator("EMAクロス シグナル", overlay=true)
fast = ta.ema(close, 12)
slow = ta.ema(close, 26)
longCond = ta.crossover(fast, slow)
shortCond = ta.crossunder(fast, slow)
// strategy.entry() の代わりにシグナル表示
plotshape(longCond, "ロング", shape.triangleup, location.belowbar, color.green, size=size.small)
plotshape(shortCond, "ショート", shape.triangledown, location.abovebar, color.red, size=size.small)
// アラート条件(スマホ通知用)
alertcondition(longCond, "ロングシグナル", "EMAゴールデンクロス")
alertcondition(shortCond, "ショートシグナル", "EMAデッドクロス")
plot(fast, "短期EMA", color.blue)
plot(slow, "長期EMA", color.red)
ストラテジーは「過去データでの検証」、インジケーターは「リアルタイムでのシグナル表示とアラート」。この2つをセットで作ることで、検証済みの手法をリアルトレードに活かせる。
よくある質問
Q: ストラテジーとインジケーターの違いは?
ストラテジーはstrategy()で宣言し、仮想の売買を実行してバックテスト結果を表示する。インジケーターはindicator()で宣言し、チャート上に描画する。ストラテジーは検証用、インジケーターはリアルトレード用。
Q: ストラテジーで自動売買はできる?
Pineスクリプト単体では取引所に注文を送れない。ただし、alertcondition()やアラートのWebhook機能を使えば、外部の自動売買システムと連携できる。
Q: バックテスト期間はどのくらい必要?
最低でも100トレード以上が出る期間。日足なら2〜5年、1時間足なら半年〜1年、15分足なら3〜6ヶ月が目安。短すぎると統計的に信頼できない。
Q: 勝率とプロフィットファクター、どちらが重要?
プロフィットファクター。勝率40%でもプロフィットファクター2.0なら十分に利益が出る(損小利大型)。逆に勝率80%でもプロフィットファクターが1.0以下なら長期的に負ける(コツコツドカン型)。
Q: process_orders_on_closeはtrueにすべき?
基本はfalse(デフォルト)のままにする。trueにすると「バーの終値を見てからそのバーの終値で約定する」という、リアルでは不可能な動作になり、バックテスト結果が過大評価される。
まとめ
Pineスクリプトのストラテジーは「コードを書いてバックテストする」までは簡単だ。難しいのは「その結果が信頼できるかを判断する」こと。
手数料とスリッページは必ず設定する。カーブフィッティングを避けるため、パラメータの安定領域を探す。複数銘柄・時間足でテストして堅牢性を確認する。分割テストでイン/アウトサンプルの成績を比較する。
バックテストの数字が良いことと、リアルで勝てることは別の話だ。この記事で紹介した検証手法を使って、「本当に使えるストラテジー」を見極めてほしい。













