Bybit APIでレバレッジ・マージンモード・
リスクリミットをPythonから設定する
110043の正体

set_leverage() を呼ぶと retCode 110043 が返ってきて驚く——これは「エラー」ではなく「既に同じ値なので変更しなかった」という意味です。この記事ではレバレッジ設定の正しい扱い、クロス/分離/ポートフォリオマージンの違い、リスクリミット階層が最大レバレッジを制限する仕組み、そしてbot起動時に口座設定を宣言的に収束させる実装までをサンプルコード7本でまとめます。

確認日
2026-07-30
API バージョン
Bybit V5
Python
3.10 以上
pybit
5.17.0

※ Bybit V5は機能追加のペースが速いAPIです。パラメータの最終的な正解は必ず公式ドキュメントで確認してください。本記事のコードはすべてテストネットで動作確認しています。

目次

  1. 110043の正体|エラーではない
  2. レバレッジ設定の正しい実装
  3. 「設定レバレッジ」と「実効レバレッジ」は別物
  4. マージンモード|クロス・分離・ポートフォリオ
  5. リスクリミット階層が上限を決める
  6. 口座設定を宣言的に収束させる
  7. レバレッジ変更が既存ポジションに与える影響
  8. レバレッジ・マージン関連のエラーコード
  9. よくある疑問Q&A

この記事は記事20(ポジション管理)の Position モデルと記事03の SymbolRegistry を使います。

110043の正体|エラーではない

最初に誰もが遭遇する挙動現象
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倍にする)は達成されている。
110043を例外にすると、再起動のたびにbotが落ちる

retCode != 0 を一律に例外にする実装だと、bot起動時のレバレッジ設定で毎回クラッシュします(初回だけ成功し、2回目以降は110043)。

これは記事06で分類したBENIGN(実質成功)コードの代表例です。同種のものに 110025(ポジションモード変更なし)、110026(マージンモード変更なし)、110027(マージン変更なし)、110075(リスクID変更なし)があります。設定系のAPIは「冪等な操作」なので、変更なしは成功として扱うのが正しい設計です。

レバレッジ設定の正しい実装

