最近は暑すぎてプールが気持ちいいです。
今日は夏休みをとって、昼間から泳ぎに行きました。
それでは今日も作業を進めていきます。
はじめに
前回は、零号機の月別損益をグラフ化しました。

その結果、長期的には利益を残しているものの、毎月均等に利益を積み上げているわけではなく、月によって成績にかなりムラがあることが分かりました。
では、そのムラはどこから生まれているのでしょうか。
今回は「時間帯」に注目してみます。
当初、私は決済済み取引から時間を取り出し、「時間帯別分析」を行っていました。
しかし、コードを見直してみると、
trades = df[
df["取引種別"] == "close"
].copy()
ここで抽出しているのはclose、つまり決済データです。
したがって、ここから取り出した時間は「エントリーした時間」ではなく、決済した時間でした。
もちろん、決済時間を分析することにも意味はあります。
「どの時間帯に利益や損失が確定しているのか」を見ることで、零号機の出口側の特徴を観測できるからです。
しかし、
「どの時間帯にエントリーした取引の成績が良いのか」
を調べるには、エントリー時間を基準にした分析も必要です。
そこで今回は、一つの取引について、
エントリー
↓
ポジション保有
↓
決済
という流れを作り、
「エントリー時間帯」と「決済時間帯」の両方から零号機を分析してみます。
今回やること
今回使用するreport.csvには、エントリーと決済が別々の行として記録されています。
例えば、
注文番号1 sell 2015.01.06 14:30
注文番号1 close 2015.01.06 17:00
という形です。
同じ注文番号が付いているので、
注文番号1
14:30
エントリー
↓
17:00
決済
という一つの取引として対応させることができます。
今回はこの「注文番号」を使って、
【入口】
エントリー時間帯別分析
【出口】
決済時間帯別分析
の2つを行います。
1.エントリー行を取り出す
まず、report.csvからエントリーした行だけを取り出します。
entries = df[
df["取引種別"].isin(
[
"buy",
"sell"
]
)
].copy()
エントリーには、
buy
sell
の2種類があります。
そこで今回は、
.isin(
[
"buy",
"sell"
]
)
を使っています。
.isin()は、指定した複数の値のいずれかに該当するかを調べる処理です。
つまり、
buy → 対象
sell → 対象
close → 対象外
として、エントリー行だけを抽出しています。
2.必要な列だけを残す
エントリー側から今回必要なのは、
注文番号
時間
です。
そこで、
entries = entries[
[
"注文番号",
"時間"
]
].copy()
として、必要な2列だけを残します。
さらに、
entries = entries.rename(
columns={
"時間": "エントリー時間"
}
)
とします。
元のCSVでは単に「時間」という名前なので、このままでは後で決済時間と区別できません。
そこで、
時間
↓
エントリー時間
と列名を変更しています。
.rename()は、列名などの名前を変更するための処理です。
3.決済行を取り出す
次に、決済された行だけを取り出します。
trades = df[
df["取引種別"] == "close"
].copy()
これはこれまでの分析でも使ってきた処理です。
そして、
trades = trades.rename(
columns={
"時間": "決済時間"
}
)
として、
時間
↓
決済時間
と名前を変更します。
これで、
entries → エントリー側
trades → 決済側
と役割が明確になりました。
4.注文番号を使って結合する
ここが今回の重要な処理です。
エントリー側と決済側には、同じ取引であれば同じ「注文番号」が記録されています。
そこで、
trades = trades.merge(
entries,
on="注文番号",
how="left",
validate="one_to_one"
)
とします。
.merge()は、共通する値を基準に2つのDataFrameを結合する処理です。
今回は、
on="注文番号"
なので、「注文番号」が同じ行同士を結び付けます。
例えば、
【entries】
注文番号 エントリー時間
1 14:30
2 18:30
と、
【trades】
注文番号 決済時間 損益
1 17:00 +884
2 20:00 +281
を結合すると、
注文番号 エントリー時間 決済時間 損益
1 14:30 17:00 +884
2 18:30 20:00 +281
となります。
これで、一つの取引について入口と出口を同時に持つデータができました。
また、
validate="one_to_one"
によって、
1つの注文番号に対して、エントリーと決済が1件ずつ存在する
こともチェックしています。
今回のreport.csvでは、エントリー1,720件、決済1,720件がすべて注文番号で1対1に対応していました。
5.エントリー時間と決済時間を日時型にする
続いて、時間をPythonで日時として扱えるようにします。
date_columns = [
"エントリー時間",
"決済時間"
]
for col in date_columns:
trades[col] = pd.to_datetime(
trades[col],
format="%Y.%m.%d %H:%M",
errors="coerce"
)
今回は、
date_columns
というリストに、
エントリー時間
決済時間
を入れています。
そして、
for col in date_columns:
によって、2つの列に同じ処理を繰り返しています。
つまり、
1回目
col = "エントリー時間"
2回目
col = "決済時間"
となります。
そのため、同じpd.to_datetime()を2回書かずに済みます。
6.エントリー時間帯と決済時間帯を作る
日時型に変換したので、「時」だけを取り出します。
trades["エントリー時間帯"] = (
trades["エントリー時間"]
.dt.hour
)
trades["決済時間帯"] = (
trades["決済時間"]
.dt.hour
)
例えば、
2015.01.06 14:30
なら、
14
を取り出します。
これで、一つの取引について、
注文番号
エントリー時間
決済時間
エントリー時間帯
決済時間帯
損益
という情報を持てるようになりました。
7.時間帯別分析を関数にする
ここから、
エントリー時間帯別分析
決済時間帯別分析
の2つを行います。
集計方法はどちらも同じなので、同じコードを2回書くのではなく、今回は関数にしてみます。
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()
)
)
)
defは、複数の処理をまとめて関数を作るための構文です。
今回は、
analyze_by_hour
という名前の関数を作っています。
この関数に、
分析するDataFrame
分析する時間帯の列
を渡せば、時間帯別の成績を集計してくれるようにします。
8.勝率とPFを計算する
続いて、関数の中で勝率を計算します。
analysis["勝率"] = (
analysis["勝ち数"]
/ analysis["取引回数"]
* 100
)
これは、
勝ち数 ÷ 取引回数 × 100
です。
PFも計算します。
analysis["PF"] = (
analysis["総利益"]
/ analysis["総損失"]
.abs()
.replace(
0,
float("nan")
)
)
PFは、
総利益 ÷ 総損失の絶対値
です。
さらに、
.replace(
0,
float("nan")
)
によって、総損失が0だった場合の0除算を防いでいます。
9.必要な列だけを残して結果を返す
最後に、
analysis = analysis[
[
"取引回数",
"勝ち数",
"負け数",
"勝率",
"総損益",
"平均損益",
"PF"
]
]
として、最終的に必要な列だけを残します。
PFを計算するために使った、
総利益
総損失
はここで外しています。
そして、
return analysis
とします。
returnは、関数の中で作った結果を関数の外へ返すための処理です。
これでanalyze_by_hour()という時間帯別分析の関数が完成しました。
10.エントリー時間帯別に分析する
まずは入口側です。
entry_hour_analysis = analyze_by_hour(
trades,
"エントリー時間帯"
)
先ほど作ったanalyze_by_hour()に、
trades
エントリー時間帯
を渡しています。
これによって、
「何時にエントリーした取引が、最終的にどれだけの利益・損失を出したのか」
を集計できます。
11.決済時間帯別に分析する
出口側も同じです。
exit_hour_analysis = analyze_by_hour(
trades,
"決済時間帯"
)
今度は、
「何時に決済された取引が、どれだけの利益・損失を出したのか」
を集計します。
同じ関数を使っていますが、
エントリー時間帯
決済時間帯
のどちらを渡すかによって、分析する対象を変えています。
12.CSVとして保存する
分析結果は、それぞれCSVとして保存します。
entry_hour_analysis.to_csv(
"results/entry_hour_analysis.csv",
encoding="utf-8-sig"
)
exit_hour_analysis.to_csv(
"results/exit_hour_analysis.csv",
encoding="utf-8-sig"
)
これで、
results
├─ entry_hour_analysis.csv
└─ exit_hour_analysis.csv
が作成されます。
13.グラフを作る
まず、エントリー時間帯別です。
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()
これで、
entry_hour_profit.png
→ エントリー時間帯別
exit_hour_profit.png
→ 決済時間帯別
という2つのグラフが作成されます。
グラフの出力結果


