ランダムウォークへの挑戦⑧ | Pythonでポジションの保有時間別に成績を分析する(2026/7/26)

未分類

最近大きな仕事が終わりました。

なかなかに調整が大変でしたが、形として見えるものを世に送り出せたその一端を担えたので、いい経験でした。

このあたりの話もいずれしたいと思います。

では、今日も進めていきます。

はじめに

前回は、零号機の取引成績を、

エントリー曜日決済曜日

に分けて分析しました。

ランダムウォークへの挑戦⑦ | Pythonでエントリー曜日と決済曜日から零号機を分析する(2026/7/23)
2075年。これは何の年?答えは、平均寿命を考えた時の私の享年。そんなことを考えていた今日この頃です。それでは作業を進めていきます。はじめに前回は、零号機の取引成績を「エントリー時間帯」と「決済時間帯」の2つに分けて分析しました。その結果、…

ここまで、

いつ入ったのか
いつ出たのか

という「入口」と「出口」を、時間帯・曜日という視点から観測してきました。

そこで今回は、その入口と出口のに注目します。

エントリー
    ↓
    │
    │ ← 今回分析する「保有時間」
    │
    ↓
決済

同じ零号機の取引でも、

  • 1時間以内に終わった取引
  • 数時間保有した取引
  • 1日以上持ち続けた取引

では、成績に違いがあるのでしょうか。

今回は、エントリー時間と決済時間の差からポジションの保有時間を計算し、保有時間帯ごとの勝率・損益・PFなどを比較してみます。


今回やること

第6回で、注文番号を使ってエントリー行と決済行を結合しました。

そのため、現在のtradesには既に、

注文番号
エントリー時間
決済時間
損益

が1取引1行で入っています。

今回は、

エントリー時間と決済時間
        ↓
      引き算
        ↓
    保有時間

を計算します。

そして、

1時間未満
1~2時間
2~4時間
4~8時間
8~24時間
1~3日
3日以上

というグループに分け、成績を比較します。


1.保有時間分析用のデータを作る

まず、現在のtradesをコピーします。

holding_trades = trades.copy()

第6回で既にエントリー時間と決済時間をtradesに結合しています。

そのため今回は、既に完成している1取引1行のデータをそのまま利用します。


2.日時が欠けている取引を除外する

続いて、

holding_trades = holding_trades.dropna(
    subset=[
        "エントリー時間",
        "決済時間"
    ]
).copy()

とします。

.dropna()は、欠損値が存在する行を取り除くための処理です。

今回は、

subset=[
    "エントリー時間",
    "決済時間"
]

と指定しているので、

エントリー時間または決済時間が欠けている取引を除外する

という意味になります。

保有時間を計算するには入口と出口の両方が必要なので、どちらかが欠けている取引は分析対象から外します。


3.保有時間を「分」で計算する

今回の中心となる処理です。

holding_trades["保有時間(分)"] = (
    holding_trades["決済時間"]
    - holding_trades["エントリー時間"]
).dt.total_seconds() / 60

まず、

holding_trades["決済時間"]
- holding_trades["エントリー時間"]

によって、

決済時間 − エントリー時間

を計算します。

例えば、

エントリー 10:00
決済    12:30

なら、

12:30 - 10:00
= 2時間30分

です。

ただし、この時点では単純な数値ではなく「時間差」のデータになっています。

そこで、

.dt.total_seconds()

を使って秒数へ変換します。

2時間30分なら、

9,000秒

です。

さらに、

/ 60

とすることで、

9,000 ÷ 60
= 150分

となります。

つまり、このコード全体で、

1取引ごとの保有時間を分単位で計算

しています。


4.保有時間を「時間」にも変換する

分析結果を読むときには、分だけでは少し分かりにくいので、時間単位の列も作ります。

holding_trades["保有時間(時間)"] = (
    holding_trades["保有時間(分)"]
    / 60
)

例えば、

150分

なら、

150 ÷ 60
= 2.5時間

です。

これで、

保有時間(分) 150
保有時間(時間)2.5

という2種類の表現を持てるようになります。


5.異常な保有時間を除外する

次に、

holding_trades = holding_trades[
    holding_trades["保有時間(分)"] >= 0
].copy()

とします。

通常、

決済時間

が、

エントリー時間

より前になることはありません。

もし保有時間がマイナスになっていたら、時刻データや結合に何らかの問題がある可能性があります。

そこで、

>= 0

という条件で、0分以上の正常なデータだけを残しています。


6.保有時間の区切りを決める

次に、保有時間をいくつかのグループへ分けます。

holding_bins = [
    0,
    60,
    120,
    240,
    480,
    1440,
    4320,
    float("inf")
]

数字はすべて「分」です。

つまり、

0分
60分      = 1時間
120分     = 2時間
240分     = 4時間
480分     = 8時間
1440分    = 24時間
4320分    = 3日
∞

という境界を作っています。

続いて、それぞれの範囲に名前を付けます。

