OANDA「先月の上位、下位200口座のトレード分析」の散布図を少し詳しく見てみる

OANDA散布図×Python分析 データ分析
OANDA散布図×Python分析

皆さん、こんにちは、ウルフです🐺

Xでは何度も呟いているOANDAさんの「先月の上位、下位200口座のトレード分析」について、今日はちょっと詳しくみていきます。といっても散布図だけ。

 

 

散布図が美しい

OANDAさんが公開している「過去6ヶ月間の勝率とリスクリワード比率の散布図」を見たことはありますか?これですこれ↓

美しい。キレイに溝(隙間)ができてる。見事に、上位トレーダーと下位トレーダーの差を見事に可視化している。初めて見たときは感動したんだが、なかなか伝わらない笑

 

 

何が素晴らしいのか

散布図を見ると一目瞭然で:

  • 勝率(横軸): トレードで勝つ確率
  • リスクリワード比率(縦軸): 平均利益÷平均損失

この2つの指標で、収益プラスのトレーダーと収益マイナスのトレーダーがくっきり分かれているんだなこれが。

「勝率が高ければいい」わけでも「リスクリワードが高ければいい」わけでもない。

両方のバランスが重要というのが、この散布図を見れば誰でも理解できます。

OANDAさんのサイトにはリスクリワード比率とありますが、中身は「平均利益÷平均損失」なのでペイオフレシオのことですな。以降この記事では、リスクリワードとあったらペイオフレシオのことだと思ってね。

また、点がバラけていることから

  • 勝ち組にもいろんな勝ち方がある
  • 負け組にもいろんな負け方がある
  • 勝率は低くてもいいからリスクリワードを上げるべきって、アドバイスはウソ
  • リスクリワードは悪くてもいいから勝率を上げようって、アドバイスもウソ
  • 要は自分にあった戦略を打ち立てるのが大事

ってとこですかね。もちろんボリュームゾーン(点が多く存在しているところ)はあるので、今勝ってない人はとりあえずこの辺目指したらどう?ってのはある。

 

 

 

データを詳しく見る方法

ここからが本題です。

この散布図はPlotlyというJavaScriptライブラリで描画されています。
つまり、生データを取得できるんですな。それがあればもうこっちのもん😂

何ができるのか

取得した生データをPythonやエクセルに取り込めば:

  • より詳細な統計分析
  • 自分のトレードデータとの比較
  • 勝率とリスクリワードの最適バランスの検証
  • 下位が上位になるための最短パス(が分かるかも)

など、いろいろ分析できます。

 

 

Pythonで分析してみる

散布図データを取得して、Pythonで分析する手順を紹介します。エクセルは苦手なので各自頑張って笑

データ分析用Python準備

Jupyter notebookやColabに以下のコードを貼り付けておく。

import re
import json
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import japanize_matplotlib  # 日本語表示用

