曜日別の損益を分析する | ランダムウォークへの挑戦⑧2026/7/26

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

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

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

前回は曜日別の損益を分析するところまで実施しました。

今回は保有時間別分析のロジックを組み込みます。

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

import os

# resultsフォルダが存在しない場合は作成
os.makedirs("results", exist_ok=True)

# 元データdfの時間列を日時型に変換
# tradesではなくdfを変換するのがポイント
df["時間"] = pd.to_datetime(
    df["時間"],
    format="%Y.%m.%d %H:%M",
    errors="coerce"
)

# 日時変換に失敗した行を確認
invalid_time_count = df["時間"].isna().sum()

if invalid_time_count > 0:
    print(
        f"日時に変換できなかった行が "
        f"{invalid_time_count}件あります。"
    )

# エントリー行を抽出
# このCSVではbuyまたはsellがエントリー行
entry_trades = (
    df[df["取引種別"].isin(["buy", "sell"])]
    [
        [
            "注文番号",
            "時間",
            "取引種別"
        ]
    ]
    .copy()
)

# 列名を変更
entry_trades = entry_trades.rename(
    columns={
        "時間": "エントリー時間",
        "取引種別": "売買方向"
    }
)

# 決済行を抽出
close_trades = (
    df[df["取引種別"] == "close"]
    [
        [
            "注文番号",
            "時間",
            "損益"
        ]
    ]
    .copy()
)

# 列名を変更
close_trades = close_trades.rename(
    columns={
        "時間": "決済時間"
    }
)

# 注文番号を使って
# エントリー行と決済行を結合
holding_trades = pd.merge(
    entry_trades,
    close_trades,
    on="注文番号",
    how="inner"
)

# 念のため、結合後の時刻も日時型に変換
holding_trades["エントリー時間"] = pd.to_datetime(
    holding_trades["エントリー時間"],
    errors="coerce"
)

holding_trades["決済時間"] = pd.to_datetime(
    holding_trades["決済時間"],
    errors="coerce"
)

# エントリー時間または決済時間が欠けている行を除外
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を計算
# 総損失が0の場合は0除算を避ける
holding_analysis["PF"] = (
    holding_analysis["総利益"]
    / holding_analysis["総損失"]
    .abs()
    .replace(0, float("nan"))
)

# 表示する列を整理
holding_analysis = holding_analysis[
    [
        "取引回数",
        "勝ち数",
        "負け数",
        "勝率",
        "総損益",
        "平均損益",
        "PF",
        "平均保有時間"
    ]
]

# ==============================
# 結果を表示
# ==============================

print("\n===== 保有時間別分析 =====")
print(holding_analysis.round(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.to_csv(
    "results/holding_time_analysis.csv",
    encoding="utf-8-sig"
)

print(
    "\n保有時間別分析結果を "
    "results/holding_time_analysis.csv "
    "として保存しました。"
)

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

print(
    "各取引の保有時間を "
    "results/holding_time_trades.csv "
    "として保存しました。"
)

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

# グラフ用の英語ラベル
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()

print(
    "保有時間別損益グラフを "
    "results/holding_time_profit.png "
    "として保存しました。"
)

ポイント

Python標準のosという機能を読み込んでいます。

  • 保存先のresultsフォルダを作るためにPython標準のosをインストール
  • pd.merge()は、2つの表を共通する値で結合する機能
  • .isna()は値が欠損しているかどうかを調べるメソッド

出力結果は以下の通りでした。

分析

出力結果から、零号機は「利益は2〜4時間以内に決着がつく」タイプであり、4時間を超えて保有した取引は成績が大きく悪化することが分かりました。

これは今後、「一定時間経過後に強制決済する」という改善案を試してもいいと思います。

今日はここまで。

次回は、連勝・連敗分析を行いたいと思います。

関連記事

コメント

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