熊本で震度7の地震がありました。
今年、西南戦争を巡る旅として熊本に旅行に行ったばかりなので、他人事とは思えず、大変驚いています。復旧作業中だった熊本城の石垣も一部崩れてしまったとのことで悲しい気持ちです。
熊本でお世話になった郷土料理のお店や地酒のお店の人たちも無事でありますように。
今回も作業を続けていきます。
はじめに
前回は、ポジションの保有時間別に零号機の成績を分析しました。

その結果、1~4時間で終了した取引では好成績だった一方、4~8時間では成績が大きく悪化するという特徴が見つかりました。
今回は少し視点を変えて、
「勝ち負けがどのように連続しているのか」
を分析します。
これまで勝率やPF、総損益などを調べてきましたが、実際にEAを運用することを考えると、
最大で何連敗するのか
何連勝くらいまで起こるのか
1~2連敗はどの程度の頻度で発生するのか
といった情報も重要です。
例えばバックテスト上では9連敗が通常の範囲内だったと分かっていれば、実運用で5連敗しただけで慌ててシステムを停止する必要はないかもしれません。
逆に、過去にはほとんど発生しなかったような長い連敗が続けば、システムや相場環境に何らかの変化が起きている可能性を考える材料になります。
今回はPythonを使って、零号機の連勝・連敗の特徴を観測してみます。
今回調べること
今回は、次の項目を調べます。
- 最大連勝数
- 最大連敗数
- 平均連勝数
- 平均連敗数
- 連勝・連敗の発生回数
- 連勝数・連敗数ごとの分布
- それぞれの連勝・連敗期間に発生した損益
考え方としては、
勝ち
勝ち
勝ち
負け
負け
勝ち
という取引があれば、
3連勝
2連敗
1勝
というグループに分けます。
そのためにはまず、取引を時系列順に並べる必要があります。
1.取引を決済時間順に並べる
まず、分析対象のtradesを決済時間順に並べます。
streak_trades = (
trades
.sort_values("決済時間")
.reset_index(drop=True)
.copy()
)
連勝・連敗を調べるには、
実際に取引が終了した順番
が重要です。
そこで、
.sort_values("決済時間")
を使い、決済時間の古いものから順番に並べます。
続く、
.reset_index(drop=True)
では、並べ替えた後の行番号を、
0
1
2
3
4
...
と振り直します。
drop=Trueを指定することで、並べ替える前の古いインデックスは列として残しません。
2.各取引を「勝ち・負け・引き分け」に分類する
次に、各取引の損益を見て結果を分類します。
def classify_result(profit):
if profit > 0:
return "勝ち"
elif profit < 0:
return "負け"
else:
return "引き分け"
この関数では、
損益 > 0
→ 勝ち
損益 < 0
→ 負け
損益 = 0
→ 引き分け
と判定します。
そして、
streak_trades["結果"] = (
streak_trades["損益"]
.apply(classify_result)
)
によって、損益列のすべての値にclassify_result()を適用します。
例えば、
損益 結果
+500 勝ち
+300 勝ち
-1,000 負け
-800 負け
+400 勝ち
という新しい結果列が作られます。
3.取引順を付ける
続いて、取引に番号を付けます。
streak_trades["取引順"] = range(
1,
len(streak_trades) + 1
)
range()によって、
1
2
3
4
5
...
という連番を作っています。
後で、
何番目の取引から何番目の取引まで連敗していたのか
を確認できるようにするためです。
4..shift()で一つ前の取引と比較する
ここからが今回の重要な部分です。
result_changed = (
streak_trades["結果"]
!= streak_trades["結果"].shift()
)
.shift()は、データを指定した行数だけずらす処理です。
例えば、
現在 1つ前
勝ち NaN
勝ち 勝ち
負け 勝ち
負け 負け
勝ち 負け
という状態を作れます。
そして、
!=
で現在の結果と一つ前の結果が違うかを比較します。
すると、
勝ち True
勝ち False
負け True
負け False
勝ち True
となります。
つまりTrueになっているところが、
勝ち→負け、負け→勝ちなど、結果が切り替わった場所
です。
5..cumsum()で連続グループを作る
次に、
streak_trades["連続グループ"] = (
result_changed.cumsum()
)
とします。
.cumsum()は累積合計を求めるメソッドです。
Pythonでは、
True → 1
False → 0
として計算できます。
先ほどの、
True
False
True
False
True
に.cumsum()を使うと、
1
1
2
2
3
となります。
したがって、
勝ち グループ1
勝ち グループ1
負け グループ2
負け グループ2
勝ち グループ3
となります。
これによって、連続した勝ち・負けをひとまとまりとして扱えるようになります。
6.連勝・連敗グループごとに集計する
作成した連続グループを使って集計します。
streak_groups = (
streak_trades
.groupby("連続グループ")
.agg(
結果=("結果", "first"),
連続回数=("結果", "size"),
開始取引=("取引順", "min"),
終了取引=("取引順", "max"),
開始時間=("決済時間", "min"),
終了時間=("決済時間", "max"),
期間損益=("損益", "sum"),
平均損益=("損益", "mean")
)
.reset_index(drop=True)
)
ここでは1つの連勝・連敗グループについて、
結果
連続回数
開始した取引
終了した取引
開始時間
終了時間
期間損益
平均損益
をまとめています。
例えば3連敗なら、
結果 負け
連続回数 3
開始取引 100
終了取引 102
期間損益 -3,500円
という1行のデータになります。
7.引き分けを除外する
今回は連勝・連敗を分析したいので、
streak_groups = streak_groups[
streak_groups["結果"].isin(
[
"勝ち",
"負け"
]
)
].copy()
として、勝ち・負けのグループだけを残します。
ただし、ここには一つ注意点があります。
引き分けが途中に入った場合、連勝・連敗はそこで一度途切れたものとして扱われます。
例えば、
勝ち
勝ち
引き分け
勝ち
勝ち
は「4連勝」ではなく、
2連勝
引き分け
2連勝
として判定されます。
今回はこの定義で分析します。
8.連勝データと連敗データに分ける
続いて、
winning_streaks = streak_groups[
streak_groups["結果"] == "勝ち"
].copy()
losing_streaks = streak_groups[
streak_groups["結果"] == "負け"
].copy()
として、
winning_streaks
→ 連勝だけ
losing_streaks
→ 連敗だけ
に分けます。
これで、それぞれを個別に分析できます。
9.最大・平均連勝数と連敗数を求める
最大連勝数は、
max_winning_streak = (
winning_streaks["連続回数"].max()
if not winning_streaks.empty
else 0
)
最大連敗数は、
max_losing_streak = (
losing_streaks["連続回数"].max()
if not losing_streaks.empty
else 0
)
で求めます。
.max()は最大値を取得するメソッドです。
また、
if not winning_streaks.empty
では、
winning_streaksにデータが存在するか
を確認しています。
データが存在しない状態で計算しようとして問題が起きるのを防ぐためです。
平均についても同様に、
average_winning_streak = (
winning_streaks["連続回数"].mean()
if not winning_streaks.empty
else 0
)
average_losing_streak = (
losing_streaks["連続回数"].mean()
if not losing_streaks.empty
else 0
)
とします。
10.最大連勝・最大連敗が発生した期間を抽出する
最大値だけでは、
いつ最大連敗が発生したのか
が分かりません。
そこで、
max_winning_streak_rows = winning_streaks[
winning_streaks["連続回数"]
== max_winning_streak
].copy()
max_losing_streak_rows = losing_streaks[
losing_streaks["連続回数"]
== max_losing_streak
].copy()
とします。
例えば最大連敗数が9なら、
losing_streaks["連続回数"]
== 9
となる行をすべて抽出します。
最大連敗が複数回発生していれば、そのすべてが残ります。
11.連勝・連敗数ごとの発生回数を調べる
続いて、
1連勝は何回あったのか
2連勝は何回あったのか
3連勝は何回あったのか
を調べます。
winning_streak_distribution = (
winning_streaks["連続回数"]
.value_counts()
.sort_index()
.rename_axis("連勝数")
.reset_index(name="発生回数")
)
.value_counts()は、同じ値が何回登場したのかを数えるメソッドです。
例えば、
連続回数
1
2
1
3
2
1
なら、
1 → 3回
2 → 2回
3 → 1回
となります。
.sort_index()で連勝数の小さい順に並べ、
.rename_axis("連勝数")
.reset_index(name="発生回数")
によって、
連勝数 発生回数
1 3
2 2
3 1
という通常のDataFrameに整えます。
連敗も同じです。
losing_streak_distribution = (
losing_streaks["連続回数"]
.value_counts()
.sort_index()
.rename_axis("連敗数")
.reset_index(name="発生回数")
)
12.分析結果を表示する
基本情報を表示します。
print(
"\n===== 連勝・連敗分析 ====="
)
print(
f"最大連勝数: {max_winning_streak}回"
)
print(
f"最大連敗数: {max_losing_streak}回"
)
print(
f"平均連勝数: {average_winning_streak:.2f}回"
)
print(
f"平均連敗数: {average_losing_streak:.2f}回"
)
print(
f"連勝の発生回数: {len(winning_streaks)}回"
)
print(
f"連敗の発生回数: {len(losing_streaks)}回"
)
さらに、最大連勝・最大連敗が発生した期間も表示します。
今回は損益金額だけ小数点以下を四捨五入します。
max_winning_streak_display = (
max_winning_streak_rows[
[
"連続回数",
"開始時間",
"終了時間",
"期間損益"
]
].copy()
)
max_winning_streak_display["期間損益"] = (
max_winning_streak_display["期間損益"]
.round(0)
)
連敗側も同様です。
max_losing_streak_display = (
max_losing_streak_rows[
[
"連続回数",
"開始時間",
"終了時間",
"期間損益"
]
].copy()
)
max_losing_streak_display["期間損益"] = (
max_losing_streak_display["期間損益"]
.round(0)
)
これで例えば、
-9274.63
なら、
-9275
として表示されます。
13.CSVとして保存する
今回の分析結果もresultsフォルダに保存します。
streak_trades.to_csv(
"results/streak_trades.csv",
index=False,
encoding="utf-8-sig"
)
streak_groups_output = streak_groups.copy()
streak_groups_output[
[
"期間損益",
"平均損益"
]
] = (
streak_groups_output[
[
"期間損益",
"平均損益"
]
].round(0)
)
streak_groups_output.to_csv(
"results/streak_groups.csv",
index=False,
encoding="utf-8-sig"
)
winning_streak_distribution.to_csv(
"results/winning_streak_distribution.csv",
index=False,
encoding="utf-8-sig"
)
losing_streak_distribution.to_csv(
"results/losing_streak_distribution.csv",
index=False,
encoding="utf-8-sig"
)
streak_groups.csvについても、期間損益と平均損益だけ1円単位に四捨五入します。
以下の4つのcsvファイルが出力されます。
results/streak_trades.csv
results/streak_groups.csv
results/winning_streak_distribution.csv
results/losing_streak_distribution.csv
14.連勝・連敗の分布をグラフ化する
最後に、連勝数と連敗数の発生回数を棒グラフにします。
連勝側は、
plt.figure(
figsize=(10, 6)
)
plt.bar(
winning_streak_distribution["連勝数"],
winning_streak_distribution["発生回数"]
)
plt.title(
"Winning Streak Distribution"
)
plt.xlabel(
"Number of Consecutive Wins"
)
plt.ylabel(
"Frequency"
)
plt.xticks(
winning_streak_distribution["連勝数"]
)
plt.tight_layout()
plt.savefig(
"results/winning_streak_distribution.png",
dpi=150
)
plt.close()
連敗側も同様です。
plt.figure(
figsize=(10, 6)
)
plt.bar(
losing_streak_distribution["連敗数"],
losing_streak_distribution["発生回数"]
)
plt.title(
"Losing Streak Distribution"
)
plt.xlabel(
"Number of Consecutive Losses"
)
plt.ylabel(
"Frequency"
)
plt.xticks(
losing_streak_distribution["連敗数"]
)
plt.tight_layout()
plt.savefig(
"results/losing_streak_distribution.png",
dpi=150
)
plt.close()
これによって、
1連勝・1連敗
2連勝・2連敗
3連勝・3連敗
……
が、それぞれ何回発生しているのかを視覚的に確認できます。
出力結果


