【Python】移動平均線クロスボットを作る|pandasでテクニカル分析
前回はPythonでFX APIを叩く基本を学びました。
今回は移動平均線クロス戦略をPythonで実装します。MQL4で作ったEAと同じロジックを、Pythonで再現してみましょう。
今回作るもの
- ローソク足データを取得
- pandasで移動平均を計算
- ゴールデンクロス/デッドクロスを検出
- 自動で売買注文を出す
追加ライブラリのインストール
pip install pandas numpyローソク足データの取得
GMOコインのPublic APIでローソク足を取得します。
import requestsimport pandas as pdfrom datetime import datetime, timedelta
def get_klines(symbol="USD_JPY", interval="1hour", count=100): """ ローソク足データを取得
Args: symbol: 通貨ペア interval: 時間足("1min", "5min", "15min", "30min", "1hour", "4hour", "1day") count: 取得本数 """ url = "https://forex-api.coin.z.com/public/v1/klines" params = { "symbol": symbol, "interval": interval, "date": datetime.now().strftime("%Y%m%d") }
response = requests.get(url, params=params) data = response.json()
if data["status"] == 0: klines = data["data"]
# DataFrameに変換 df = pd.DataFrame(klines) df["openTime"] = pd.to_datetime(df["openTime"]) df["open"] = df["open"].astype(float) df["high"] = df["high"].astype(float) df["low"] = df["low"].astype(float) df["close"] = df["close"].astype(float)
# 最新のcount本だけ取得 df = df.tail(count).reset_index(drop=True)
return df else: print(f"エラー: {data}") return None
# テストdf = get_klines()print(df.tail())出力例:
openTime open high low close95 2025-12-13 06:00:00+00:00 149.450 149.520 149.400 149.51096 2025-12-13 07:00:00+00:00 149.510 149.550 149.480 149.52097 2025-12-13 08:00:00+00:00 149.520 149.600 149.500 149.58098 2025-12-13 09:00:00+00:00 149.580 149.620 149.550 149.60099 2025-12-13 10:00:00+00:00 149.600 149.650 149.590 149.640移動平均の計算
pandasを使えば、移動平均の計算は1行です。
def calculate_ma(df, fast_period=5, slow_period=20): """ 移動平均を計算してDataFrameに追加 """ df["ma_fast"] = df["close"].rolling(window=fast_period).mean() df["ma_slow"] = df["close"].rolling(window=slow_period).mean() return df
# 計算実行df = calculate_ma(df)print(df[["close", "ma_fast", "ma_slow"]].tail())出力例:
close ma_fast ma_slow95 149.510 149.4700 149.420096 149.520 149.4900 149.435097 149.580 149.5100 149.450098 149.600 149.5440 149.470099 149.640 149.5700 149.4900MQL4ではiMA()関数を使いましたが、Pythonではrolling().mean()で同じことができます。
クロス検出ロジック
ゴールデンクロス/デッドクロスを検出します。
def detect_cross(df): """ クロスを検出
Returns: "golden_cross": ゴールデンクロス発生 "dead_cross": デッドクロス発生 None: クロスなし """ if len(df) < 2: return None
# 現在と1本前のMA値 current_fast = df["ma_fast"].iloc[-1] current_slow = df["ma_slow"].iloc[-1] prev_fast = df["ma_fast"].iloc[-2] prev_slow = df["ma_slow"].iloc[-2]
# NaNチェック if pd.isna(current_fast) or pd.isna(prev_fast): return None
# ゴールデンクロス: 短期MAが長期MAを下から上に突き抜けた if prev_fast < prev_slow and current_fast > current_slow: return "golden_cross"
# デッドクロス: 短期MAが長期MAを上から下に突き抜けた if prev_fast > prev_slow and current_fast < current_slow: return "dead_cross"
return None
# テストsignal = detect_cross(df)print(f"シグナル: {signal}")完成版:MA Cross Bot
すべてを組み合わせた完成版です。
import osimport timeimport hmacimport hashlibimport jsonimport requestsimport pandas as pdfrom datetime import datetimefrom dotenv import load_dotenv
load_dotenv()
# === 設定 ===API_KEY = os.environ.get("GMO_FX_API_KEY")API_SECRET = os.environ.get("GMO_FX_API_SECRET")SYMBOL = "USD_JPY"ORDER_SIZE = 100FAST_MA_PERIOD = 5SLOW_MA_PERIOD = 20CHECK_INTERVAL = 60 # 秒
# === APIヘルパー関数 ===def create_signature(timestamp, method, path, body=""): text = timestamp + method + path + body return hmac.new( bytes(API_SECRET.encode("ascii")), bytes(text.encode("ascii")), hashlib.sha256 ).hexdigest()
def get_timestamp(): return str(int(time.time() * 1000))
def private_request(method, path, body=None): """Private APIリクエスト""" timestamp = get_timestamp() body_str = "" if body is None else json.dumps(body)
headers = { "API-KEY": API_KEY, "API-TIMESTAMP": timestamp, "API-SIGN": create_signature(timestamp, method, path, body_str), "Content-Type": "application/json" }
url = "https://forex-api.coin.z.com/private" + path
if method == "GET": response = requests.get(url, headers=headers) elif method == "POST": response = requests.post(url, headers=headers, data=body_str)
return response.json()
# === データ取得 ===def get_klines(symbol, interval="1hour"): """ローソク足データを取得""" url = "https://forex-api.coin.z.com/public/v1/klines" params = { "symbol": symbol, "interval": interval, "date": datetime.now().strftime("%Y%m%d") }
response = requests.get(url, params=params) data = response.json()
if data["status"] == 0: df = pd.DataFrame(data["data"]) df["openTime"] = pd.to_datetime(df["openTime"]) df["close"] = df["close"].astype(float) return df return None
# === テクニカル分析 ===def calculate_ma(df, fast_period, slow_period): """移動平均を計算""" df["ma_fast"] = df["close"].rolling(window=fast_period).mean() df["ma_slow"] = df["close"].rolling(window=slow_period).mean() return df
def detect_cross(df): """クロスを検出""" if len(df) < 2: return None
current_fast = df["ma_fast"].iloc[-1] current_slow = df["ma_slow"].iloc[-1] prev_fast = df["ma_fast"].iloc[-2] prev_slow = df["ma_slow"].iloc[-2]
if pd.isna(current_fast) or pd.isna(prev_fast): return None
if prev_fast < prev_slow and current_fast > current_slow: return "golden_cross" if prev_fast > prev_slow and current_fast < current_slow: return "dead_cross"
return None
# === 取引 ===def get_positions(): """保有ポジションを取得""" result = private_request("GET", f"/v1/openPositions?symbol={SYMBOL}") if result["status"] == 0: return result["data"]["list"] return []
def place_order(side): """成行注文を出す""" body = { "symbol": SYMBOL, "side": side, "executionType": "MARKET", "size": str(ORDER_SIZE), "timeInForce": "FAK" } result = private_request("POST", "/v1/order", body)
if result["status"] == 0: print(f"✅ 注文成功: {side} {ORDER_SIZE} {SYMBOL}") return True else: print(f"❌ 注文エラー: {result}") return False
def close_all_positions(side): """全ポジションを決済""" positions = get_positions() for pos in positions: if (pos["side"] == "BUY" and side == "SELL") or \ (pos["side"] == "SELL" and side == "BUY"): body = { "symbol": SYMBOL, "side": side, "executionType": "MARKET", "size": pos["size"], "positionId": pos["positionId"], "timeInForce": "FAK" } private_request("POST", "/v1/closeOrder", body) print(f"✅ 決済: {pos['side']} {pos['size']}")
# === メインループ ===def run_bot(): print("=" * 50) print(f"🤖 Python MA Cross Bot 起動") print(f" 通貨ペア: {SYMBOL}") print(f" 短期MA: {FAST_MA_PERIOD} / 長期MA: {SLOW_MA_PERIOD}") print("=" * 50)
current_position = None # "LONG", "SHORT", or None
while True: try: # ローソク足データを取得 df = get_klines(SYMBOL) if df is None: print("データ取得失敗。リトライ...") time.sleep(10) continue
# 移動平均を計算 df = calculate_ma(df, FAST_MA_PERIOD, SLOW_MA_PERIOD)
# クロスを検出 signal = detect_cross(df)
# 現在の状態を表示 latest = df.iloc[-1] print(f"[{datetime.now().strftime('%H:%M:%S')}] " f"Close: {latest['close']:.3f} | " f"MA{FAST_MA_PERIOD}: {latest['ma_fast']:.3f} | " f"MA{SLOW_MA_PERIOD}: {latest['ma_slow']:.3f} | " f"Signal: {signal or '-'}")
# シグナルに応じて売買 if signal == "golden_cross" and current_position != "LONG": print("🔔 ゴールデンクロス検出!") if current_position == "SHORT": close_all_positions("BUY") place_order("BUY") current_position = "LONG"
elif signal == "dead_cross" and current_position != "SHORT": print("🔔 デッドクロス検出!") if current_position == "LONG": close_all_positions("SELL") place_order("SELL") current_position = "SHORT"
except Exception as e: print(f"エラー: {e}")
time.sleep(CHECK_INTERVAL)
if __name__ == "__main__": run_bot()MQL4 vs Python 比較
同じ移動平均線クロス戦略を、MQL4とPythonで実装しました。
MA値の取得
MQL4:
double fastMA = iMA(NULL, 0, 5, 0, MODE_SMA, PRICE_CLOSE, 0);Python:
df["ma_fast"] = df["close"].rolling(window=5).mean()クロス検出
MQL4:
if(fastMA_prev < slowMA_prev && fastMA_current > slowMA_current)Python:
if prev_fast < prev_slow and current_fast > current_slow:注文
MQL4:
OrderSend(Symbol(), OP_BUY, 0.01, Ask, 3, sl, tp, "comment", magic, 0, clrGreen);Python:
private_request("POST", "/v1/order", {"symbol": "USD_JPY", "side": "BUY", ...})ロジックは同じ、書き方が違うだけ!
Pythonならではの拡張
データ分析
# 過去100本分のリターンを分析df["return"] = df["close"].pct_change()print(f"平均リターン: {df['return'].mean():.4%}")print(f"標準偏差: {df['return'].std():.4%}")print(f"シャープレシオ: {df['return'].mean() / df['return'].std():.2f}")複数時間足の分析
# 1時間足と日足の両方を分析df_1h = get_klines(SYMBOL, "1hour")df_1d = get_klines(SYMBOL, "1day")
# 日足が上昇トレンドの時だけ、1時間足でエントリーif df_1d["close"].iloc[-1] > df_1d["ma_slow"].iloc[-1]: # 1時間足のシグナルに従う ...Slack通知
def send_slack(message): webhook_url = os.environ.get("SLACK_WEBHOOK_URL") requests.post(webhook_url, json={"text": message})
# シグナル発生時に通知if signal: send_slack(f"🔔 {signal} 検出! {SYMBOL}")次のステップ
移動平均線クロスボットができました!
次はRSIを使った逆張りボットをPythonで作ります。
📚 次に読む記事: Python RSI逆張りボットを作ろう
Pythonならpandas-taという便利なライブラリで、RSIはもちろんMACD、ボリンジャーバンドなど、あらゆるテクニカル指標が簡単に計算できます!
🎁おすすめツール・サービス
※上記リンクにはプロモーション(広告)が含まれています
量子相場謀略|ホソノP
ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。