def parse_and_plot_oanda_plotly(raw_text):
    """
    OANDA(Plotly.js形式)のソースコードから
    name, color, x(勝率), y(リスクリワード) を抽出してプロットする
    """
    
    # ---------------------------------------------------------
    # 1. データの抽出 (テキスト解析)
    # ---------------------------------------------------------
    
    # データは {"customdata": ... } というブロックごとに分かれているため、
    # customdata を区切り文字としてデータを分割します
    chunks = raw_text.split('{"customdata"')
    
    # 最初のチャンクはゴミデータなので捨てる
    data_chunks = chunks[1:]
    
    extracted_data = []
    
    for chunk in data_chunks:
        # --- A. 名前の抽出 ("name":"...") ---
        # Unicodeエスケープされている場合(\u4e0a...)と、そのままの場合に対応
        name_match = re.search(r'"name":"(.*?)"', chunk)
        if not name_match:
            continue
        
        raw_name = name_match.group(1)
        # JSONデコードを使ってUnicodeエスケープ(\uXXXX)を日本語に戻す
        try:
            series_name = json.loads(f'"{raw_name}"')
        except:
            series_name = raw_name

        # --- B. 色の抽出 ("color":"#...") ---
        color_match = re.search(r'"color":"(#[a-fA-F0-9]{6})"', chunk)
        series_color = color_match.group(1) if color_match else "#333333"

        # --- C. X軸データ(勝率)の抽出 ("x":[...]) ---
        x_match = re.search(r'"x":\[(.*?)\]', chunk)
        if not x_match:
            continue
        # カンマ区切りの文字列を数値リストに変換
        x_str = x_match.group(1)
        x_values = [float(v) for v in x_str.split(',') if v.strip() != '']

        # --- D. Y軸データ(RR比)の抽出 ("y":[...]) ---
        y_match = re.search(r'"y":\[(.*?)\]', chunk)
        if not y_match:
            continue
        y_str = y_match.group(1)
        y_values = [float(v) for v in y_str.split(',') if v.strip() != '']

        # --- E. データフレーム化 ---
        # xとyの長さが合うものだけ処理
        if len(x_values) == len(y_values) and len(x_values) > 0:
            df_temp = pd.DataFrame({
                '勝率': x_values,
                'リスクリワード': y_values,
                'category': series_name,
                'color': series_color
            })
            extracted_data.append(df_temp)

    if not extracted_data:
        print("エラー: 有効なデータブロックが見つかりませんでした。テキストを確認してください。")
        return

    # 全データを結合
    df = pd.concat(extracted_data, ignore_index=True)
    
    # 凡例の順序を整える(OANDAの表示順序に合わせる)
    # 通常の出現順序: 上位100 -> 下位100 -> 上位101-200 -> 下位101-200 -> 6ヶ月連続
    legend_order = [
        "上位100", "下位100", 
        "上位101~200", "下位101~200", 
        "6ヶ月連続プラス"
    ]
    
    # ---------------------------------------------------------
    # 2. グラフ描画
    # ---------------------------------------------------------
    fig, ax = plt.subplots(figsize=(14, 9), facecolor='white')

    # 定義した順序に従ってプロット(順序に含まれないデータは出現順に追加)
    existing_categories = df['category'].unique()
    plot_order = [c for c in legend_order if c in existing_categories]
    
    # もし定義外のカテゴリがあれば末尾に追加
    for cat in existing_categories:
        if cat not in plot_order:
            plot_order.append(cat)

    for label in plot_order:
        subset = df[df['category'] == label]
        if subset.empty:
            continue
        
        color = subset['color'].iloc[0]
        
        ax.scatter(
            subset['勝率'], 
            subset['リスクリワード'], 
            label=label, 
            c=color, 
            s=30,          # 点のサイズ
            alpha=1.0,     # 透明度
            edgecolors='none',
            zorder=3       # グリッドの手前に表示
        )

    # ---------------------------------------------------------
    # 3. スタイル調整(OANDA風)
    # ---------------------------------------------------------
    ax.set_title("過去6ヶ月の勝率とリスクリワード比率の散布", fontsize=18, pad=20, weight='bold', color='#444444')
    
    # 軸ラベル
    ax.set_xlabel("勝率", fontsize=12, labelpad=10, color='#666666')
    ax.set_ylabel("リスクリワード比率", fontsize=12, labelpad=10, color='#666666')

    # 軸範囲
    ax.set_xlim(0, 100)
    ax.set_ylim(0, 5)

    # X軸をパーセント表示
    ax.xaxis.set_major_formatter(mtick.PercentFormatter())

    # グリッド線(薄いグレー)
    ax.grid(True, which='both', color='#e6e6e6', linestyle='-', linewidth=1.0)
    ax.set_axisbelow(True) # グリッドを最背面に

    # 枠線を消す(上と右)
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['left'].set_color('#cccccc')
    ax.spines['bottom'].set_color('#cccccc')

    # 凡例の設定(グラフ下部に横並び)
    leg = ax.legend(
        loc='upper center', 
        bbox_to_anchor=(0.5, -0.08), 
        ncol=5, 
        frameon=False, 
        fontsize=11,
        columnspacing=2.0,
        handletextpad=0.5
    )
    # 凡例の文字色
    for text in leg.get_texts():
        text.set_color('#555555')
        
    # マーカーサイズを凡例用に調整
    for handle in leg.legend_handles:
        handle.set_sizes([60.0])

    plt.tight_layout()
    plt.show()