holding_labels = [
    "1時間未満",
    "1~2時間",
    "2~4時間",
    "4~8時間",
    "8~24時間",
    "1~3日",
    "3日以上"
]

これで、

0~60分
→ 1時間未満

60~120分
→ 1~2時間

120~240分
→ 2~4時間

という対応を作ります。


7.pd.cut()で保有時間を分類する

実際にグループ分けするのが、

holding_trades["保有時間帯"] = pd.cut(
    holding_trades["保有時間(分)"],
    bins=holding_bins,
    labels=holding_labels,
    right=False,
    include_lowest=True
)

です。

pd.cut()は、連続した数値を指定した範囲ごとに分類するためのpandasの関数です。

例えば、

保有時間 45分
→ 1時間未満

保有時間 90分
→ 1~2時間

保有時間 180分
→ 2~4時間

という形になります。

ここで、

right=False

としているため、

0 <= x < 60

というように、左側を含み、右側を含まない区間になります。

したがって、ちょうど60分なら、

1時間未満

ではなく、

1~2時間

に分類されます。


8.保有時間帯ごとに成績を集計する

続いて、これまでの時間帯・曜日分析と同じように.groupby().agg()を使います。

holding_analysis = (
    holding_trades
    .groupby(
        "保有時間帯",
        observed=False
    )
    .agg(
        取引回数=("損益", "count"),
        勝ち数=(
            "損益",
            lambda x: (x > 0).sum()
        ),
        負け数=(
            "損益",
            lambda x: (x < 0).sum()
        ),
        総損益=("損益", "sum"),
        平均損益=("損益", "mean"),
        総利益=(
            "損益",
            lambda x: x[x > 0].sum()
        ),
        総損失=(
            "損益",
            lambda x: x[x < 0].sum()
        ),
        平均保有時間=(
            "保有時間(時間)",
            "mean"
        )
    )
)

今回は、

.groupby("保有時間帯")

なので、

1時間未満
1~2時間
2~4時間
……

ごとに取引をまとめます。

さらに今回は、これまでの分析項目に加えて、

平均保有時間=(
    "保有時間(時間)",
    "mean"
)

も計算しています。

これによって、それぞれのグループで実際に平均何時間ポジションを保有していたのかも確認できます。


9.勝率を計算する

勝率はこれまでと同じです。

holding_analysis["勝率"] = (
    holding_analysis["勝ち数"]
    / holding_analysis["取引回数"]
    * 100
)

つまり、

勝ち数 ÷ 取引回数 × 100

です。


10.PFを計算する

PFも同様です。

holding_analysis["PF"] = (
    holding_analysis["総利益"]
    / holding_analysis["総損失"]
    .abs()
    .replace(
        0,
        float("nan")
    )
)

総損失の絶対値で総利益を割っています。

また、

.replace(
    0,
    float("nan")
)

によって、損失が一度もなかったグループの0除算を防いでいます。


11.必要な列だけ残す

続いて、

holding_analysis = holding_analysis[
    [
        "取引回数",
        "勝ち数",
        "負け数",
        "勝率",
        "総損益",
        "平均損益",
        "PF",
        "平均保有時間"
    ]
]

として、最終的に確認したい列だけを残します。


12.表示用データでは損益を1円単位に丸める

損益金額については小数点以下を表示せず、1円単位に四捨五入します。

ただし、分析の計算そのものには元の値を残します。

そこで表示用のDataFrameを別に作ります。

holding_analysis_display = (
    holding_analysis.copy()
)

holding_analysis_display[
    [
        "総損益",
        "平均損益"
    ]
] = (
    holding_analysis_display[
        [
            "総損益",
            "平均損益"
        ]
    ]
    .round(0)
)

これによって、

12345.67円

なら、

12346円

として表示されます。

一方、

勝率
PF
平均保有時間

については、小数点以下も残します。


13.分析結果を表示する

print(
    "\n===== 保有時間別分析 ====="
)

print(
    holding_analysis_display.round(
        {
            "勝率": 2,
            "PF": 2,
            "平均保有時間": 2
        }
    )
)

これで、損益金額は整数、勝率・PF・平均保有時間は小数点以下2桁程度で確認できます。


14.保有時間の基本情報も確認する

グループ別分析だけでなく、保有時間全体の特徴も確認します。

print(
    "\n===== 保有時間の基本情報 ====="
)

print(
    f"分析対象取引数: "
    f"{len(holding_trades)}"
)

print(
    f"最短保有時間: "
    f"{holding_trades['保有時間(時間)'].min():.2f}時間"
)

print(
    f"平均保有時間: "
    f"{holding_trades['保有時間(時間)'].mean():.2f}時間"
)

print(
    f"中央値: "
    f"{holding_trades['保有時間(時間)'].median():.2f}時間"
)

print(
    f"最長保有時間: "
    f"{holding_trades['保有時間(時間)'].max():.2f}時間"
)

ここでは、

最短
平均
中央値
最長

を確認します。