分析
エントリー時間帯と決済時間帯を分けてグラフ化すると、同じ「時間帯分析」でも、入口と出口ではかなり違う姿が見えてきました。
まず、エントリー時間帯を見ると、最も大きな利益を出しているのは10時台です。
10時台エントリー
取引回数:210回
勝率:64.29%
総損益:+81,605円
平均損益:+389円
PF:2.62
11時台も、
11時台エントリー
取引回数:174回
勝率:63.79%
総損益:+42,722円
PF:1.70
と大きなプラスになっています。
また、22時台は取引回数が22回と少ないものの、
22時台エントリー
総損益:+25,410円
PF:6.07
という結果でした。
一方、明確にマイナスとなっている時間帯もあります。
4時台 :-8,419円
9時台 :-21,068円
15時台 :-26,095円
17時台 :-10,336円
21時台 :-23,920円
特に15時台は、88回の取引で勝率59.09%あるにもかかわらず、総損益は**-26,094.56円**、PFは0.62でした。
ここでも、勝率だけでは成績の良し悪しを判断できないことが分かります。
決済時間帯では、朝から夕方に利益が集中している
次に決済時間帯を見てみます。
こちらでは、5時台から19時台にかけてプラスとなる時間帯がかなり多いことが分かります。
特に、
5時台 :+44,133円
9時台 :+35,959円
10時台 :+52,599円
11時台 :+30,298円
12時台 :+34,293円
17時台 :+29,025円
19時台 :+21,971円
と、多くの時間帯で利益を積み上げています。
なかでも10時台は、
取引回数:126回
勝率:76.19%
総損益:+52,599円
PF:3.79
となっており、エントリー時間帯だけでなく、決済時間帯でも好成績でした。
23時台は「入る時間」ではなく「終わる時間」として悪い
今回、最も興味深かったのが23時台です。
エントリー時間帯では、
23時台エントリー
取引回数:14回
勝率:50.00%
総損益:+7,344円
PF:2.44
と、むしろプラスです。
ところが決済時間帯では、
23時台決済
取引回数:54回
勝率:35.19%
総損益:-59,526円
平均損益:-1,102.33円
PF:0.24
となっています。
つまり、
23時台にエントリーすること自体が悪いわけではなく、23時台に決済された取引の成績が非常に悪い
ということです。
以前、決済時間だけを見ていたときは「23時台は悪い時間帯」と考えていました。
しかし、今回エントリー時間を追加したことで、その解釈が変わりました。
23時台に損失が集中している理由は、
- もっと早い時間にエントリーしたポジションが23時まで残っている
- 長時間保有した負けポジションが23時台に決済されている
- 零号機の決済ロジックがこの時間帯で不利に働いている
といった可能性があります。
この段階では、まだ原因は特定できません。
9時台も入口と出口で正反対
9時台もかなり面白い結果です。
9時台エントリー
取引回数:140回
勝率:52.86%
総損益:-21,068円
PF:0.68
一方、
9時台決済
取引回数:85回
勝率:70.59%
総損益:+35,959円
PF:3.60
となっています。
つまり、
9時台に入った取引は苦戦しているのに、9時台に終わった取引はかなり好成績
です。
これも、「9時台は良い時間帯」「9時台は悪い時間帯」と一括りにはできないことを示しています。
10時台は入口と出口の両方で強い
その一方で、10時台は、
エントリー:+81,605円
決済 :+52,599円
と、入口と出口の両方で大きな利益を出しています。
これは今回の分析の中でも特徴的です。
ただし、
10時台にエントリーすれば成績が良くなる
と、この段階で断定するのはまだ早いと思います。
時間帯そのものが原因なのか、その時間帯に発生しやすい相場環境が関係しているのか、それとも零号機のロジックによって結果的にこの時間帯へ偏っているだけなのかは、まだ分からないからです。
現時点では、
「10時台は、エントリー・決済の両方で好成績だった」
という観測結果として残しておきます。
今回分かったこと
今回、エントリー時間と決済時間を分けて分析したことで、単純な「時間帯別分析」では見えなかった違いが見えてきました。
特に、
23時台
エントリー:+7,344円
決済 :-59,526円
9時台
エントリー:-21,068円
決済 :+35,959円
のように、同じ時間帯でも入口と出口で全く逆の結果になるケースがありました。
一方で、
10時台
エントリー:+81,605円
決済 :+52,599円
のように、両方で好成績となる時間帯もあります。
今回の分析から分かったのは、
「○時台は強い・弱い」と時間だけで評価するのではなく、いつ入ったのか、いつ出たのかを分けて考える必要がある
ということです。
そして、入口と出口の違いを見ると、自然に次の疑問が出てきます。
その間、ポジションをどれくらい保有していたのか。
23時台に決済された大きな損失は、長時間保有したポジションに集中しているのでしょうか。
まだ改善策を決める段階ではありません。
まずは零号機の性格を、一つずつ観測していきます。
今回の完成コード
第5回までのコードに今回の時間帯別分析を追加した完成形です。
コード全文はこちら
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"
)
# ==============================
# エントリー行を抽出
# ==============================
# buy・sellがエントリー行
entries = df[
df["取引種別"].isin(
[
"buy",
"sell"
]
)
].copy()
# エントリー分析に必要な列だけ残す
entries = entries[
[
"注文番号",
"時間"
]
].copy()
# 「時間」を「エントリー時間」に変更
entries = entries.rename(
columns={
"時間": "エントリー時間"
}
)
# ==============================
# 決済行を抽出
# ==============================
# closeが決済行
trades = df[
df["取引種別"] == "close"
].copy()
# 「時間」を「決済時間」に変更
trades = trades.rename(
columns={
"時間": "決済時間"
}
)
# ==============================
# エントリーと決済を結合
# ==============================
# 注文番号を使って1つの取引として結合
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:.2f}"
)
print(
f"総利益: {gross_profit:.2f}"
)
print(
f"総損失: {gross_loss:.2f}"
)
print(
f"PF: {profit_factor:.2f}"
)
print(
f"平均利益: {average_profit:.2f}"
)
print(
f"平均損失: {average_loss:.2f}"
)
print(
f"最大ドローダウン: "
f"{max_balance_drawdown:.2f}"
)
print(
f"最終残高: "
f"{trades['残高'].iloc[-1]:.2f}"
)
# ==============================
# 月別分析
# ==============================
monthly_profit = (
trades
.groupby("年月")["損益"]
.sum()
.reset_index()
)
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()
print(
"月別損益グラフを "
"results/monthly_profit.png "
"として保存しました。"
)
# ==============================
# エントリー時間帯・決済時間帯を作成
# ==============================
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
)
# PF
analysis["PF"] = (
analysis["総利益"]
/ analysis["総損失"]
.abs()
.replace(
0,
float("nan")
)
)
# 必要な列だけ残し、
# 表示順を整理
analysis = analysis[
[
"取引回数",
"勝ち数",
"負け数",
"勝率",
"総損益",
"平均損益",
"PF"
]
]
# 集計結果を返す
return analysis
# ==============================
# エントリー時間帯別分析
# ==============================
entry_hour_analysis = analyze_by_hour(
trades,
"エントリー時間帯"
)
print(
"\n===== エントリー時間帯別分析 ====="
)
print(
entry_hour_analysis.round(2)
)
# ==============================
# 決済時間帯別分析
# ==============================
exit_hour_analysis = analyze_by_hour(
trades,
"決済時間帯"
)
print(
"\n===== 決済時間帯別分析 ====="
)
print(
exit_hour_analysis.round(2)
)
# ==============================
# エントリー時間帯別分析をCSV保存
# ==============================
entry_hour_analysis.to_csv(
"results/entry_hour_analysis.csv",
encoding="utf-8-sig"
)
print(
"エントリー時間帯別分析結果を "
"results/entry_hour_analysis.csv "
"として保存しました。"
)
# ==============================
# 決済時間帯別分析をCSV保存
# ==============================
exit_hour_analysis.to_csv(
"results/exit_hour_analysis.csv",
encoding="utf-8-sig"
)
print(
"決済時間帯別分析結果を "
"results/exit_hour_analysis.csv "
"として保存しました。"
)
# ==============================
# エントリー時間帯別損益グラフ
# ==============================
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()
print(
"エントリー時間帯別損益グラフを "
"results/entry_hour_profit.png "
"として保存しました。"
)
# ==============================
# 決済時間帯別損益グラフ
# ==============================
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()
print(
"決済時間帯別損益グラフを "
"results/exit_hour_profit.png "
"として保存しました。"
)
次回
次回は、時間帯から視点を変えて、曜日別の成績を分析します。
今回と同じように、
エントリーした曜日と決済した曜日
を分けて比較します。
火曜日にエントリーした取引が強いのか。
それとも、火曜日に決済された取引が強いだけなのか。
時間帯分析と同じように、入口と出口で結果が変わるのかを確認していきます。
そして曜日分析のあとには、今回見えてきた「入口と出口の間」に注目し、保有時間の分析へ進む予定です。

おまけ
この本を読みました。
「私たちが小麦を栽培化したのではなく、小麦が私たちを家畜化した。」
この一文が衝撃的でした。
人間は最も成功した種で地球の主人公だと、無意識に思っていましたが、実は我々は小麦にコントロールされていたという衝撃。
また、「種」としての成功は必ずしも「個」の幸福にはつながらないという主張も納得しました。
家畜だけでなく、現代日本人ももれなく当てはまります。
「幸せとは、内なる感情の追及をやめること」
ブッダの教えがここまで科学的とは思いませんでした。
コメント