# ==========================================
# ここにデータを貼り付けてください
# ==========================================
raw_text = """
(コピーした内容をここに貼り付け。この文章は削除してね。)

"""

# 実行
if __name__ == "__main__":
    # データ貼り付け確認用
    if len(raw_text) < 500:
        print("【注意】raw_text 変数にデータを貼り付けてから実行してください。")
    else:
        parse_and_plot_oanda_plotly(raw_text)

次にコードの(コピーした内容をここに貼り付け。この文章は削除してね。)の部分に入れるデータをサイトからゲットします。

 

データをゲットする

散布図上で右クリック → フレームのソースを表示をクリック

Ctrl+F(MacはCommand (⌘) + F)で表示される検索窓に「Plotly.newPlot( 」を入力して検索します。最後にスペースがあります。スペースがないと2件ヒットしちゃうので。その場合2件目が正解。

Plotly.newPlot( 以降をすべてコピーする。下図なら”8064⋯以降を全てコピーする

コピーしたら、先程のPythonコードにある(コピーした内容をここに貼り付け。この文章は削除してね。)の箇所に貼り付けて実行してください。下のように散布図が表示されるはず。

エラーが出たらチャッピー(ChatGPT)に聞いてね。

サイトの散布図と見比べて同じものが表示できているか確認してください。

 

  

 

色々分析してみる。

とりあえず平均と中央値が見たいなーってことで

  • 上位100と上位101-200
  • 下位100と下位101-200
  • 6ヶ月連続プラス

に分けてプロットします。曲線はリスクリワード(先述の通り正確にはペイオフレシオ)と勝率から求めた損益分岐となる線。★がそれぞれのグループの平均です。◆が中央値。

まあ、こんなもんかなという感想ですが、6月連続プラス勢の平均★よりも中央値◆がかなり右下(高勝率低ペイオフレシオ)に寄っているのは、ちょいと面白い。

 

 

各セルをカウントしてみる

てことは右下にけっこう人がいるの?ってことで、1つのセル(勝率5%区切り、ペイオフレシオ0.5区切り)ごとにそれぞれ何人いるか確認すると、右下にいっぱいいた(例えば、勝率95-100%、PR0-0.5に16人いて、うち15人が上位200位)。左隣も13人、33人といっぱいいる。そら中央値はそっちに寄りますな。

 

ちと分かりにくいかもしれないので、グループごとにカウント。ボリュームゾーンが分かりやすくなった。

逆に閑散としているゾーンも分かりやすくなりましたね。上位勢(左)と6ヶ月連続プラス(右)の勝ち組で勝率40%未満はほぼいないってことに注目。

 

 

バルサラの破産確率を入れてみる

次にバルサラ破産確率でも散布図に重ねてみっぺかな。各人がリスクにさらしている%が分からないので、2%と5%を表示。

秒スキャ勢はハイレバでやってるはずなので紫線を越えたいところ。
PF(プロフィットファクター)の線も入れといたので参考まで。

 

 

(簡易)効用を入れてみる

超簡単に言うとプロスペクト理論(累積プロスペクト理論)の代表的な推定では、「損失が利得よりどれくらい強く評価されるか」を表す損失回避係数 λ が 2前後〜2.5前後(特に2.25あたり)としてよく引用されます。これは「同額の損失は、同額の利得より2倍強の心理的インパクトを持ちやすい」という目安です(ただし λ は課題設定や母集団で結構ブレる)。

ここでは確率加重(起こりやすさの感じ方の歪み)や参照点などはひとまず無視しつつ、散布図の読み取りを助けるために 「勝率50%のとき、心理値 Utility=0 になる線」を“心理的中立線”として点線で表示してみました。

価値関数の曲率(α=0.88)は入れているので、Utility=0 は PR=λ(=2.25)そのものではなく、PR ≈ λ^(1/α)(ざっくり2.5)の位置になります(厳密なプロスペクト理論の適用というより、参考線です)。

見て分かる通り、なかなか高いハードルですね。勝ち組もだいぶ脱落しちゃう。

ま、僕はだいたい勝率75-80%、PR1あたりなので心理値0よりギリ上にいますけどねー(謎の自慢😁)。

なお、プロスペクト理論を提唱したダニエル・カーネマンとエイモス・トベルスキーの論文を読んだことがない方は、こちらからどうぞ

 

 

「勝ち組確率」のベイズ推定マップ

今見えている良さげな領域が、単なる人数の偏りや偶然ではなく、統計的にどれくらい堅いのかをチェックします。やり方はベイズ(二項-ベータ)で推定して勝ち組確率の期待値(信用区間:95%)を出すだけ。

右図を見ると分かる通り、人が少ないところは赤(偶然じゃね?)になるので右下の緑が濃いところを狙うのが良き。

下は上図右の拡大。ちょっとずれると確率がガクンと落ちるところは狙わないほうが良いので。緑が密集しているところ(青丸あたり)をターゲットに自分の成績を改善していくのがよさそう。

繰り返しになりますが、低勝率高PRで勝ってるやつもいるが、現実的にはそこを狙うよりは、高勝率低PRを狙うほうが筋が良いと言えそう。

 

 

クラスタリングしてみる

GMM(混合ガウスモデル)でクラスタリングしてみる。楕円が2重(濃い色と薄い色)になっているのは1σと2σ。

Style 4は良さげだが、楕円が鬼でかいのであまり参考にならない。Style 1と4が重なっているあたりが狙い目かな。

なお、Style2はペイオフレシオが5以上のグループで、だいぶ上の方にいるので見切れてます。

  

 

上位勢への最短パス

下位勢が上位勢(というかバルサラ破産確率<1%)に達する最短ルートを図示してみる。B、CはAの一部を拡大したもの。まあ青線に素直に向かっていきますわな。

これじゃ面白くないので、以下を追加してみる。

  • 「RR(PR)を上げる努力は、勝率を上げる努力に比べて、心理的に 約2.25倍 の負荷がかかる」と仮定し、より「楽な方(スコアが低い方)」を推奨ルートとして矢印を描く。
  • ただし、「勝率70%以上の人」がさらに勝率を上げるのは難しいかもなので、難易度ペナルティを重くする。

で、結果はこれ。うーん微妙。ちょっと矢印が倒れた(PRを上げるより勝率を上げることで青線を越えようとしている)かな。

 

ということで、あなたが下位ゾーンにいる場合は最短距離で青線超えを目指すよりは、少し倒す(PR向上の努力より勝率向上の努力をちょいと多めにする)のが良いかもしれない。

 

 

 

限界

色々書いてきましたが、お気付きの通り、

  • 上位勢にたまたま勝っただけで、その前はボロボロだった人が含まれている。
  • 下位勢にもたまたま負けただけで、直前までボロ勝ちだった人が含まれている。
  • 6ヶ月連続プラス勢は勝ってはいるが、さほど稼いでいないのかも。
  • サンプルサイズの限界がある。

なので、どこまで信じるか、本当に目指してもいいのかという疑問は拭えません。有用なデータではあるけど、盲信せず参考程度にしておきましょう。

 

 

 

データ分析、検証の重要性

データをちょいと深堀りすると見えてくるものがあります。

「小さく負けてもいい、大きく取るのが大事」
「リスクリワード2.5以上を目指せ」

こういう一般論、よく聞きますよね。

ホンマに?ホンマに?ホンマに?

って疑うことが大事ですな。なので、この記事も半分疑ってかかりましょう。

 

 

 

まとめ

  • OANDAさんの散布図は上位と下位の差が一目瞭然
  • Plotly形式のグラフを見たらとりあえず自分で分析しちゃいなよ
  • 勝っていない人は、まずは勝率60-70%かつペイオフレシオ1あたりを狙うのが良さそう

ってとこかしら。

そして、勝ってる人もボリュームゾーンから外れているなら、ちょっと気に留めておいてもいいかもしれませんね。

それでは今日も良いトレードを!

 


参考リンク:

  • OANDA オーダーブック・ポジションブック(散布図ページ)
    https://www.oanda.jp/lab-education/oanda_lab/oanda_rab/tradingdata/

コメント

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