特に平均値だけでなく中央値を見ることで、一部の非常に長い取引に平均値が引っ張られていないかも確認できます。


15.CSVとして保存する

分析結果を保存します。

holding_analysis_csv = (
    holding_analysis.copy()
)

holding_analysis_csv[
    [
        "総損益",
        "平均損益"
    ]
] = (
    holding_analysis_csv[
        [
            "総損益",
            "平均損益"
        ]
    ]
    .round(0)
)

holding_analysis_csv.to_csv(
    "results/holding_time_analysis.csv",
    encoding="utf-8-sig"
)

今回のCSVでも、損益金額は1円単位に丸めて保存します。

さらに、各取引の保有時間も保存します。

holding_trades.to_csv(
    "results/holding_time_trades.csv",
    index=False,
    encoding="utf-8-sig"
)

こちらは集計前のデータなので、

注文番号
エントリー時間
決済時間
保有時間
損益

などを取引単位で確認できます。


16.保有時間別損益をグラフ化する

最後に棒グラフを作ります。

グラフでは日本語が文字化けすることがあるため、横軸は英語にします。

holding_labels_en = [
    "Under 1 hour",
    "1-2 hours",
    "2-4 hours",
    "4-8 hours",
    "8-24 hours",
    "1-3 days",
    "3 days or more"
]

そして、

plt.figure(
    figsize=(12, 6)
)

plt.bar(
    holding_labels_en,
    holding_analysis["総損益"]
)

plt.title(
    "Profit by Holding Time"
)

plt.xlabel(
    "Holding Time"
)

plt.ylabel(
    "Profit"
)

plt.xticks(
    rotation=30
)

plt.axhline(
    0,
    linewidth=1
)

plt.tight_layout()

plt.savefig(
    "results/holding_time_profit.png",
    dpi=150
)

plt.close()

とします。

これで、

results/holding_time_profit.png

として、保有時間別の総損益を視覚化できます。

出力結果

出力結果

保有時間別の結果をグラフにしてみると、かなり特徴的な結果になりました。

まず、保有時間ごとの総損益は次のようになりました。

保有時間取引回数勝率総損益平均損益PF
1時間未満21958.45%+53,743円+245円1.98
1~2時間40785.50%+244,194円+600円6.27
2~4時間52379.92%+216,103円+413円6.98
4~8時間40921.03%-302,324円-739円0.10
8~24時間12047.50%-60,328円-503円0.69
1~3日3860.53%+20,686円+544円1.94
3日以上4100.00%+51,419円+12,855円

グラフを見て真っ先に目につくのが、4~8時間だけ極端に大きなマイナスになっていることです。

1~4時間の取引が非常に強い

まず好成績だったのが、1~4時間で決済された取引です。

特に1~2時間では、

取引回数:407回
勝率:85.50%
総損益:+244,194円
平均損益:+600円
PF:6.27

となりました。

さらに2~4時間でも、

取引回数:523回
勝率:79.92%
総損益:+216,103円
平均損益:+413円
PF:6.98

です。

PFだけを見ると、むしろ2~4時間が6.98で最も高くなっています。

1~2時間と2~4時間を合わせると930取引あります。

そのため、少数の取引がたまたま大きな利益を出したという結果でもなさそうです。

現時点では、

零号機はエントリー後1~4時間程度で決着した取引で、非常に良い成績を残している

という特徴が見えてきました。

ところが4時間を超えると一変する

最も衝撃的だったのが4~8時間です。

取引回数:409回
勝ち数:86回
負け数:323回
勝率:21.03%
総損益:-302,324円
平均損益:-739円
PF:0.10

総損益は-302,324円。

しかも409回と十分な取引数があります。

勝率はわずか21.03%、PFは0.10です。

1~2時間では勝率85.50%だったものが、4~8時間になると21.03%まで低下しています。

これはかなり大きな違いです。

グラフでも、

1~2時間    大幅プラス
2~4時間    大幅プラス
      ↓
4~8時間    大幅マイナス

という、はっきりとした境目が見えます。

零号機では、4時間以内に決着するかどうかが一つの大きな分岐点になっている可能性があります。

8~24時間もマイナス

4~8時間ほど極端ではありませんが、8~24時間についても、

取引回数:120回
勝率:47.50%
総損益:-60,328円
平均損益:-503円
PF:0.69

とマイナスになりました。

したがって今回の結果だけを見ると、

4時間を超えて24時間以内に決済された取引は、零号機にとって苦しい領域

となっています。

では「4時間で強制決済」すればいいのか?

ここで当然思いつくのが、

4時間を超えたポジションは強制的に決済すればいいのでは?

という改善案です。

しかし、今回の結果だけでは、まだそう結論づけることはできません。

ここはかなり重要です。

今回分析しているのは、

最終的に4~8時間保有された取引の最終損益

だからです。

例えば、5時間後に-1,000円で決済された取引があったとしても、

3時間59分時点   -1,500円
5時間時点       -1,000円

だった可能性があります。

