バックテスト、AIが作成したロング編

investment

バックテストのコードをAIに書いてもらった。

🔹戦略概要(VwapMacdTrend / ロング専用)

VWAPとMACDヒストグラムを利用した短期ロング戦略
日中の上昇トレンド局面で、VWAPより上で押し目を拾い、短時間の上昇波を狙う。


🔸取引ルールまとめ

1. 時間フィルタ

  • 取引許可時間:9:05~14:30
  • 14:55 に強制クローズ(引け前にすべて決済)

2. エントリー条件(ロングのみ)

以下のすべてを満たすときに買いエントリー:

  • 現在価格が VWAP付近または上price >= vwap*0.999
  • VWAPの傾きが ほぼ水平以上(vwap_slope > -1)
  • MACDヒストグラムが 上昇傾向hist >= hist_prev * 0.95
  • VWAPからの乖離率が ±0.3%以内
  • ATR(ボラティリティ)が 価格の0.2%以上
  • 出来高が直近20本の中央値の 40%以上

さらに「押し目型エントリー」も許可:

  • VWAPタッチ(LowがVWAP±0.2%以内)
  • price > vwap かつ hist > hist_prev

いずれかを満たした場合にロング(buy)発注。


3. 損切り・利確設定

  • 損切り(SL)
    過去5本の最安値 or 「ATR×倍率(0.7)」のどちらか価格より下側
  • 利確(TP)
    エントリー価格から上方向に 1.3R(Rはリスク幅)
    → リスクリワード比 ≈ 1:1.3
  • ロット(size)
    資金の 0.5% を1トレードのリスク上限に設定
    size = equity * risk_per_trade / risk(最低1株)

4. ポジション管理ルール

  • ポジション保有中に以下が起きたら即クローズ:
    • 経過時間が 45分以上 かつ +0.5R未満の利益
    • MACDヒストグラムが 悪化hist < hist_prev
    • 価格が VWAPを下回る
  • 同時ポジションは常に1つのみ(Backtest が自動制御)

5. 実行環境の設定

  • 約定方式:同バー終値(デフォルト設定)
  • 手数料:0(仮設定)
  • データ:yfinance から 1分足・8日分 を取得
  • 日本時間を基準(Asia/Tokyo)

