Alpaca で株・暗号資産の自動売買を動かした後、OANDA で FX に対応したときに想定外のハマりどころが連発しました。FX 特有の罠を5つ紹介します。
罠1:JPY建て口座の残高を USD に換算しないとポジションサイズがおかしくなる
OANDA 口座が JPY 建ての場合、/accounts/{id}/summary が返す balance や NAV は日本円です。そのまま USD 計算のサイジング式に入れると、為替レートが1ドル=150円の場合に 150倍の発注量 になります。
// account-tools.ts
const summary = await oanda.getAccountSummary();
let navUsd = summary.nav;
// JPY 建て口座の場合は USD に換算してからサイザーに渡す
if (summary.currency === "JPY") {
const tick = await oanda.getLatestPrice("USD_JPY");
if (tick && tick.price > 0) {
navUsd = summary.nav / tick.price; // 例: 1,500,000 / 150 = 10,000 USD
}
}
さらに JPY 建てペア(USD_JPY, GBP_JPY)は SL の「1 pip 価値」が USD 換算で異なります。厳密には1 unit あたりの pip 価値は 1 pip / entry_rate USD になります。そのため JPY ペアのサイジングは保守的(小さめ)になります。
罠2:USD_base ペアの SL 距離は外貨建てになる
USD_JPY や USD_CAD のように USD が base(左)の場合、|entry - SL| の単位が quote 通貨(JPY/CAD) になります。
// position-sizer.ts
const quoteConv = isUsdBase ? entry : 1;
// USD_JPY: entry ≈ 150, SL距離 0.5円 → USD換算 = 0.5 / 150 = 0.00333 USD/unit
const riskPerUnitUSD = riskPerShare / quoteConv;
quantity = Math.floor(riskTarget / riskPerUnitUSD);
EUR_USD のような quote-USD ペアならこの換算は不要(quoteConv = 1)ですが、USD_JPY 系は必ず換算が必要です。これを忘れると発注量が正しくなりません。
罠3:OANDA の SL/TP はネイティブ管理で、ソフトウェア監視が不要な代わりに決済検知が難しい
Alpaca の暗号資産では SL/TP をソフトウェアで監視してクローズします。OANDA は stopLossOnFill / takeProfitOnFill を注文時に渡せばブローカー側でネイティブ管理してくれます。
// OANDA の注文(SL/TP はネイティブ)
{
"order": {
"type": "MARKET",
"instrument": "GBP_USD",
"units": "10000",
"stopLossOnFill": { "price": "1.2450" },
"takeProfitOnFill": { "price": "1.2650" }
}
}
ただし問題があります。ブローカーが bar close 前のティックで SL/TP を執行した場合、バーが閉じたときには価格が SL/TP を越えていないように見える ことがあります。
SL = 1.2450
15:00 バーの動き: 1.2460 → 1.2449(SL ヒット)→ 1.2455(バー終値)
→ バー終値 1.2455 > SL 1.2450 → ソフトウェア判定: まだオープン!
→ でも実際はブローカー側でクローズ済み
解決策:OANDA の closed trades API で同期する
// hermes.ts
private async syncOandaFxOutcomes(): Promise<void> {
const pendingFx = this.memory.getPendingTrades()
.filter(e => e.type === "real" && e.symbol.includes("_"));
for (const entry of pendingFx) {
// OANDA の決済済みトレードと照合
const closed = await oanda.getClosedTrades(entry.symbol, entry.timestamp);
if (closed.length) {
const outcome = closed[0].realizedPL >= 0 ? "WIN" : "LOSS";
this.memory.updateOutcome(entry.id, outcome, pnlPct);
}
}
}
毎サイクルの冒頭で OANDA の closed trades API を叩いて、メモリ側の記録を同期します。
罠4:OANDA の REST API ページネーション
/trades エンドポイントで from + to + count を同時に指定すると API がエラーを返します。
// ❌ これはエラーになる
const params = { from: startTime, to: endTime, count: 50 };
// ✅ from + count のみ(to を除く)
const params = { from: startTime, count: 50 };
OANDA の API ドキュメントには書いてありますが見落としやすいです。from+to+count の組み合わせは拒否されます。
罠5:GBP ペアのレバレッジ上限が他ペアと異なる
OANDA のペーパー口座では GBP_USD / GBP_JPY は最大レバレッジが 20x に制限されていますが、EUR_USD は 25x、USD_JPY は 30x など異なります。
一律で最大レバレッジを設定すると GBP ペアで上限を超えた発注をしてしまいます。
// ペア別レバレッジキャップを strategy-params.json で管理
{
"GBP_USD": {
"leverageCap": 20
},
"EUR_USD": {
"leverageCap": 25
}
}
// position-sizer.ts
const effectiveLeverage = leverageOverride ?? this.maxLeverage;
const maxByLeverage = Math.floor(equity * effectiveLeverage / notionalPerUnit);
quantity = Math.min(quantity, maxByLeverage);
FX と株・暗号資産の主な違いまとめ
| 項目 | 株(Alpaca) | 暗号資産(Alpaca) | FX(OANDA) |
|---|---|---|---|
| SL/TP 管理 | ブローカー(bracket) | ソフトウェア | ブローカー(ネイティブ) |
| 決済検知 | bracket が自動クローズ | SL/TP チェックして close_position | closed trades API で同期 |
| 発注単位 | 株数(整数) | 小数可(0.0001 まで) | units(1,000〜100,000) |
| 口座通貨換算 | 不要(USD 建て) | 不要 | JPY 建て口座は要換算 |
| セッション | 平日 9:30-16:00 ET | 24/7 | 平日 24h(月〜金 UTC) |
FX は「執行はブローカーに任せられる」という利点がある一方、口座通貨・leverage・pip 換算など FX 固有の複雑さがあります。特に JPY 建て口座 × JPY ペアの組み合わせは最も注意が必要でした。