この場合、4時間で強制決済すると、むしろ損失は大きくなります。

逆に、

3時間59分時点   +500円
5時間時点       -1,000円

だったなら、4時間付近で決済することで改善できた可能性があります。

今回の分析から分かるのはあくまで、

「4~8時間で終了した取引の成績が極端に悪かった」

という事実です。

「4時間で決済すれば成績が改善する」ことを証明したわけではありません。

この区別は、今後EAを改善するときに重要になりそうです。

1日を超えると再びプラスになる

さらに興味深いのが、保有時間が長くなると再びプラスになっていることです。

1~3日では、

取引回数:38回
勝率:60.53%
総損益:+20,686円
平均損益:+544円
PF:1.94

となっています。

つまり、

「保有時間が長いほど成績が悪くなる」という単純な関係ではありません。

今回見えているのは、

1時間未満       +
1~2時間        +++
2~4時間        +++
4~8時間        ----
8~24時間       -
1~3日          +

という形です。

4~24時間に、いわば「成績の谷」が存在しています。

これはなかなか面白い結果です。

3日以上は判断できない

3日以上では、

取引回数:4回
勝率:100%
総損益:+51,419円
平均損益:+12,855円

という驚くような数字になっています。

ただし、取引回数はわずか4回です。

しかも4回すべて勝っているため、総損失が0円となり、PFも計算できずNaNになっています。

したがって、

3日以上持てば儲かる

とはまったく言えません。

これは少数の特殊な取引によって生じた結果として、参考程度に扱います。


今回分かったこと

今回の保有時間別分析では、零号機にかなりはっきりした特徴が見つかりました。

特に、

1~4時間で終了した取引は非常に好成績である一方、4~8時間では成績が急激に悪化する

という結果です。

4~8時間では409回の取引がありながら、

総損益-302,324円、勝率21.03%、PF0.10

となりました。

これは偶然として無視するにはかなり大きな差に見えます。

一方、1~3日になると再びプラスになるため、単純に「長時間保有=悪い」というわけでもありません。

今回の観測から、

零号機では、エントリー後4時間前後に何か大きな分岐点が存在するのではないか?

という新しい疑問が生まれました。

ただし、まだChapter1「観測」です。

ここでいきなり4時間の強制決済ルールを追加するのではなく、まずはなぜ4~8時間の取引だけがこれほど悪いのかを調べていきます。

次回

ここまで零号機について、

  • 月別
  • エントリー・決済時間帯
  • エントリー・決済曜日
  • 保有時間

という角度から観測してきました。

今回、特に気になる特徴として、

「4~8時間保有された取引が極端に弱い」

ことが分かりました。

ただ、EAの弱点を見る方法はこれだけではありません。

1回1回の取引だけを見るのではなく、

負け始めたとき、零号機は何連敗するのか?

という視点もあります。

次回は、零号機の連勝・連敗を分析します。

最大で何連勝・何連敗しているのか。

連敗はどの程度の頻度で発生するのか。

そして、大きなドローダウンにつながるような「負け方」に特徴はあるのか。

1回の勝ち負けではなく、取引の「連なり」から零号機の性格を観測してみます。

ランダムウォークへの挑戦⑨ | Pythonで取引の連勝・連敗を分析する(2026/7/28)
熊本で震度7の地震がありました。今年、西南戦争を巡る旅として熊本に旅行に行ったばかりなので、他人事とは思えず、大変驚いています。復旧作業中だった熊本城の石垣も一部崩れてしまったとのことで悲しい気持ちです。熊本でお世話になった郷土料理のお店や…

コード全文

コード全文はこちら
import os
import pandas as pd
import matplotlib.pyplot as plt


# ==============================
# resultsフォルダを作成
# ==============================

os.makedirs(
    "results",
    exist_ok=True
)


# ==============================
# CSV読み込み
# ==============================

df = pd.read_csv(
    "/mnt/c/MT4/report.csv",
    encoding="utf-8-sig"
)


# ==============================
# エントリー行を抽出
# ==============================

entries = df[
    df["取引種別"].isin(
        [
            "buy",
            "sell"
        ]
    )
].copy()


entries = entries[
    [
        "注文番号",
        "時間"
    ]
].copy()


entries = entries.rename(
    columns={
        "時間": "エントリー時間"
    }
)


# ==============================
# 決済行を抽出
# ==============================

trades = df[
    df["取引種別"] == "close"
].copy()


trades = trades.rename(
    columns={
        "時間": "決済時間"
    }
)


# ==============================
# エントリーと決済を結合
# ==============================

trades = trades.merge(
    entries,
    on="注文番号",
    how="left",
    validate="one_to_one"
)


# ==============================
# 日時型へ変換
# ==============================

date_columns = [
    "エントリー時間",
    "決済時間"
]


for col in date_columns:

    trades[col] = pd.to_datetime(
        trades[col],
        format="%Y.%m.%d %H:%M",
        errors="coerce"
    )


print(
    "\n===== 日時変換エラー確認 ====="
)