6. デバッグ・統計

  • 条件ブロックの通過回数を block 辞書でカウント:
    • time, dist, vol, atr, cond, ok, pos
  • 結果をCSV出力(_trades.csv
  • グラフ表示(bt.plot(open_browser=True)

7. 戦略の特徴

  • VWAP上での押し目買い+MACDの勢い確認を基本にしており、
    “短期の上昇波を確実に取る”スタイル。
  • 引け前に必ずポジションを解消するため、日中完結・持ち越しなし。
  • 1分足でボラティリティが小さいときにはトレードがほとんど発生しないが、
    上昇トレンドの日には複数回のエントリーが発生。

要するにこのロング戦略は:

「VWAPより上でMACDが改善している場面で押し目を拾い、
日中の短期トレンドを確実に取る」

というコンセプト

コード

import yfinance as yf
import pandas as pd
import pandas_ta as ta
from backtesting import Backtest, Strategy
from datetime import time
import pytz

ALLOW_LONG  = False
ALLOW_SHORT = True

brand_names = {
    "1570.T": "Nikkei NF",
    "6526.T": "Sociotech",
    "7203.T": "Toyota",
    "6857.T": "Advantest",
    "6146.T": "Disco",
    "4062.T": "Ibiden",
    "6920.T": "LaserTech",
    "5803.T": "Fujikura",
    "8035.T": "Tokyo Electron",
    "6758.T": "SONY group",
    "9984.T": "Softbank group",
    "AAPL":   "Apple Inc.",
    }

# ===== 1) データ取得(列を最初からフラットに) =====
TICKER   = '7203.T'
INTERVAL = "1m"
PERIOD   = "8d"

df = yf.download(
    TICKER, interval=INTERVAL, period=PERIOD,
    auto_adjust=False, group_by="column"  # ← これで ['Open','High',...] の一次元列
).dropna()

# --- ダウンロード直後に追加(列の正規化) ---
import pandas as pd

# 1) MultiIndex → 1段化(安全)
if isinstance(df.columns, pd.MultiIndex):
    df.columns = ['_'.join([str(x) for x in c if x]).strip() for c in df.columns]

# 2) 1570.T の接尾辞付き列を素名にコピー
def pick_base(name: str):
    # 完全一致 or "name_..." を優先的に拾う
    cands = [c for c in df.columns if c == name] \
          or [c for c in df.columns if c.startswith(name + '_')]
    return cands[0] if cands else None

base_map = {}
for name in ['Open', 'High', 'Low', 'Close', 'Volume']:
    src = pick_base(name)
    if src is None:
        raise KeyError(f"{name} 列が見つかりません。現在の列: {list(df.columns)}")
    df[name] = df[src]  # 素名で上書き作成

# 3) 余分なNaN除去(お好みで)
df.dropna(subset=['Open','High','Low','Close','Volume'], inplace=True)

# --- VWAP(安定版) ---
def intraday_vwap(df):
    tp = (df['High'] + df['Low'] + df['Close']) / 3
    idx = df.index
    if getattr(idx, "tz", None) is None:
        idx = idx.tz_localize("Asia/Tokyo")
    day_key = pd.Series(idx.tz_convert("Asia/Tokyo").date, index=df.index)
    cum_vol = df['Volume'].groupby(day_key).cumsum()
    cum_vp  = (tp * df['Volume']).groupby(day_key).cumsum()
    return (cum_vp / cum_vol.where(cum_vol != 0, pd.NA)).astype('float64')

df['VWAP'] = intraday_vwap(df)

# --- MACD(確実に作る版) ---
import pandas_ta as ta
macd = ta.macd(df['Close'], fast=8, slow=21, signal=9, append=True)
if isinstance(macd, pd.DataFrame):
    # 列名を統一
    if isinstance(macd.columns, pd.MultiIndex):
        macd.columns = ['_'.join([str(x) for x in c if x]).strip() for c in macd.columns]
    macd.rename(columns={
        'MACD_8_21_9':  'MACD',
        'MACDs_8_21_9': 'SIGNAL',
        'MACDh_8_21_9': 'HIST'
    }, inplace=True)
    df = df.join(macd)
else:
    # もし None を返す版なら append=True で追加
    df.ta.macd(close='Close', fast=12, slow=26, signal=9, append=True)
    # 追加された列を拾ってリネーム
    rename_map = {}
    for c in list(df.columns):
        s = str(c)
        if 'MACD_12_26_9' in s and 'MACDs' not in s and 'MACDh' not in s:
            rename_map[c] = 'MACD'
        if 'MACDs_12_26_9' in s:
            rename_map[c] = 'SIGNAL'
        if 'MACDh_12_26_9' in s:
            rename_map[c] = 'HIST'
    if rename_map:
        df.rename(columns=rename_map, inplace=True)

# 最終チェック
for col in ['MACD','SIGNAL','HIST']:
    if col not in df.columns:
        print("現在の列:", list(df.columns))
        raise KeyError(f"MACD列 {col} が見つかりません")

# --- Backtestに渡す列だけに絞る ---
need = ['Open','High','Low','Close','Volume','VWAP','HIST']
df = df[need].dropna()

JST = pytz.timezone("Asia/Tokyo")
FLAT_TIME = time(14, 55)  # 14:55にフラットにする例


# ===== 5) 戦略 ===== TOYOTA向け
from datetime import time, timedelta
import pytz
JST = pytz.timezone("Asia/Tokyo")

# === パラメータ(調整しやすい所) ===
OPEN_OK_FROM   = time(9, 5)
NO_NEW_AFTER   = time(14, 30)
FLAT_TIME      = time(14, 55)

VWAP_DIST_MAX  = 0.003    # 0.2%
VOL_MED_WIN    = 20
VOL_FLOOR_MULT = 0.4
ATR_PCT_MIN    = 0.0020   # 0.35%
TIME_STOP_MIN  = 45       # 分
R_MULT_TP      = 1.3      # 全利確のR倍率(1.5〜2.0で試す)

class VwapMacdTrend(Strategy):
    sl_ticks = 10
    atr_mult = 0.7
    risk_per_trade = 0.005  # コスト抑制で一旦0.5%
    block = None

    def init(self):
        # 実行のたびにリセット
        type(self).block = dict(time=0, dist=0, vol=0, atr=0, cond=0, ok=0, pos=0)

        self.atr = self.I(ta.atr, pd.Series(self.data.High),
                                   pd.Series(self.data.Low),
                                   pd.Series(self.data.Close), 14)
        # 出来高中央値
        self.vol_med = self.I(lambda v: pd.Series(v).rolling(VOL_MED_WIN).median().bfill(),
                              self.data.Volume)

    def _jst_time(self):
        return self.data.index[-1].to_pydatetime().astimezone(JST).time()

    def next(self):
        # --- 引け前はフラット ---
        if self._jst_time() >= FLAT_TIME and self.position:
            self.position.close(); return

        price     = self.data.Close[-1]
        vwap      = self.data.df['VWAP'].iloc[-1]
        vwap_prev = self.data.df['VWAP'].iloc[-2]
        hist      = self.data.df['HIST'].iloc[-1]
        hist_prev = self.data.df['HIST'].iloc[-2]
        vwap_slope = vwap - vwap_prev

        # --- 既存ポジ管理(タイムストップ & 失速/割れ) ---
        if self.position:
            trade = self.trades[-1]
            dur = self.data.index[-1] - self.data.index[trade.entry_bar]
            # エントリー時の想定リスク(R)を復元
            r = max(trade.entry_price - (trade.sl or trade.entry_price - self.sl_ticks), 1e-6)
            # タイムストップ:45分で +0.5R 未満なら撤退
            if dur >= timedelta(minutes=TIME_STOP_MIN):
                upnl_r = (price - trade.entry_price) / r
                if upnl_r < 0.5:
                    self.position.close(); return
            # 失速/割れ
            if (hist < hist_prev) or (price < vwap):
                self.position.close()
            return

        # --- 損切りとサイズ ---
        recent_low = min(self.data.Low[-5:])
        atr = max(float(self.atr[-1]), 1e-6)
        sl_price = min(recent_low, price - max(self.sl_ticks, atr * self.atr_mult))
        risk = max(price - sl_price, 1e-6)
        size = max(int((self.equity * self.risk_per_trade) / risk), 1)

        # 既存ポジ判定
        if self.position:
            # 既存ポジの管理ロジック…
            return

        # --- 時間帯フィルタ ---
        t = self._jst_time()
        if not (OPEN_OK_FROM <= t < NO_NEW_AFTER):
            type(self).block['time'] += 1
            return

        # --- 乖離 ---
        if abs(price - vwap)/max(vwap,1) > VWAP_DIST_MAX:
            type(self).block['dist'] += 1
            return

        # --- ボラ ---
        if (self.atr[-1] / max(price,1)) < ATR_PCT_MIN:
            type(self).block['atr'] += 1
            return

        # --- 出来高 ---
        if self.data.Volume[-1] < VOL_FLOOR_MULT * max(self.vol_med[-1], 1):
            type(self).block['vol'] += 1
            return

        # --- エントリー条件(“二段目”) ---
        #hist_up = (hist > 0) and (hist >= hist_prev * 0.97)  # 例
        hist_up = (hist >= hist_prev * 0.95)
        long_ok = (price >= vwap*0.999) and (vwap_slope > -1) and hist_up

        # ここに押し目型を入れているなら pullback_long も算出
        VWAP_TOUCH = 0.002
        touch = (abs(self.data.Low[-1]-vwap)/vwap <= VWAP_TOUCH)
        pullback_long = (price > vwap) and touch and (hist > hist_prev)

        entry_ok = long_ok or pullback_long
        if not entry_ok:
            type(self).block['cond'] += 1
            return

        # 発注直前
        # --- 全利確は 1.8R で(R_MULT_TP) ---
        tp = price + R_MULT_TP * risk
        type(self).block['ok'] += 1   # 条件を通過した回数

        # 発注
        self.buy(size=size, sl=sl_price, tp=tp)
        type(self).block['pos'] += 1

# ===== 6) 実行 =====
bt = Backtest(
    df, VwapMacdTrend,
    cash=1_000_000,
    commission=0.0  # 手数料0.03%+滑り0.05% をまとめて近似
)
stats = bt.run()
print(stats)
print("BLOCK:", VwapMacdTrend.block)

# 取引明細を確認
print(stats['_trades'].head())
# CSVに保存
stats['_trades'].to_csv('trades.csv', index=False)

#print(bt.run()._strategy.block)

bt.plot(open_browser=True)

これの結果は次の通り。寄り付きで大きくギャップダウン直後のリバウンドをうまく取れたけど、ちょっと条件が厳しすぎるのかトレードが2回しかない。資金は100万円を想定して、1回目は約300株、2回目は約200株のロットで、合計7000円のプラス。
ロットが1株からになっているので修正が必要。

Start                     2025-10-07 00:33...
End 2025-10-17 06:24...
Duration 10 days 05:51:00
Exposure Time [%] 0.47207
Equity Final [$] 1007151.3
Equity Peak [$] 1007254.3
Return [%] 0.71513
Buy & Hold Return [%] -2.72908
Return (Ann.) [%] 25.1652
Volatility (Ann.) [%] 4.80729
CAGR [%] 19.16019
Sharpe Ratio 5.2348
Sortino Ratio inf
Calmar Ratio 260.64738
Alpha [%] 0.76206
Beta 0.0172
Max. Drawdown [%] -0.09655
Avg. Drawdown [%] -0.04942
Max. Drawdown Duration 2 days 06:17:00
Avg. Drawdown Duration 0 days 18:08:00
# Trades 2
Win Rate [%] 100.0
Best Trade [%] 0.8212
Worst Trade [%] 0.2236
Avg. Trade [%] 0.52195
Max. Trade Duration 0 days 00:07:00
Avg. Trade Duration 0 days 00:05:00

コメント

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