※ Bybit V5は機能追加のペースが速いAPIです。パラメータの最終的な正解は必ず公式ドキュメントで確認してください。本記事のコードはすべてテストネットで動作確認しています。
この記事は記事20(ポジション管理)の Position モデルと記事03の SymbolRegistry を使います。
from client import make_client
c = make_client()
# 1回目: 成功する
res = c.http.set_leverage(category="linear", symbol="BTCUSDT",
buyLeverage="10", sellLeverage="10")
print(res["retCode"], res["retMsg"])
# -> 0 OK
# 2回目: 同じ値を送ると「エラー」が返る
res = c.http.set_leverage(category="linear", symbol="BTCUSDT",
buyLeverage="10", sellLeverage="10")
print(res["retCode"], res["retMsg"])
# -> 110043 Set leverage has not been modified
#
# これは失敗ではない。「既に10倍なので何も変更しなかった」という意味。
# 目的(レバレッジを10倍にする)は達成されている。
retCode != 0 を一律に例外にする実装だと、bot起動時のレバレッジ設定で毎回クラッシュします(初回だけ成功し、2回目以降は110043)。
これは記事06で分類したBENIGN(実質成功)コードの代表例です。同種のものに 110025(ポジションモード変更なし)、110026(マージンモード変更なし)、110027(マージン変更なし)、110075(リスクID変更なし)があります。設定系のAPIは「冪等な操作」なので、変更なしは成功として扱うのが正しい設計です。
| パラメータ | 内容 | 注意 |
|---|---|---|
| category | linear / inverse | 現物・オプションには設定できない |
| symbol | 銘柄 | 銘柄ごとに設定(口座全体ではない) |
| buyLeverage | ロング側のレバレッジ | 文字列で渡す |
| sellLeverage | ショート側のレバレッジ | クロスマージンではbuyと同じ値にする必要がある |
"""leverage.py — レバレッジ設定(110043を正しく扱う)
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
# 設定系APIの「変更なし」を意味するコード。実質成功として扱う
NOT_MODIFIED = {
110043: "レバレッジは変更されていません(既に同じ値)",
110025: "ポジションモードは変更されていません",
110026: "マージンモードは変更されていません",
110027: "マージンは変更されていません",
110075: "リスクIDは変更されていません",
}
# 変更を拒否される正当な理由
LEVERAGE_BLOCKED = {
110013: "リスクリミット階層のためレバレッジを設定できません",
110036: "クロスマージンモードのためレバレッジを変更できません",
110038: "ポートフォリオマージンモードのためレバレッジを変更できません",
110086: "注文レバレッジがリスクリミット階層の上限を超えています",
110107: "inverse契約で上限を超えるレバレッジは許可されません",
}
class LeverageError(RuntimeError):
def __init__(self, ret_code: int, ret_msg: str, hint: str = ""):
detail = f" — {hint}" if hint else ""
super().__init__(f"[{ret_code}] {ret_msg}{detail}")
self.ret_code = ret_code
@dataclass
class LeverageResult:
symbol: str
leverage: Decimal
changed: bool # False は「既に同じ値だった」
ret_code: int
def line(self) -> str:
state = "変更" if self.changed else "変更なし(既に同値)"
return f"{self.symbol:<12} {self.leverage}x {state}"
def set_leverage(http, symbol: str, leverage: Decimal | int | str,
category: str = "linear",
sell_leverage: Decimal | int | str | None = None
) -> LeverageResult:
"""レバレッジを設定する。110043は「変更なし」として正常に扱う。
クロスマージンでは buy と sell を同じ値にする必要がある。
"""
buy = str(leverage)
sell = str(sell_leverage if sell_leverage is not None else leverage)
res = http.set_leverage(category=category, symbol=symbol,
buyLeverage=buy, sellLeverage=sell)
code = res["retCode"]
if code == 0:
return LeverageResult(symbol, Decimal(buy), changed=True, ret_code=0)
if code in NOT_MODIFIED:
return LeverageResult(symbol, Decimal(buy), changed=False,
ret_code=code)
raise LeverageError(code, res["retMsg"],
LEVERAGE_BLOCKED.get(code, ""))
def get_current_leverage(http, symbol: str,
category: str = "linear") -> Decimal | None:
"""現在の設定レバレッジを取得する。
ポジションが無くても leverage フィールドは返る
(その銘柄に対する設定値として保持されている)。
"""
res = http.get_positions(category=category, symbol=symbol)
if res["retCode"] != 0:
return None
rows = res["result"]["list"]
if not rows:
return None
lev = rows[0].get("leverage")
return Decimal(str(lev)) if lev else None
def ensure_leverage(http, symbol: str, target: Decimal | int,
category: str = "linear",
registry=None) -> LeverageResult:
"""望むレバレッジへ収束させる(冪等)。
銘柄の上限を超える指定は、事前にクリップして無駄なエラーを避ける。
"""
target = Decimal(str(target))
if registry is not None:
max_lev = registry.get(symbol, category).max_leverage
if max_lev > 0 and target > max_lev:
print(f" [clip] {symbol}: 指定{target}x は上限{max_lev}x を"
f"超えるため {max_lev}x に切り下げます")
target = max_lev
current = get_current_leverage(http, symbol, category)
if current is not None and current == target:
return LeverageResult(symbol, target, changed=False, ret_code=0)
return set_leverage(http, symbol, target, category)
if __name__ == "__main__":
from client import make_client
from symbol_rules import SymbolRegistry
c = make_client()
reg = SymbolRegistry(c.http)
for symbol in ("BTCUSDT", "ETHUSDT"):
current = get_current_leverage(c.http, symbol)
max_lev = reg.get(symbol, "linear").max_leverage
print(f"{symbol}: 現在 {current}x / 上限 {max_lev}x")
print("\n--- 収束型で設定する(何度呼んでも安全) ---")
for _ in range(3):
r = ensure_leverage(c.http, "BTCUSDT", 5, registry=reg)
print(f" {r.line()}")
print("\n--- 上限超過は自動でクリップされる ---")
r = ensure_leverage(c.http, "BTCUSDT", 999, registry=reg)
print(f" {r.line()}")
ここは実務で最も誤解される点です。レバレッジを10倍に設定しても、10倍のリスクを取っていることにはなりません。
| 用語 | 意味 | リスクとの関係 |
|---|---|---|
| 設定レバレッジ | 1ポジションに必要な証拠金の倍率。「最大でどこまで建てられるか」の枠 | 直接のリスクではない |
| 実効レバレッジ | ポジション想定元本 ÷ 口座純資産。実際に取っているリスク | これが本当のリスク量 |
資金12,000 USDTでBTC(価格120,000)を0.1BTC買った場合、想定元本は12,000 USDTなので実効レバレッジは1.0倍です。設定レバレッジが10倍でも、必要証拠金が1,200 USDTになるだけで、価格変動に対する損益は変わりません。
つまり設定レバレッジは「証拠金の使用効率」を決めるだけで、リスク量を決めるのはポジションサイズです。「レバレッジを下げれば安全」というのは誤解で、同じポジションサイズなら損益は同じです(違うのは清算までの距離だけ)。
リスクを管理したいなら、レバレッジ設定ではなくポジションサイズを制御してください(記事23)。
"""effective_leverage.py — 実効レバレッジを計算し、設定値との違いを示す
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from position import Position, fetch_positions
@dataclass
class LeverageSnapshot:
equity: Decimal
total_notional: Decimal
total_im: Decimal # 使用中の初期証拠金
positions: list[Position]
@property
def effective_leverage(self) -> Decimal:
"""実際に取っているリスク倍率。これが本当の指標。"""
if self.equity <= 0:
return Decimal(0)
return self.total_notional / self.equity
@property
def margin_usage(self) -> Decimal:
"""証拠金の使用率。設定レバレッジが効くのはこちら。"""
if self.equity <= 0:
return Decimal(0)
return self.total_im / self.equity
def report(self) -> str:
lines = [
f"口座純資産 : {self.equity:>14,.2f}",
f"想定元本合計 : {self.total_notional:>14,.2f}",
f"使用中の証拠金 : {self.total_im:>14,.2f}",
"",
f"実効レバレッジ : {self.effective_leverage:>13.2f}x "
f"<- 実際のリスク",
f"証拠金使用率 : {self.margin_usage:>13.1%} "
f"<- 余力の指標",
"",
f"{'symbol':<12}{'設定レバ':>9}{'想定元本':>14}"
f"{'実効レバ':>10}{'価格1%で':>11}",
]
for p in self.positions:
eff = p.notional / self.equity if self.equity else Decimal(0)
pnl_1pct = p.notional * Decimal("0.01")
lines.append(
f"{p.symbol:<12}{p.leverage:>8}x{p.notional:>14,.0f}"
f"{eff:>9.2f}x{pnl_1pct:>+11,.2f}"
)
return "\n".join(lines)
def snapshot(http) -> LeverageSnapshot:
bal = http.get_wallet_balance(accountType="UNIFIED")["result"]["list"][0]
equity = Decimal(bal["totalEquity"] or "0")
total_im = Decimal(bal["totalInitialMargin"] or "0")
positions = fetch_positions(http)
total_notional = sum((p.notional for p in positions), Decimal(0))
return LeverageSnapshot(equity=equity, total_notional=total_notional,
total_im=total_im, positions=positions)
def demonstrate() -> None:
"""設定レバレッジを変えても損益は変わらないことを数値で示す。"""
equity = Decimal("12000")
price = Decimal("120000")
qty = Decimal("0.1")
notional = qty * price
print("資金 12,000 USDT で BTC 0.1枚(想定元本 12,000 USDT)を保有\n")
print(f"{'設定レバ':>9}{'必要証拠金':>14}{'余力':>12}"
f"{'価格-10%の損益':>16}{'実効レバ':>10}")
print("-" * 62)
for lev in (1, 2, 5, 10, 25, 50):
im = notional / lev
free = equity - im
pnl = -notional * Decimal("0.10") # 設定レバレッジに依存しない
print(f"{lev:>8}x{im:>14,.0f}{free:>12,.0f}"
f"{pnl:>+16,.0f}{notional / equity:>9.2f}x")
print("\n※ 損益はどの設定レバレッジでも同じ(-1,200 USDT)。")
print(" 変わるのは『必要証拠金』と『清算までの距離』だけ。")
print(" リスク量を決めるのはポジションサイズであり、レバレッジ設定ではない。")
if __name__ == "__main__":
from client import make_client
demonstrate()
print("\n" + "=" * 62)
c = make_client()
print(snapshot(c.http).report())
| モード | 証拠金 | 清算 | レバレッジ設定 | 向く用途 |
|---|---|---|---|---|
| クロス (Cross) |
口座全体を共有 | 口座全体の維持率で判定。個別のliqPriceは不定 | buy/sellを同値にする | 資金効率重視。統合口座の既定 |
| 分離 (Isolated) |
ポジションごとに分離 | そのポジションの証拠金のみ失う | buy/sellを別々に設定可 | 1銘柄の損失を隔離したい |
| ポートフォリオ (Portfolio) |
相殺を考慮して算出 | 口座全体 | 個別設定不可(110038) | オプションとの複合戦略 |
"""margin_mode.py — マージンモードの取得と切り替え
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
注意: モードの切り替えは口座全体の証拠金計算を変える操作。
ポジション・未約定注文がある状態では失敗する。
"""
from __future__ import annotations
from decimal import Decimal
from enum import Enum
from leverage import NOT_MODIFIED, LeverageError
class MarginMode(str, Enum):
REGULAR = "REGULAR_MARGIN" # 通常(クロス/分離を銘柄ごとに選ぶ)
PORTFOLIO = "PORTFOLIO_MARGIN" # ポートフォリオマージン
ISOLATED = "ISOLATED_MARGIN" # 分離(口座レベルの指定)
def get_margin_mode(http) -> str:
res = http.get_account_info()
if res["retCode"] != 0:
raise RuntimeError(f"get_account_info失敗: {res['retMsg']}")
return str(res["result"].get("marginMode", ""))
def set_margin_mode(http, mode: MarginMode) -> bool:
"""口座のマージンモードを切り替える。戻り値は実際に変更されたか。"""
res = http.set_margin_mode(setMarginMode=mode.value)
code = res["retCode"]
if code == 0:
print(f"[OK] マージンモードを {mode.value} に変更しました")
return True
if code in NOT_MODIFIED or code == 110073:
print(f"[skip] 既に {mode.value} です(または変更不要)")
return False
raise LeverageError(code, res["retMsg"],
"ポジション・未約定注文を解消してから再試行してください")
def switch_isolated(http, symbol: str, isolated: bool,
leverage: Decimal | int = 10,
category: str = "linear") -> bool:
"""銘柄単位でクロス/分離を切り替える。
tradeMode: 0 = クロス / 1 = 分離
分離に切り替える際はレバレッジも同時に指定する必要がある。
"""
res = http.switch_margin_mode(
category=category, symbol=symbol,
tradeMode=1 if isolated else 0,
buyLeverage=str(leverage), sellLeverage=str(leverage),
)
code = res["retCode"]
if code == 0:
print(f"[OK] {symbol} を{'分離' if isolated else 'クロス'}"
f"マージンに変更({leverage}x)")
return True
if code in NOT_MODIFIED:
print(f"[skip] {symbol} は既に"
f"{'分離' if isolated else 'クロス'}マージンです")
return False
if code == 110024:
raise LeverageError(code, res["retMsg"],
"ポジション保有中は切り替えできません")
raise LeverageError(code, res["retMsg"])
def describe_mode(http) -> str:
"""現在のモードと、その帰結を説明する。"""
mode = get_margin_mode(http)
bal = http.get_wallet_balance(accountType="UNIFIED")["result"]["list"][0]
mmr = Decimal(bal.get("accountMMRate") or "0")
lines = [f"口座のマージンモード: {mode}"]
if mode == MarginMode.PORTFOLIO.value:
lines += [
" - 銘柄ごとのレバレッジ設定は不可(110038)",
" - オプションとの相殺が効き、必要証拠金が減る",
]
else:
lines += [
" - 銘柄ごとにクロス/分離を選べる",
" - クロスでは liqPrice が空文字になる(口座全体で判定するため)",
]
lines.append(f" - 現在の口座維持率: {mmr * 100:.3f}%"
f"(100%に近づくと清算・記事22)")
return "\n".join(lines)
if __name__ == "__main__":
from client import make_client
c = make_client()
print(describe_mode(c.http))
# 銘柄単位の切り替え(ポジションが無い状態で実行)
# switch_isolated(c.http, "BTCUSDT", isolated=True, leverage=5)
クロスマージンは資金効率が良い代わりに、1つの銘柄の損失が口座全体の証拠金を削ります。アルトコインで想定外の損失が出ると、無関係なBTCのポジションまで清算されます。
統合口座では現物とデリバティブも証拠金を共有するため、「現物で買って長期保有しているBTCが、先物の損失で清算される」ことも起こりえます。長期保有資産と短期売買を同じ口座に置かないのが安全です(記事25でサブアカウント分離を扱います)。
「最大レバレッジ100倍」と書かれていても、ポジションが大きくなると使える倍率が下がります。これがリスクリミット(Risk Limit)です。
| 階層 | ポジション上限 | 最大レバレッジ | 維持証拠金率 |
|---|---|---|---|
| 1(既定) | 小 | 高い | 低い |
| 2 | 中 | 下がる | 上がる |
| 3以降 | 大 | さらに下がる | さらに上がる |
具体的な数値は銘柄ごとに異なるため、APIから階層表を取得して自分で確認する必要があります。
"""risk_limit.py — リスクリミット階層の取得と、実現可能な最大レバレッジの逆算
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class RiskTier:
risk_id: int
limit_value: Decimal # このtierで許容されるポジション価値の上限
max_leverage: Decimal
maintenance_margin: Decimal # 維持証拠金率
initial_margin: Decimal
def line(self) -> str:
return (f" id={self.risk_id:<4} 上限 {self.limit_value:>14,.0f} "
f"最大 {self.max_leverage:>6}x "
f"維持 {self.maintenance_margin * 100:>6.2f}% "
f"初期 {self.initial_margin * 100:>6.2f}%")
def fetch_risk_tiers(http, symbol: str,
category: str = "linear") -> list[RiskTier]:
res = http.get_risk_limit(category=category, symbol=symbol)
if res["retCode"] != 0:
raise RuntimeError(f"get_risk_limit失敗: {res['retMsg']}")
tiers: list[RiskTier] = []
for r in res["result"]["list"]:
tiers.append(RiskTier(
risk_id=int(r["id"]),
limit_value=Decimal(str(r["riskLimitValue"])),
max_leverage=Decimal(str(r["maxLeverage"])),
maintenance_margin=Decimal(str(r["maintenanceMargin"])),
initial_margin=Decimal(str(r["initialMargin"])),
))
return sorted(tiers, key=lambda t: t.limit_value)
def tier_for_notional(tiers: list[RiskTier],
notional: Decimal) -> RiskTier | None:
"""想定元本から、適用される階層を求める。"""
for t in tiers:
if notional <= t.limit_value:
return t
return tiers[-1] if tiers else None
def max_leverage_for_notional(tiers: list[RiskTier],
notional: Decimal) -> Decimal:
"""その規模のポジションで実際に使える最大レバレッジ。"""
t = tier_for_notional(tiers, notional)
return t.max_leverage if t else Decimal(1)
def max_notional_for_equity(tiers: list[RiskTier], equity: Decimal,
margin_usage: Decimal = Decimal("0.5")
) -> tuple[Decimal, RiskTier | None]:
"""資金から、階層の制約を守って建てられる最大の想定元本を求める。
階層が上がるほどレバレッジが下がるため、単純な equity * maxLeverage
では計算できない。各階層で実現可能な元本を試して最大値を取る。
"""
usable = equity * margin_usage
best = Decimal(0)
best_tier: RiskTier | None = None
for t in tiers:
# この階層のレバレッジで建てられる元本
candidate = usable * t.max_leverage
# ただしこの階層の上限を超えられない
candidate = min(candidate, t.limit_value)
if candidate > best:
best, best_tier = candidate, t
return best, best_tier
if __name__ == "__main__":
from client import make_client
c = make_client()
for symbol in ("BTCUSDT", "ETHUSDT"):
tiers = fetch_risk_tiers(c.http, symbol)
print(f"=== {symbol} のリスクリミット階層({len(tiers)}段) ===")
for t in tiers[:6]:
print(t.line())
if len(tiers) > 6:
print(f" ... 他 {len(tiers) - 6} 段")
print("\n 想定元本別に使える最大レバレッジ:")
for notional in ("10000", "100000", "1000000", "10000000"):
n = Decimal(notional)
lev = max_leverage_for_notional(tiers, n)
tier = tier_for_notional(tiers, n)
print(f" {n:>12,.0f} USDT -> {lev:>6}x "
f"(id={tier.risk_id if tier else '-'}, "
f"維持{tier.maintenance_margin * 100 if tier else 0:.2f}%)")
bal = c.http.get_wallet_balance(accountType="UNIFIED")["result"]["list"][0]
equity = Decimal(bal["totalEquity"] or "0")
if equity > 0:
cap, tier = max_notional_for_equity(tiers, equity)
print(f"\n 資金 {equity:,.0f} USDT(証拠金50%使用)で建てられる上限:")
print(f" 約 {cap:,.0f} USDT "
f"(id={tier.risk_id if tier else '-'})")
print()
リスクリミットは「大きなポジションは取引所にとってリスクなので、より多くの証拠金を要求する」という仕組みです。維持証拠金率が上がるということは、清算されやすくなることも意味します。
資金が増えてポジションが大きくなると、同じレバレッジ設定でも清算距離が縮むため、小資金で機能していた戦略がそのままスケールしません。この点は記事31のバックテストで maintenance_margin_rate を固定値にしている前提の限界でもあります。大きな資金で運用するなら、階層表を反映した清算シミュレーションが必要です(記事22)。
bot起動時に「レバレッジ・マージンモード・ポジションモード」を設定する処理は、命令形ではなく収束型で書くべきです。何度起動しても同じ結果になり、110043などで落ちません。
"""account_setup.py — 望む口座設定を宣言し、現状と差分だけ適用する
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
思想: 「設定する」ではなく「この状態であることを保証する」。
冪等なので起動のたびに安全に呼べる。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal
from leverage import LeverageError, ensure_leverage, get_current_leverage
from position_mode import ModeSwitchBlocked, ensure_mode
from risk_limit import fetch_risk_tiers, max_leverage_for_notional
@dataclass
class DesiredSetup:
"""botが前提とする口座設定。設定ファイルから読む想定。"""
symbols: dict[str, Decimal] = field(default_factory=dict) # symbol -> レバ
hedge_mode: bool = False
category: str = "linear"
# 想定する最大ポジション規模。リスクリミット階層の検査に使う
planned_notional: Decimal = Decimal("10000")
@dataclass
class SetupResult:
applied: list[str] = field(default_factory=list)
skipped: list[str] = field(default_factory=list)
failed: list[tuple[str, str]] = field(default_factory=list)
warnings: list[str] = field(default_factory=list)
@property
def ok(self) -> bool:
return not self.failed
def report(self) -> str:
lines = []
if self.applied:
lines.append("変更した設定:")
lines += [f" + {a}" for a in self.applied]
if self.skipped:
lines.append("既に望む状態:")
lines += [f" = {s}" for s in self.skipped]
if self.warnings:
lines.append("警告:")
lines += [f" ! {w}" for w in self.warnings]
if self.failed:
lines.append("失敗:")
lines += [f" x {sym}: {msg}" for sym, msg in self.failed]
return "\n".join(lines) or "変更なし"
def apply_setup(http, desired: DesiredSetup, registry=None) -> SetupResult:
"""望む設定へ収束させる。失敗しても他の銘柄の処理は続ける。"""
result = SetupResult()
for symbol, target_lev in desired.symbols.items():
# 1) リスクリミット階層で、その規模で目標レバレッジが使えるか検査
try:
tiers = fetch_risk_tiers(http, symbol, desired.category)
allowed = max_leverage_for_notional(tiers, desired.planned_notional)
if target_lev > allowed:
result.warnings.append(
f"{symbol}: 想定元本{desired.planned_notional:,.0f}では"
f"最大{allowed}xまで(指定{target_lev}x)"
)
except Exception as e:
result.warnings.append(f"{symbol}: 階層取得に失敗 ({e})")
# 2) ポジションモード
try:
ensure_mode(http, symbol, desired.hedge_mode, desired.category)
except ModeSwitchBlocked as e:
result.warnings.append(f"{symbol}: モード変更不可 ({e})")
# 3) レバレッジ
try:
r = ensure_leverage(http, symbol, target_lev, desired.category,
registry=registry)
if r.changed:
result.applied.append(f"{symbol} レバレッジ {r.leverage}x")
else:
result.skipped.append(f"{symbol} レバレッジ {r.leverage}x")
except LeverageError as e:
result.failed.append((symbol, str(e)))
return result
def verify_setup(http, desired: DesiredSetup) -> list[str]:
"""設定が本当に反映されているかを事後確認する。"""
problems: list[str] = []
for symbol, target in desired.symbols.items():
actual = get_current_leverage(http, symbol, desired.category)
if actual is None:
problems.append(f"{symbol}: レバレッジを確認できません")
elif actual != target:
problems.append(
f"{symbol}: 期待{target}x だが実際は{actual}x"
)
return problems
if __name__ == "__main__":
from client import make_client
from symbol_rules import SymbolRegistry
c = make_client()
reg = SymbolRegistry(c.http)
desired = DesiredSetup(
symbols={"BTCUSDT": Decimal("5"), "ETHUSDT": Decimal("5")},
hedge_mode=False,
planned_notional=Decimal("20000"),
)
print("--- 1回目 ---")
print(apply_setup(c.http, desired, registry=reg).report())
print("\n--- 2回目(冪等なので何も変わらない) ---")
print(apply_setup(c.http, desired, registry=reg).report())
print("\n--- 事後確認 ---")
problems = verify_setup(c.http, desired)
if problems:
for p in problems:
print(f" ! {p}")
print("\n 設定が反映されていません。botを起動しないでください")
else:
print(" すべて期待どおりです")
verify_setup() のような事後確認を入れているのが要点です。設定APIが成功を返しても、期待どおりの値になっているとは限りません(リスクリミットで切り下げられた、モードの制約で無視された、など)。
実運用では「設定が期待値と一致しなければbotを起動しない」という判断にしてください。想定と違うレバレッジで動き始めると、リスク量が意図とずれたまま取引が始まります。
"""leverage_impact.py — レバレッジ変更が証拠金と清算距離に与える影響を試算
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
注意: 実際の清算判定は口座全体の維持率で行われる(クロスマージン)。
これは「分離マージンでの近似」であり、傾向を掴むための試算。
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class LiqEstimate:
leverage: Decimal
initial_margin: Decimal
liq_price: Decimal
distance_pct: Decimal
def line(self) -> str:
return (f" {self.leverage:>5}x 証拠金 {self.initial_margin:>11,.2f} "
f"清算価格 {self.liq_price:>11,.2f} "
f"距離 {self.distance_pct:>6.2f}%")
def estimate_liquidation(entry: Decimal, qty: Decimal, leverage: Decimal,
mmr: Decimal = Decimal("0.005"),
is_long: bool = True) -> LiqEstimate:
"""分離マージン前提の清算価格の近似。
証拠金 = 想定元本 / レバレッジ
清算は「証拠金 + 含み損 <= 維持証拠金」で発生する。
ロング: entry * (1 - 1/lev + mmr)
ショート: entry * (1 + 1/lev - mmr)
"""
notional = entry * qty
im = notional / leverage
if is_long:
liq = entry * (Decimal(1) - Decimal(1) / leverage + mmr)
else:
liq = entry * (Decimal(1) + Decimal(1) / leverage - mmr)
distance = abs(liq - entry) / entry * 100
return LiqEstimate(leverage=leverage, initial_margin=im,
liq_price=liq, distance_pct=distance)
def compare_leverages(http, symbol: str, category: str = "linear") -> None:
"""既存ポジションについて、レバレッジ別の清算距離を比較する。"""
from position import fetch_positions
positions = [p for p in fetch_positions(http)
if p.symbol == symbol and p.category == category]
if not positions:
print(f"{symbol}: ポジションがありません(想定値で試算します)")
entry, qty, is_long = Decimal("120000"), Decimal("0.1"), True
current_lev = Decimal("10")
else:
p = positions[0]
entry, qty, is_long = p.avg_price, p.size, p.is_long
current_lev = p.leverage
print(f"{symbol}: {p.side} {qty} @ {entry}(現在 {current_lev}x)")
print(f"\n想定元本 {entry * qty:,.2f} USDT / "
f"{'ロング' if is_long else 'ショート'}")
print(f"{'レバ':>7}{'必要証拠金':>13}{'清算価格':>14}{'清算距離':>10}")
print("-" * 46)
for lev in (Decimal(x) for x in (1, 2, 3, 5, 10, 20, 50)):
print(estimate_liquidation(entry, qty, lev, is_long=is_long).line())
print("\n※ ポジションサイズが同じなら損益は同じ。")
print(" レバレッジを上げて変わるのは『必要証拠金が減る』ことと")
print(" 『清算までの距離が縮む』ことだけ。")
print(" クロスマージンでは口座全体の維持率で判定されるため、")
print(" 実際の清算はこの試算と一致しない(記事22で正確に扱う)。")
if __name__ == "__main__":
from client import make_client
c = make_client()
compare_leverages(c.http, "BTCUSDT")
"""test_leverage.py — 110043の扱いが正しいかテストする
本コードは本人利用に限り自由に使用可。再配布・商用利用・販売は禁止。
pytest test_leverage.py -v
"""
from __future__ import annotations
from decimal import Decimal
import pytest
from leverage import LeverageError, set_leverage
from mock_exchange import MockExchange, err, ok
def test_110043_treated_as_success():
"""110043は例外にせず、changed=False で返すこと。"""
ex = MockExchange()
# MockExchange.set_leverage は既定で110043を返す
r = set_leverage(ex, "BTCUSDT", 10)
assert r.changed is False
assert r.ret_code == 110043
assert r.leverage == Decimal("10")
def test_success_marks_changed():
ex = MockExchange()
ex.inject_error["set_leverage"] = ok({})
r = set_leverage(ex, "BTCUSDT", 10)
assert r.changed is True
assert r.ret_code == 0
def test_real_error_raises():
"""110013(リスクリミットで拒否)は例外になること。"""
ex = MockExchange()
ex.inject_error["set_leverage"] = err(
110013, "Cannot set leverage due to risk limit level")
with pytest.raises(LeverageError) as e:
set_leverage(ex, "BTCUSDT", 100)
assert e.value.ret_code == 110013
# ヒントが付与されていること
assert "リスクリミット" in str(e.value)
def test_cross_margin_requires_same_leverage():
"""クロスマージン想定: buy/sellを同値で送っていること。"""
ex = MockExchange()
ex.inject_error["set_leverage"] = ok({})
set_leverage(ex, "BTCUSDT", 7)
sent = ex.called("set_leverage")[-1]
assert sent["buyLeverage"] == "7"
assert sent["sellLeverage"] == "7"
def test_leverage_passed_as_string():
"""数値ではなく文字列で送っていること(10001対策)。"""
ex = MockExchange()
ex.inject_error["set_leverage"] = ok({})
set_leverage(ex, "BTCUSDT", Decimal("12.5"))
sent = ex.called("set_leverage")[-1]
assert isinstance(sent["buyLeverage"], str)
assert sent["buyLeverage"] == "12.5"
def test_asymmetric_leverage_for_isolated():
"""分離マージンではbuy/sellを別々に指定できること。"""
ex = MockExchange()
ex.inject_error["set_leverage"] = ok({})
set_leverage(ex, "BTCUSDT", 10, sell_leverage=5)
sent = ex.called("set_leverage")[-1]
assert sent["buyLeverage"] == "10"
assert sent["sellLeverage"] == "5"
| retCode | 意味 | 対処 |
|---|---|---|
| 110043 | レバレッジ変更なし | 実質成功。無視する |
| 110025 110026 / 110027 | モード・マージン変更なし | 実質成功。無視する |
| 110075 | リスクID変更なし | 実質成功。無視する |
| 110013 | リスクリミット階層のため設定不可 | ポジション規模に対して高すぎる。階層表を確認(サンプル4) |
| 110016 | 数量がリスクリミットを超過 | 階層を上げるか、ポジションを分割する |
| 110024 | ポジション保有中は切替不可 | 全決済してから切り替える |
| 110028 | 未約定注文があると切替不可 | 全キャンセルしてから切り替える |
| 110036 | クロスマージンのため変更不可 | 分離マージンに切り替えてから設定する |
| 110038 | ポートフォリオマージンのため変更不可 | 個別のレバレッジ設定はできない仕様 |
| 110086 | 注文レバレッジが階層上限を超過 | 発注時のレバレッジを下げる |
| 110073 | マージンモード設定失敗 | 口座の状態(ポジション・注文)を確認 |
| 110076 | 自動証拠金追加は分離のみ | クロスでは autoAddMargin を設定できない |
apply_setup)。新しい銘柄を扱い始めたときに設定を忘れると、取引所の既定値(銘柄によって異なる)で取引されます。set_risk_limit() で変更できますが、110047(余力不足)や110048(ポジション価値が新しい上限を超える)で拒否されることがあります。階層を上げる必要が出た時点で、そもそもポジションが資金に対して大きすぎる可能性を検討してください。get_positions() の isReduceOnly を監視して検知し(記事20のサンプル8)、新規エントリーを止めて既存ポジションの管理に切り替えるのが正しい対応です。apply_setup() のように、(1) 110043等のBENIGNコードは無視、(2) 1銘柄の失敗で全体を止めない、(3) 事後に verify_setup() で確認という3点を守ってください。特に(3)が重要で、「設定に失敗したまま起動してしまう」のを防げます。設定が期待値と違うなら、botを起動せずに通知して人間の判断を待つのが安全です。投資は自己判断・自己責任でお願いいたします。本ブログの情報・ツール・ソフトウェアを参考・使用して、取引をされて何らかの損失を被られても、当サイトでは責任は一切負いません(間接的・付随的な障害・損失も含め一切の責任を負いません)。自己責任でご利用ください。
当サイトからダウンロードしたプログラム・EAは本人利用に限り完全無料でご利用いただけます。プログラムの全てまたは一部の複製・コピー・リバースエンジニアリング・改変・2次配布・商用利用・転売・販売等は一切禁止いたします。違反を発見した場合は法的処置を行います。
当サイト上に存在するソフトウェア・ツール・文章・画像・動画等の著作物を無断転載することを禁止します。引用の範囲を超えるものについては、法的処置を行います。
当サイトで配布するツール・ソフトウェアは無料提供ですのでサポートなどは行っておりません(逆に丁寧にサポートしているサイトはほとんど違法サイトですのでご注意ください。投資助言・代理業登録なしでサポート・助言することは法律で禁止されています)。万が一、ツールやソフトウェアに不具合や問題があっても当サイトでは修正・対応などができない場合があります。ご利用の環境でトラブルが発生しても当サイトでは助言することはできませんので自己責任でご利用ください。検索するなどして自分で解決してください。
当サイトでは取引所・証券会社へのリンクを貼っていますが、これは情報として提供するものであり、勧誘行為などは一切行っておりません。そのため、取引所・証券会社の登録方法・使い方・オススメ金融機関・オススメ金融商品などを説明した記事はサイト内にありませんので、ご自身で調べた上で判断ください(本当はサイト内で丁寧に説明したいのですが、仲介・取次ぎにあたる可能性があり第一種金融商品取引業登録を行っていない場合、違法となる可能性があるため出来ません。ご容赦ください)。
当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。当サイト掲載情報やリンク先の掲載情報の合法性、正確性、道徳性、最新性、適切性、著作権の許諾や有無など、その内容については一切の保証を致しかねます。
当サイトは、ご訪問いただいた時間、訪問者のアクセス元のホスト名、IPアドレス、OSとブラウザのバージョン、リンク元のURL、モニターの情報等のパソコン環境情報を収集し、アクセス解析をしております。この目的は訪問者が検索サイトからご訪問いただいた場合、どのような検索で当サイトに来訪いただいたかの解析にのみ使用しており、第3者に公開はいたしません。また、個人を特定するものではありません。