print(
    trades[
        [
            "エントリー時間",
            "決済時間"
        ]
    ].isna().sum()
)


# ==============================
# 年月列を作成
# ==============================

trades["年月"] = (
    trades["決済時間"]
    .dt.strftime("%Y-%m")
)


# ==============================
# 基本集計
# ==============================

total_trades = len(trades)


wins = trades[
    trades["損益"] > 0
]


losses = trades[
    trades["損益"] < 0
]


win_count = len(wins)
loss_count = len(losses)


win_rate = (
    win_count
    / total_trades
    * 100
)


gross_profit = (
    wins["損益"].sum()
)

gross_loss = (
    losses["損益"].sum()
)


profit_factor = (
    gross_profit / abs(gross_loss)
    if gross_loss != 0
    else float("nan")
)


total_profit = (
    trades["損益"].sum()
)


average_profit = (
    wins["損益"].mean()
)


average_loss = (
    losses["損益"].mean()
)


# ==============================
# 最大ドローダウン
# ==============================

trades["過去最高残高"] = (
    trades["残高"].cummax()
)


trades["ドローダウン"] = (
    trades["残高"]
    - trades["過去最高残高"]
)


max_balance_drawdown = (
    trades["ドローダウン"].min()
)


# ==============================
# 基本分析結果を表示
# ==============================

print(
    "\n===== EA分析結果 ====="
)

print(
    f"総取引数: {total_trades}"
)

print(
    f"勝ち取引: {win_count}"
)

print(
    f"負け取引: {loss_count}"
)

print(
    f"勝率: {win_rate:.2f}%"
)

print(
    f"総損益: {total_profit:.0f}"
)

print(
    f"総利益: {gross_profit:.0f}"
)

print(
    f"総損失: {gross_loss:.0f}"
)

print(
    f"PF: {profit_factor:.2f}"
)

print(
    f"平均利益: {average_profit:.0f}"
)

print(
    f"平均損失: {average_loss:.0f}"
)

print(
    f"最大ドローダウン: "
    f"{max_balance_drawdown:.0f}"
)

print(
    f"最終残高: "
    f"{trades['残高'].iloc[-1]:.0f}"
)


# ==============================
# 月別分析
# ==============================

monthly_profit = (
    trades
    .groupby("年月")["損益"]
    .sum()
    .reset_index()
)


monthly_profit["損益"] = (
    monthly_profit["損益"]
    .round(0)
)


print(
    "\n===== 月別損益 ====="
)

print(
    monthly_profit
)


# ==============================
# 月別損益グラフ
# ==============================

plt.figure(
    figsize=(14, 6)
)

plt.bar(
    monthly_profit["年月"],
    monthly_profit["損益"]
)

plt.title(
    "Monthly Profit"
)

plt.xlabel(
    "Month"
)

plt.ylabel(
    "Profit"
)

plt.xticks(
    rotation=90
)

plt.axhline(
    0,
    linewidth=1
)

plt.tight_layout()

plt.savefig(
    "results/monthly_profit.png",
    dpi=150
)

plt.close()


# ==============================
# エントリー時間帯・決済時間帯
# ==============================

trades["エントリー時間帯"] = (
    trades["エントリー時間"]
    .dt.hour
)


trades["決済時間帯"] = (
    trades["決済時間"]
    .dt.hour
)


# ==============================
# 時間帯別分析関数
# ==============================

def analyze_by_hour(
    data,
    hour_column
):

    analysis = (
        data
        .groupby(hour_column)
        .agg(
            取引回数=(
                "損益",
                "count"
            ),
            勝ち数=(
                "損益",
                lambda x: (x > 0).sum()
            ),
            負け数=(
                "損益",
                lambda x: (x < 0).sum()
            ),
            総損益=(
                "損益",
                "sum"
            ),
            平均損益=(
                "損益",
                "mean"
            ),
            総利益=(
                "損益",
                lambda x: x[x > 0].sum()
            ),
            総損失=(
                "損益",
                lambda x: x[x < 0].sum()
            )
        )
    )


    analysis["勝率"] = (
        analysis["勝ち数"]
        / analysis["取引回数"]
        * 100
    )


    analysis["PF"] = (
        analysis["総利益"]
        / analysis["総損失"]
        .abs()
        .replace(
            0,
            float("nan")
        )
    )


    analysis = analysis[
        [
            "取引回数",
            "勝ち数",
            "負け数",
            "勝率",
            "総損益",
            "平均損益",
            "PF"
        ]
    ]


    return analysis


# ==============================
# エントリー時間帯別分析
# ==============================

entry_hour_analysis = analyze_by_hour(
    trades,
    "エントリー時間帯"
)


entry_hour_display = (
    entry_hour_analysis.copy()
)

entry_hour_display[
    [
        "総損益",
        "平均損益"
    ]
] = (
    entry_hour_display[
        [
            "総損益",
            "平均損益"
        ]
    ].round(0)
)


print(
    "\n===== エントリー時間帯別分析 ====="
)