パラメータ内容注意
categorylinear / inverse現物・オプションには設定できない
symbol銘柄銘柄ごとに設定(口座全体ではない)
buyLeverageロング側のレバレッジ文字列で渡す
sellLeverageショート側のレバレッジクロスマージンではbuyと同じ値にする必要がある
leverage.pyサンプル1
"""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ポジションに必要な証拠金の倍率。「最大でどこまで建てられるか」の枠 直接のリスクではない
実効レバレッジ ポジション想定元本 ÷ 口座純資産。実際に取っているリスク これが本当のリスク量
レバレッジ10倍で0.1BTC建てても、実効レバレッジは1倍未満のことがある

資金12,000 USDTでBTC(価格120,000)を0.1BTC買った場合、想定元本は12,000 USDTなので実効レバレッジは1.0倍です。設定レバレッジが10倍でも、必要証拠金が1,200 USDTになるだけで、価格変動に対する損益は変わりません。

つまり設定レバレッジは「証拠金の使用効率」を決めるだけで、リスク量を決めるのはポジションサイズです。「レバレッジを下げれば安全」というのは誤解で、同じポジションサイズなら損益は同じです(違うのは清算までの距離だけ)。

リスクを管理したいなら、レバレッジ設定ではなくポジションサイズを制御してください(記事23)。

effective_leverage.pyサンプル2
"""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サンプル3
"""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サンプル4
"""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サンプル5
"""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サンプル6
"""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サンプル7
"""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 を設定できない
参考リンク(一次情報)

よくある疑問Q&A

レバレッジを低くすれば安全ですか?
ポジションサイズが同じなら、損益はレバレッジ設定に依存しません。サンプル2で数値で示したとおり、資金12,000でBTC 0.1枚を持つなら、設定が1倍でも50倍でも価格が10%下がれば損失は1,200 USDTです。変わるのは「必要証拠金」と「清算までの距離」だけです。
したがって「レバレッジを下げる」ことに意味があるのは清算距離を広げる目的のときだけで、リスク量を減らしたいならポジションサイズを小さくする必要があります(記事23)。この誤解は非常に多いので、実効レバレッジで管理する習慣をつけてください。
レバレッジは口座全体の設定ですか?
銘柄ごとの設定です。BTCUSDTを5倍、ETHUSDTを10倍のように個別に設定できます。そのため複数銘柄を扱うbotは、起動時に全対象銘柄のレバレッジを設定する必要があります(サンプル5の apply_setup)。新しい銘柄を扱い始めたときに設定を忘れると、取引所の既定値(銘柄によって異なる)で取引されます。
クロスマージンと分離マージン、どちらを使うべきですか?
botの運用方針で決まります。クロスは資金効率が良い反面、1銘柄の損失が口座全体を巻き込みます。分離はそのポジションの証拠金だけを失うため損失が隔離されますが、資金効率が落ち、証拠金を追加しないと早く清算されます。
複数銘柄を扱う戦略で「1つの銘柄の想定外の動きで全部失いたくない」なら分離、資金効率を重視するならクロスです。資金調達率アービトラージ(記事35)のように現物と先物を組み合わせる戦略では、証拠金共有が前提なのでクロス(統合口座)が必要になります。
ポジションを持ったままレバレッジを変更できますか?
クロスマージンではできません(110036)。分離マージンでは可能な場合がありますが、必要証拠金が変わるため清算価格が動きます。レバレッジを下げれば必要証拠金が増え、余力が減ります。上げれば清算価格が現在値に近づきます。
いずれにせよポジション保有中のレバレッジ変更は避けるのが安全です。botの設計としては、起動時(ポジションなしの状態)に設定を確定させ、稼働中は変更しないのが基本です。
リスクリミット階層は自分で上げるべきですか?
大きなポジションを建てる必要があるときだけです。階層を上げると最大レバレッジが下がり維持証拠金率が上がるため、清算されやすくなります。既定の階層1で足りるなら変更しないでください。set_risk_limit() で変更できますが、110047(余力不足)や110048(ポジション価値が新しい上限を超える)で拒否されることがあります。階層を上げる必要が出た時点で、そもそもポジションが資金に対して大きすぎる可能性を検討してください。
isReduceOnlyがtrueになっていて新規建てできません
リスクリミットの調整や、取引所側の制限(上場終了前、リスク管理措置)により「縮小のみ許可」の状態になっています。この状態では新規建てが110023で拒否されます。原因はポジション規模がリスクリミットの上限を超えた、または銘柄の取扱いが変更された等です。get_positions() の isReduceOnly を監視して検知し(記事20のサンプル8)、新規エントリーを止めて既存ポジションの管理に切り替えるのが正しい対応です。
起動時の設定でbotが落ちないようにするには?
サンプル5の apply_setup() のように、(1) 110043等のBENIGNコードは無視、(2) 1銘柄の失敗で全体を止めない、(3) 事後に verify_setup() で確認という3点を守ってください。特に(3)が重要で、「設定に失敗したまま起動してしまう」のを防げます。設定が期待値と違うなら、botを起動せずに通知して人間の判断を待つのが安全です。

注意事項・免責事項

投資は自己判断・自己責任でお願いいたします。本ブログの情報・ツール・ソフトウェアを参考・使用して、取引をされて何らかの損失を被られても、当サイトでは責任は一切負いません(間接的・付随的な障害・損失も含め一切の責任を負いません)。自己責任でご利用ください。

当サイトからダウンロードしたプログラム・EAは本人利用に限り完全無料でご利用いただけます。プログラムの全てまたは一部の複製・コピー・リバースエンジニアリング・改変・2次配布・商用利用・転売・販売等は一切禁止いたします。違反を発見した場合は法的処置を行います。

当サイト上に存在するソフトウェア・ツール・文章・画像・動画等の著作物を無断転載することを禁止します。引用の範囲を超えるものについては、法的処置を行います。

当サイトで配布するツール・ソフトウェアは無料提供ですのでサポートなどは行っておりません(逆に丁寧にサポートしているサイトはほとんど違法サイトですのでご注意ください。投資助言・代理業登録なしでサポート・助言することは法律で禁止されています)。万が一、ツールやソフトウェアに不具合や問題があっても当サイトでは修正・対応などができない場合があります。ご利用の環境でトラブルが発生しても当サイトでは助言することはできませんので自己責任でご利用ください。検索するなどして自分で解決してください。

当サイトでは取引所・証券会社へのリンクを貼っていますが、これは情報として提供するものであり、勧誘行為などは一切行っておりません。そのため、取引所・証券会社の登録方法・使い方・オススメ金融機関・オススメ金融商品などを説明した記事はサイト内にありませんので、ご自身で調べた上で判断ください(本当はサイト内で丁寧に説明したいのですが、仲介・取次ぎにあたる可能性があり第一種金融商品取引業登録を行っていない場合、違法となる可能性があるため出来ません。ご容赦ください)。

当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。当サイト掲載情報やリンク先の掲載情報の合法性、正確性、道徳性、最新性、適切性、著作権の許諾や有無など、その内容については一切の保証を致しかねます。

当サイトは、ご訪問いただいた時間、訪問者のアクセス元のホスト名、IPアドレス、OSとブラウザのバージョン、リンク元のURL、モニターの情報等のパソコン環境情報を収集し、アクセス解析をしております。この目的は訪問者が検索サイトからご訪問いただいた場合、どのような検索で当サイトに来訪いただいたかの解析にのみ使用しており、第3者に公開はいたしません。また、個人を特定するものではありません。