分析結果
連勝・連敗の発生回数をグラフにしてみました。
まず連勝を見ると、最も多いのは1連勝(次の取引で負けるケース)の152回でした。
その後は、
- 2連勝:100回
- 3連勝:58回
- 4連勝:32回
- 5連勝:18回
- 6連勝:15回
- 7連勝:13回
- 8連勝:9回
- 9連勝:4回
- 10連勝:2回
- 11連勝:1回
となっています。
連勝が長くなるほど発生回数は減っていますが、6~8連勝もそれなりに発生しており、最大では11連勝していました。
連勝グループ全体では404回発生しており、平均連勝数は約2.63回でした。
一方、連敗はかなり違った形になっています。
最も多いのは1連敗の246回で、2連敗が100回、3連敗が35回、4連敗が16回でした。
5連敗以上になると急激に少なくなり、
5連敗:2回
6連敗:1回
7連敗:1回
8連敗:0回
9連敗:2回
となっています。
連敗グループは403回発生し、平均連敗数は約1.63回でした。
最大連勝は11連勝、最大連敗は9連敗です。
ここで興味深いのは、単純に最大値を比較することよりも、連勝と連敗の分布の形がかなり違うことです。
平均では、
平均連勝数:約2.63回
平均連敗数:約1.63回
となっており、勝ち始めると複数回続くことがある一方、負けは1回で終わるケースがかなり多いことが分かりました。
これまでの分析では零号機の勝率が60%を超えていることが分かっていましたが、今回の分析によって、単に「勝ち取引が多い」だけではなく、勝ちはある程度まとまって発生し、負けは比較的短く終わる傾向があることも見えてきました。
ただし、ここで注意したいのは、
「連勝したから次も勝ちやすい」「連敗したから次も負けやすい」とは、まだ言えない
ということです。
今回調べたのは、あくまで過去の取引結果を並べて「何連勝・何連敗が何回発生したか」を集計したものです。
例えば、9連敗が2回しかなかったからといって、「8連敗した後は次に勝つ可能性が高い」と結論付けることはできません。
それを調べるには、連勝・連敗の長さだけでなく、その後の取引結果や発生時の相場環境まで分析する必要があります。
今回の結果からは、零号機には、
勝ちは比較的連続しやすく、負けは単発で終わることが多い。しかし、まれに長い連敗も発生する。
という特徴があることが分かりました。
この「まれに発生する長い連敗」は、今後リスク管理を考えるうえでも重要になりそうです。
最大連敗・連勝の日時
最大9連敗については、実際のstreak_groups.csvを見ると、9連敗が2回あります。
そのうち確認できる一例は、
2023/3/28 ~ 2023/4/11
9連敗
期間損益:-8,073円
です。
さらに最大11連勝は、
2021/6/9 ~ 2021/6/25
11連勝
期間損益:+8,983円
でした。
次回
今回は、取引結果を時系列に並べることで、零号機の連勝・連敗の特徴を調べました。
これまで、
- 月別損益
- エントリー・決済時間帯
- エントリー・決済曜日
- 保有時間
- 連勝・連敗
と、さまざまな角度から零号機を観察してきました。
しかし、まだ見ていない重要なものがあります。
それが、ドローダウンです。
連敗数が多ければ必ず大きなドローダウンになるとは限りません。
小さな損失が9回続く場合と、1回の大きな損失が発生する場合では、資産への影響はまったく違います。
そこで次回は、資産残高の推移からドローダウンを計算し、
「零号機は過去にどの程度の資産減少を経験してきたのか」
を調べてみます。
連勝・連敗という「回数」の分析から、次は資産そのものの落ち込みへ。
零号機のリスク面をもう少し詳しく観測していきます。