print(
    entry_hour_display.round(
        {
            "勝率": 2,
            "PF": 2
        }
    )
)


entry_hour_display.to_csv(
    "results/entry_hour_analysis.csv",
    encoding="utf-8-sig"
)


# ==============================
# 決済時間帯別分析
# ==============================

exit_hour_analysis = analyze_by_hour(
    trades,
    "決済時間帯"
)


exit_hour_display = (
    exit_hour_analysis.copy()
)

exit_hour_display[
    [
        "総損益",
        "平均損益"
    ]
] = (
    exit_hour_display[
        [
            "総損益",
            "平均損益"
        ]
    ].round(0)
)


print(
    "\n===== 決済時間帯別分析 ====="
)

print(
    exit_hour_display.round(
        {
            "勝率": 2,
            "PF": 2
        }
    )
)


exit_hour_display.to_csv(
    "results/exit_hour_analysis.csv",
    encoding="utf-8-sig"
)


# ==============================
# エントリー時間帯別グラフ
# ==============================

plt.figure(
    figsize=(12, 6)
)

plt.bar(
    entry_hour_analysis.index,
    entry_hour_analysis["総損益"]
)

plt.title(
    "Profit by Entry Hour"
)

plt.xlabel(
    "Entry Hour"
)

plt.ylabel(
    "Profit"
)

plt.xticks(
    range(24)
)

plt.axhline(
    0,
    linewidth=1
)

plt.tight_layout()

plt.savefig(
    "results/entry_hour_profit.png",
    dpi=150
)

plt.close()


# ==============================
# 決済時間帯別グラフ
# ==============================

plt.figure(
    figsize=(12, 6)
)

plt.bar(
    exit_hour_analysis.index,
    exit_hour_analysis["総損益"]
)

plt.title(
    "Profit by Exit Hour"
)

plt.xlabel(
    "Exit Hour"
)

plt.ylabel(
    "Profit"
)

plt.xticks(
    range(24)
)

plt.axhline(
    0,
    linewidth=1
)

plt.tight_layout()

plt.savefig(
    "results/exit_hour_profit.png",
    dpi=150
)

plt.close()


# ==============================
# エントリー曜日・決済曜日
# ==============================

trades["エントリー曜日番号"] = (
    trades["エントリー時間"]
    .dt.dayofweek
)


trades["決済曜日番号"] = (
    trades["決済時間"]
    .dt.dayofweek
)


weekday_names = {
    0: "Monday",
    1: "Tuesday",
    2: "Wednesday",
    3: "Thursday",
    4: "Friday",
    5: "Saturday",
    6: "Sunday"
}


trades["エントリー曜日"] = (
    trades["エントリー曜日番号"]
    .map(weekday_names)
)


trades["決済曜日"] = (
    trades["決済曜日番号"]
    .map(weekday_names)
)


# ==============================
# 曜日別分析関数
# ==============================

def analyze_by_weekday(
    data,
    weekday_number_column,
    weekday_column
):

    analysis = (
        data
        .groupby(
            [
                weekday_number_column,
                weekday_column
            ]
        )
        .agg(
            取引回数=(
                "損益",
                "count"
            ),
            勝ち数=(
                "損益",
                lambda x: (x > 0).sum()
            ),
            負け数=(
                "損益",
                lambda x: (x < 0).sum()
            ),
            総損益=(
                "損益",
                "sum"
            ),
            平均損益=(
                "損益",
                "mean"
            ),
            総利益=(
                "損益",
                lambda x: x[x > 0].sum()
            ),
            総損失=(
                "損益",
                lambda x: x[x < 0].sum()
            )
        )
    )


    analysis["勝率"] = (
        analysis["勝ち数"]
        / analysis["取引回数"]
        * 100
    )


    analysis["PF"] = (
        analysis["総利益"]
        / analysis["総損失"]
        .abs()
        .replace(
            0,
            float("nan")
        )
    )


    analysis = (
        analysis
        .reset_index()
    )


    analysis = (
        analysis
        .sort_values(
            weekday_number_column
        )
    )


    analysis = analysis[
        [
            weekday_number_column,
            weekday_column,
            "取引回数",
            "勝ち数",
            "負け数",
            "勝率",
            "総損益",
            "平均損益",
            "PF"
        ]
    ]


    return analysis


# ==============================
# エントリー曜日別分析
# ==============================

entry_weekday_analysis = analyze_by_weekday(
    trades,
    "エントリー曜日番号",
    "エントリー曜日"
)


entry_weekday_display = (
    entry_weekday_analysis.copy()
)

entry_weekday_display[
    [
        "総損益",
        "平均損益"
    ]
] = (
    entry_weekday_display[
        [
            "総損益",
            "平均損益"
        ]
    ].round(0)
)


print(
    "\n===== エントリー曜日別分析 ====="
)

print(
    entry_weekday_display.round(
        {
            "勝率": 2,
            "PF": 2
        }
    )
)


