Binance Futures Testnet to Mainnet Promotion
When to Use
Invoke this when a strategy that has been running on the Binance Futures testnet is about
to route orders against a mainnet base URL. The promotion step itself is the hazard: the
same code, pointed at a different host with different credentials, moves from fake balances
to real leveraged capital with liquidation risk. This skill supplies the gate that must pass
before an order router is handed a mainnet ExchangeConfig, plus the account-level
reconciliation steps that testnet cannot exercise.
Use it for both USDⓈ-M (fapi) and COIN-M (dapi) futures.
When NOT to Use
- Spot or Margin promotion: Binance spot uses different hosts (
api.binance.com) and a different testnet; the host allowlist and the leverage/position-mode checks here do not apply. - Brokers with a single endpoint for both environments: If environments are distinguished
only by credentials, the host-binding logic is inapplicable — see
alpaca-paper-live-key-separationfor the credential-prefix variant of this pattern. - Backtesting or offline simulation: No live endpoint is involved, so environment
segregation is irrelevant. Use
demo-account-realism-gap-assessmentto judge whether the testnet record is meaningful at all. - As a substitute for a general go-live decision: This gate checks environment wiring and
configured risk limits. It does not judge whether the strategy's performance justifies live
capital — that is
paper-to-live-promotion-checklist. - As a runtime risk control: This runs once at promotion. Continuous drawdown and exposure
enforcement belongs to
kill-switch-and-drawdown-circuit-breakers.
Prerequisites
- Python 3.10+ (standard library only; this module performs no network I/O).
- Separate Binance Futures testnet and mainnet API keys, held in distinctly named environment variables. Testnet keys are issued from a separate registration flow and are not valid on mainnet — if the same value appears in both configs, one leg is wrong.
- Mainnet key with Futures trading permission enabled and, where the account allows it, an IP allowlist. Binance's API-key permission and expiry rules have changed more than once; confirm the current rules in Binance's API management docs rather than assuming.
- A testnet track record produced against a testnet host (verify this — it is the first thing the gate checks).
Workflow
-
Build both configurations: Construct
ExchangeConfigfor testnet and mainnet.MainnetPromotionManager.__init__rejects, asValueError, any config whoseenvironmentenum is wrong, any pair that shares anapi_keyorapi_secret, and any nonsensical risk ceiling (e.g.max_capital_risk_pct=2, the percent-vs-fraction slip). -
Bind each URL to its environment:
verify_api_connectivityrequires HTTPS and an exact hostname match against the allowlist for the declared environment. Both legs are checked — a TESTNET-labelled config pointing atfapi.binance.commeans the "paper" track record was produced with real orders, so it invalidates the promotion rather than merely warning. Exact matching is deliberate: astartswith/endswithcomparison acceptshttps://fapi.binance.com.attacker.example. -
Validate risk parameters, failing closed:
validate_risk_parametersrejects a missing key rather than defaulting it, rejects NaN/Inf, rejects non-integer leverage (Binance accepts integer leverage only), and requireshard_stop_loss_enabledto be the booleanTrue— not any truthy value, because a config loader yielding the string"false"is truthy. -
Require explicit authorization:
allow_live_promotiondefaults toFalse. Wire it from an operator-controlled deployment flag at the call site (allow_live_promotion=os.environ.get("BINANCE_ALLOW_MAINNET_PROMOTION") == "true"). The module deliberately does not read the environment itself, so the decision stays explicit and the gate stays deterministic under test. -
Reconcile mainnet account state before the first order — this is the part testnet cannot cover, because these are per-account, per-environment settings that do not travel with your code:
- Position mode:
GET /fapi/v1/positionSide/dual. If it disagrees with testnet, change it before opening anything —POST /fapi/v1/positionSide/dualis rejected with-4067when open orders exist and-4068when a position exists. - Multi-assets margin mode (
/fapi/v1/multiAssetsMargin) and per-symbol margin type (/fapi/v1/marginType, which returns-4046when already set to the requested value). - Leverage: set it with
POST /fapi/v1/leverageand read the response back. Check the permitted brackets viaGET /fapi/v1/leverageBracket; the leverage your testnet config assumed may exceed what this account and notional tier allow, and Binance has applied lower caps to newly opened futures accounts. - Symbol filters: re-read
GET /fapi/v1/exchangeInfoon mainnet.LOT_SIZE(stepSize,minQty),MIN_NOTIONAL,PRICE_FILTER(tickSize) and symbol availability are not guaranteed to match testnet, so quantities that were accepted on testnet can be rejected live.
- Position mode:
-
Promote: Call
promote_to_mainnet(strategy_params). Every call re-runs the full pre-flight sequence; a prior success never short-circuits a later parameter set. -
Pilot, then scale: Run minimum-notional size first and compare realized slippage, funding, and fees against the testnet assumptions before increasing allocation. See
incremental-capital-deployment-for-new-strategies.
Full step-by-step procedure with endpoint-level detail: see
references/workflows.md. Cited Binance API surface for this skill: seereferences/standards.md. Printable sign-off checklist: seeassets/checklist.md.
Common Pitfalls
- Treating any
https://URL as safe: HTTPS says nothing about which venue you reached. Bind the host to the declared environment and compare hostnames exactly. - Reusing one credential pair across both configs: If the shared value is the mainnet key, the "testnet" phase was live trading. If it is the testnet key, mainnet auth simply fails — the harmless direction, which is why the dangerous direction goes unnoticed.
- Defaulting a missing risk limit:
params.get("leverage", 0)turns a typo'd key into a pass. On a promotion gate, absent means reject. - Comparing against NaN:
float("nan") > max_leverageisFalse, so a NaN risk parameter passes a naive bounds check. Test finiteness explicitly. - Truthiness checks on safety flags: the string
"false"from an env var or YAML loader is truthy and will silently disable a stop-loss requirement. - Treating "already promoted" as idempotent: returning the mainnet config on a repeat call without re-validating lets a later, over-leveraged parameter set inherit an earlier approval.
- Assuming account settings carry over: position mode, multi-assets mode, margin type and
leverage are per-account and per-environment. Changing position mode after you already hold
a position or open order fails (
-4067/-4068), so reconcile before the first order. - Assuming testnet symbol filters match mainnet: differing
stepSize/minNotionalproduce live-2010rejections for sizes that worked on testnet. - Logging config objects: a plain dataclass
reprprintsapi_secretverbatim into logs and tracebacks.ExchangeConfighere redacts it; do the same for any config you add. - Trusting the testnet fill model: Binance testnet order books are thin and synthetic. Slippage, partial fills, and funding observed there are not evidence about mainnet.
- Retrying an ambiguous order on the first live orders: a timed-out
POST /fapi/v1/ordermay already have been accepted. Send a client-suppliednewClientOrderId(^[\.A-Z\:/a-z0-9_-]{1,36}$) and reconcile before resubmitting — seeorder-placement-idempotency.
Verification
- Run the unit suite:
python -m unittest discover -s skills/binance-futures-testnet-to-mainnet-promotion/scripts. - Point a TESTNET-labelled config at
https://fapi.binance.comand confirmrun_pre_flight_checksreturnsFalse. - Set
base_urltohttps://fapi.binance.com.attacker.exampleand confirm rejection. - Construct a manager with the same
api_keyin both configs and confirmValueError. - Omit
leveragefromstrategy_paramsand confirm rejection; repeat withcapital_risk_pct=float("nan")and withhard_stop_loss_enabled="false". - Leave
allow_live_promotionat its default and confirmpromote_to_mainnetraisesPromotionError. - Promote successfully, then call again with
leverage=50and confirmPromotionError. - Confirm
repr(config)andstr(config)contain no secret material. - Against the live account: confirm
POST /fapi/v1/leverageechoes back the leverage you requested, and thatGET /fapi/v1/exchangeInfofilters for every traded symbol match the quantities your sizing logic emits.