今回追記したコード全文
追加したコード全文
# ==============================
# 連勝・連敗分析
# ==============================
# 決済時間順に並べる
streak_trades = (
trades
.sort_values("決済時間")
.reset_index(drop=True)
.copy()
)
# ==============================
# 勝ち・負け・引き分けに分類
# ==============================
def classify_result(profit):
if profit > 0:
return "勝ち"
elif profit < 0:
return "負け"
else:
return "引き分け"
streak_trades["結果"] = (
streak_trades["損益"]
.apply(classify_result)
)
# ==============================
# 取引順を追加
# ==============================
streak_trades["取引順"] = range(
1,
len(streak_trades) + 1
)
# ==============================
# 結果が切り替わった場所を判定
# ==============================
result_changed = (
streak_trades["結果"]
!= streak_trades["結果"].shift()
)
# ==============================
# 連続グループを作成
# ==============================
streak_trades["連続グループ"] = (
result_changed.cumsum()
)
# ==============================
# 連勝・連敗グループを集計
# ==============================
streak_groups = (
streak_trades
.groupby("連続グループ")
.agg(
結果=(
"結果",
"first"
),
連続回数=(
"結果",
"size"
),
開始取引=(
"取引順",
"min"
),
終了取引=(
"取引順",
"max"
),
開始時間=(
"決済時間",
"min"
),
終了時間=(
"決済時間",
"max"
),
期間損益=(
"損益",
"sum"
),
平均損益=(
"損益",
"mean"
)
)
.reset_index(drop=True)
)
# 勝ち・負けだけ残す
streak_groups = streak_groups[
streak_groups["結果"].isin(
[
"勝ち",
"負け"
]
)
].copy()
# ==============================
# 連勝・連敗に分割
# ==============================
winning_streaks = streak_groups[
streak_groups["結果"] == "勝ち"
].copy()
losing_streaks = streak_groups[
streak_groups["結果"] == "負け"
].copy()
# ==============================
# 基本情報
# ==============================
max_winning_streak = (
winning_streaks["連続回数"].max()
if not winning_streaks.empty
else 0
)
max_losing_streak = (
losing_streaks["連続回数"].max()
if not losing_streaks.empty
else 0
)
average_winning_streak = (
winning_streaks["連続回数"].mean()
if not winning_streaks.empty
else 0
)
average_losing_streak = (
losing_streaks["連続回数"].mean()
if not losing_streaks.empty
else 0
)
# ==============================
# 最大連勝・最大連敗期間
# ==============================
max_winning_streak_rows = winning_streaks[
winning_streaks["連続回数"]
== max_winning_streak
].copy()
max_losing_streak_rows = losing_streaks[
losing_streaks["連続回数"]
== max_losing_streak
].copy()
# 表示用
max_winning_streak_display = (
max_winning_streak_rows[
[
"連続回数",
"開始時間",
"終了時間",
"期間損益"
]
].copy()
)
max_winning_streak_display["期間損益"] = (
max_winning_streak_display["期間損益"]
.round(0)
)
max_losing_streak_display = (
max_losing_streak_rows[
[
"連続回数",
"開始時間",
"終了時間",
"期間損益"
]
].copy()
)
max_losing_streak_display["期間損益"] = (
max_losing_streak_display["期間損益"]
.round(0)
)
# ==============================
# 結果表示
# ==============================
print(
"\n===== 連勝・連敗分析 ====="
)
print(
f"最大連勝数: {max_winning_streak}回"
)
print(
f"最大連敗数: {max_losing_streak}回"
)
print(
f"平均連勝数: {average_winning_streak:.2f}回"
)
print(
f"平均連敗数: {average_losing_streak:.2f}回"
)
print(
f"連勝の発生回数: {len(winning_streaks)}回"
)
print(
f"連敗の発生回数: {len(losing_streaks)}回"
)
print(
"\n===== 最大連勝期間 ====="
)
print(
max_winning_streak_display
)
print(
"\n===== 最大連敗期間 ====="
)
print(
max_losing_streak_display
)
# ==============================
# 連勝数ごとの分布
# ==============================
winning_streak_distribution = (
winning_streaks["連続回数"]
.value_counts()
.sort_index()
.rename_axis("連勝数")
.reset_index(name="発生回数")
)
# ==============================
# 連敗数ごとの分布
# ==============================
losing_streak_distribution = (
losing_streaks["連続回数"]
.value_counts()
.sort_index()
.rename_axis("連敗数")
.reset_index(name="発生回数")
)
print(
"\n===== 連勝数ごとの発生回数 ====="
)
print(
winning_streak_distribution
)
print(
"\n===== 連敗数ごとの発生回数 ====="
)
print(
losing_streak_distribution
)
# ==============================
# CSV保存
# ==============================
streak_trades.to_csv(
"results/streak_trades.csv",
index=False,
encoding="utf-8-sig"
)
streak_groups_output = (
streak_groups.copy()
)
streak_groups_output[
[
"期間損益",
"平均損益"
]
] = (
streak_groups_output[
[
"期間損益",
"平均損益"
]
].round(0)
)
streak_groups_output.to_csv(
"results/streak_groups.csv",
index=False,
encoding="utf-8-sig"
)
winning_streak_distribution.to_csv(
"results/winning_streak_distribution.csv",
index=False,
encoding="utf-8-sig"
)
losing_streak_distribution.to_csv(
"results/losing_streak_distribution.csv",
index=False,
encoding="utf-8-sig"
)
# ==============================
# 連勝分布グラフ
# ==============================
plt.figure(
figsize=(10, 6)
)
plt.bar(
winning_streak_distribution["連勝数"],
winning_streak_distribution["発生回数"]
)
plt.title(
"Winning Streak Distribution"
)
plt.xlabel(
"Number of Consecutive Wins"
)
plt.ylabel(
"Frequency"
)
plt.xticks(
winning_streak_distribution["連勝数"]
)
plt.tight_layout()
plt.savefig(
"results/winning_streak_distribution.png",
dpi=150
)
plt.close()
# ==============================
# 連敗分布グラフ
# ==============================
plt.figure(
figsize=(10, 6)
)
plt.bar(
losing_streak_distribution["連敗数"],
losing_streak_distribution["発生回数"]
)
plt.title(
"Losing Streak Distribution"
)
plt.xlabel(
"Number of Consecutive Losses"
)
plt.ylabel(
"Frequency"
)
plt.xticks(
losing_streak_distribution["連敗数"]
)
plt.tight_layout()
plt.savefig(
"results/losing_streak_distribution.png",
dpi=150
)
plt.close()
おまけ
今、この本を読んでいます。
ユヴァル・ノア・ハラリの「NEXUS」を詠んだ後、同じ雰囲気の本ということで、興味があり手に取ってみました。
大陸や民族によって、こうも文明文化の発展度合いが違うのはなぜなのか。
その答えを探すべく、人類誕生の700万年前からさかのぼり答えを探すというものです。
スペインのピサロがインカ帝国アタワルパを捕らえ、処刑した事実は知っていましたが、実際にどんな感じだったのか興味はありましたがずっと知らないままでした。
この本では、ピサロの下で働いていた部下が報告書で話している内容を引用することで、どのように占拠したのかが描写されています。
まだまだ途中ですが続きが気になる本です。
コメント