entry_weekday_display.to_csv(
    "results/entry_weekday_analysis.csv",
    index=False,
    encoding="utf-8-sig"
)


# ==============================
# 決済曜日別分析
# ==============================

exit_weekday_analysis = analyze_by_weekday(
    trades,
    "決済曜日番号",
    "決済曜日"
)


exit_weekday_display = (
    exit_weekday_analysis.copy()
)

exit_weekday_display[
    [
        "総損益",
        "平均損益"
    ]
] = (
    exit_weekday_display[
        [
            "総損益",
            "平均損益"
        ]
    ].round(0)
)


print(
    "\n===== 決済曜日別分析 ====="
)

print(
    exit_weekday_display.round(
        {
            "勝率": 2,
            "PF": 2
        }
    )
)


exit_weekday_display.to_csv(
    "results/exit_weekday_analysis.csv",
    index=False,
    encoding="utf-8-sig"
)


# ==============================
# エントリー曜日別グラフ
# ==============================

plt.figure(
    figsize=(10, 6)
)

plt.bar(
    entry_weekday_analysis[
        "エントリー曜日"
    ],
    entry_weekday_analysis[
        "総損益"
    ]
)

plt.title(
    "Profit by Entry Weekday"
)

plt.xlabel(
    "Entry Weekday"
)

plt.ylabel(
    "Profit"
)

plt.axhline(
    0,
    linewidth=1
)

plt.tight_layout()

plt.savefig(
    "results/entry_weekday_profit.png",
    dpi=150
)

plt.close()


# ==============================
# 決済曜日別グラフ
# ==============================

plt.figure(
    figsize=(10, 6)
)

plt.bar(
    exit_weekday_analysis[
        "決済曜日"
    ],
    exit_weekday_analysis[
        "総損益"
    ]
)

plt.title(
    "Profit by Exit Weekday"
)

plt.xlabel(
    "Exit Weekday"
)

plt.ylabel(
    "Profit"
)

plt.axhline(
    0,
    linewidth=1
)

plt.tight_layout()

plt.savefig(
    "results/exit_weekday_profit.png",
    dpi=150
)

plt.close()


# ==============================
# 保有時間別分析
# ==============================

holding_trades = trades.copy()


# 時刻が欠けている行を除外
holding_trades = holding_trades.dropna(
    subset=[
        "エントリー時間",
        "決済時間"
    ]
).copy()


# 保有時間を分単位で計算
holding_trades["保有時間(分)"] = (
    holding_trades["決済時間"]
    - holding_trades["エントリー時間"]
).dt.total_seconds() / 60


# 保有時間を時間単位でも計算
holding_trades["保有時間(時間)"] = (
    holding_trades["保有時間(分)"]
    / 60
)


# マイナスの異常値を除外
holding_trades = holding_trades[
    holding_trades["保有時間(分)"] >= 0
].copy()


# ==============================
# 保有時間を区分
# ==============================

holding_bins = [
    0,
    60,
    120,
    240,
    480,
    1440,
    4320,
    float("inf")
]


holding_labels = [
    "1時間未満",
    "1~2時間",
    "2~4時間",
    "4~8時間",
    "8~24時間",
    "1~3日",
    "3日以上"
]


holding_trades["保有時間帯"] = pd.cut(
    holding_trades["保有時間(分)"],
    bins=holding_bins,
    labels=holding_labels,
    right=False,
    include_lowest=True
)


# ==============================
# 保有時間帯ごとの成績
# ==============================

holding_analysis = (
    holding_trades
    .groupby(
        "保有時間帯",
        observed=False
    )
    .agg(
        取引回数=(
            "損益",
            "count"
        ),
        勝ち数=(
            "損益",
            lambda x: (x > 0).sum()
        ),
        負け数=(
            "損益",
            lambda x: (x < 0).sum()
        ),
        総損益=(
            "損益",
            "sum"
        ),
        平均損益=(
            "損益",
            "mean"
        ),
        総利益=(
            "損益",
            lambda x: x[x > 0].sum()
        ),
        総損失=(
            "損益",
            lambda x: x[x < 0].sum()
        ),
        平均保有時間=(
            "保有時間(時間)",
            "mean"
        )
    )
)


# 勝率
holding_analysis["勝率"] = (
    holding_analysis["勝ち数"]
    / holding_analysis["取引回数"]
    * 100
)


# PF
holding_analysis["PF"] = (
    holding_analysis["総利益"]
    / holding_analysis["総損失"]
    .abs()
    .replace(
        0,
        float("nan")
    )
)


# 必要な列だけ残す
holding_analysis = holding_analysis[
    [
        "取引回数",
        "勝ち数",
        "負け数",
        "勝率",
        "総損益",
        "平均損益",
        "PF",
        "平均保有時間"
    ]
]


# ==============================
# 表示用データ
# ==============================

holding_analysis_display = (
    holding_analysis.copy()
)


holding_analysis_display[
    [
        "総損益",
        "平均損益"
    ]
] = (
    holding_analysis_display[
        [
            "総損益",
            "平均損益"
        ]
    ].round(0)
)


print(
    "\n===== 保有時間別分析 ====="
)


print(
    holding_analysis_display.round(
        {
            "勝率": 2,
            "PF": 2,
            "平均保有時間": 2
        }
    )
)


# ==============================
# 保有時間の基本情報
# ==============================

print(
    "\n===== 保有時間の基本情報 ====="
)


print(
    f"分析対象取引数: "
    f"{len(holding_trades)}"
)


print(
    f"最短保有時間: "
    f"{holding_trades['保有時間(時間)'].min():.2f}時間"
)


print(
    f"平均保有時間: "
    f"{holding_trades['保有時間(時間)'].mean():.2f}時間"
)


print(
    f"中央値: "
    f"{holding_trades['保有時間(時間)'].median():.2f}時間"
)


print(
    f"最長保有時間: "
    f"{holding_trades['保有時間(時間)'].max():.2f}時間"
)


# ==============================
# 保有時間別CSV
# ==============================

holding_analysis_display.to_csv(
    "results/holding_time_analysis.csv",
    encoding="utf-8-sig"
)


# 各取引の保有時間も保存
holding_trades.to_csv(
    "results/holding_time_trades.csv",
    index=False,
    encoding="utf-8-sig"
)


# ==============================
# 保有時間別損益グラフ
# ==============================

holding_labels_en = [
    "Under 1 hour",
    "1-2 hours",
    "2-4 hours",
    "4-8 hours",
    "8-24 hours",
    "1-3 days",
    "3 days or more"
]


plt.figure(
    figsize=(12, 6)
)


plt.bar(
    holding_labels_en,
    holding_analysis["総損益"]
)


plt.title(
    "Profit by Holding Time"
)


plt.xlabel(
    "Holding Time"
)


plt.ylabel(
    "Profit"
)


plt.xticks(
    rotation=30
)


plt.axhline(
    0,
    linewidth=1
)


plt.tight_layout()


plt.savefig(
    "results/holding_time_profit.png",
    dpi=150
)


plt.close()

関連記事

ランダムウォークへの挑戦㉓|最高PFは本当に強いのか。Relative Band Distanceを時間分割で比較する(2026/8/30)
ランダムウォークへの挑戦㉒|Q3の境界をずらしても優位性は残るのか(2026/8/30)
ランダムウォークへの挑戦㉑|Q3の優位性は時間が変わっても残るのか(2026/8/29)
ランダムウォークへの挑戦⑳|零号機の利益は「一部の大勝ち」に支えられているのか(2026/8/20)
ランダムウォークへの挑戦⑲|BAND_DISTANCEをATRで割って「相対的なブレイクの強さ」を分析する(2026/8/19)
ランダムウォークへの挑戦⑱|バンドからの距離で零号機のエントリー位置を分析する(2026/8/19)
ランダムウォークへの挑戦⑰|ATR×BB幅で零号機の「得意な相場」を16種類に分解する(2026/8/14)
ランダムウォークへの挑戦⑯|ボリンジャーバンドの幅で零号機の得意な相場を探る(2026/8/11)
ランダムウォークへの挑戦⑮|零号機は「荒れた相場」に強いのか ― ATRと取引成績の関係をPythonで分析する(2026/8/9)
ランダムウォークへの挑戦⑭ | 零号機の「記憶」をPythonへ渡す ― trade_log.csvを分析用データに整える(2026/8/5)
ランダムウォークへの挑戦⑬ | 零号機に「記憶」を与える ― mql4でエントリーから決済までの相場環境を記録する(2026/8/2)
ランダムウォークへの挑戦⑫ | Chapter2「学習」始動 ー零号機は何を学ぶべきなのかー(2026/8/2)
ランダムウォークへの挑戦⑪ | 零号機の性格診断──Chapter1「観測」 完結(2026/8/1)
ランダムウォークへの挑戦⑩ | Pythonで資産推移とドローダウンを分析する(2026/8/1)
ランダムウォークへの挑戦⑨ | Pythonで取引の連勝・連敗を分析する(2026/7/28)
ランダムウォークへの挑戦⑧ | Pythonでポジションの保有時間別に成績を分析する(2026/7/26)
ランダムウォークへの挑戦⑦ | Pythonでエントリー曜日と決済曜日から零号機を分析する(2026/7/23)
ランダムウォークへの挑戦⑥ | Pythonでエントリー時間と決済時間から損益を分析する(2026/7/22)
ランダムウォークへの挑戦⑤ | Pythonで月別損益をグラフ化する(2026/7/5)
ランダムウォークへの挑戦④ |Pythonで「零号機」の基本成績を分析する(2026/6/11)
ランダムウォークへの挑戦③ | 環境設定(2026/5/26)
ランダムウォークへの挑戦➁ | 小目標3つ設定(2026/5/25)
ランダムウォークへの挑戦① | はじめに(2026/5/25)

コメント

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