PancakeSwap Perps¶
Connector for PancakeSwap Perps (ApolloX Diamond) on BSC. PancakeSwap is broker id 2 on the underlying ApolloX (ASX) perpetual trading platform.
Overview¶
PancakeSwap Perps is an oracle-priced margin trading venue. The Almanak SDK
integrates it through the intent system, supporting PERP_OPEN (full pipeline)
and PERP_CLOSE (direct-SDK in v1).
The router is a Diamond proxy (EIP-2535) at
0x1b6f2d3844c6ae7d56ceb3c3643b9060ba28feb0. Trade-path facets: TradingPortal
(open/close), TradingOpen / TradingClose (keeper settle), TradingReader
(views), PriceFacade (keeper entry).
Market Format¶
Markets use the same slash separator convention as GMX V2: "BTC/USD",
"ETH/USD", "BNB/USD". Each market resolves to a pairBase BSC ERC-20
address (e.g. BTCB, ETH-bsc, WBNB).
Intent.perp_open(
market="BTC/USD",
collateral_token="BNB", # native BNB margin (auto-wraps to WBNB)
collateral_amount=Decimal("0.3"),
size_usd=Decimal("500"),
is_long=True,
leverage=Decimal("1.5"),
max_slippage=Decimal("0.01"),
protocol="pancakeswap_perps",
)
Supported Operations¶
| Intent | Description |
|---|---|
Intent.perp_open() |
Open a leveraged long or short position |
Intent.perp_close(position_id=<tradeHash>, ...) |
Close a position by its bytes32 tradeHash. v1 accepts full closes only: size_usd (partial close) is rejected at compile time (CompilationStatus.FAILED). Strategies persist the tradeHash emitted at open and pass it back as position_id. build_close_transaction(trade_hash) remains available as a direct-SDK escape hatch, but the intent-compiler path is the recommended flow. |
Keeper Execution Model¶
PancakeSwap Perps uses a two-step oracle-fill execution model (similar in shape to GMX V2 but driven by Pyth on mainnet):
- Pending trade creation — your transaction calls
openMarketTrade/openMarketTradeBNB, which emits aMarketPendingTrade(user, tradeHash, trade)event. This is the transaction the SDK signs and submits. - Keeper settlement — an off-chain keeper holding
PRICE_FEEDER_ROLEsubsequently callsPriceFacadeFacet.requestPriceCallback(priceRequestId, price)(or its Pyth-VAA variant). This invokesmarketTradeCallbackinternally, which either fills the position (OpenMarketTradeevent) or refunds it (PendingTradeRefundevent) based on slippage / oracle gap checks.
Close follows the same shape: closeTrade(tradeHash) emits a pending close
request; keeper settles via closeTradeCallback, emitting
CloseTradeSuccessful(user, tradeHash, closeInfo) and one or more
CloseTradeReceived(user, tradeHash, token, amount) payout events.
Implications for strategies:
on_intent_executed(success=True)fires when the pending TX confirms, not when the keeper settles. The strategy must persist thetradeHashand poll for fill confirmation on subsequent ticks viagetPositionByHashV2(tradeHash).Extraction shape:
ResultEnricher.position_idonly accepts integer NFT IDs or 40-char hex addresses, so a 64-hex-char bytes32 tradeHash is surfaced onresult.extracted_data["position_id"]rather thanresult.position_id. Strategies should preferresult.position_idand fall back toresult.extracted_data["position_id"]:- Filled
entry_priceis only available after keeper settlement (a separate TX), soextract_entry_price()returnsNonefrom the open TX's receipt. - The slippage-to-limit-price conversion must produce a fill bound that the
keeper considers acceptable AND is within
highPriceGapP(1.5% as of v1) of the on-chain oracle's cachedbeforePrice.
Minimum Position Size¶
ApolloX enforces TradingConfig.minNotionalUsd (200 USD as of v1) on every
open. Notional is computed as price (1e8) × qty (1e10) ÷ 1e18. Strategies
opening positions below this floor will revert at the synchronous open call
with TradingCheckerFacet: Position is too small.
Collateral Tokens¶
| Chain | Supported Margin |
|---|---|
| BSC | BNB (native, via openMarketTradeBNB), WBNB, USDT, USDC |
For native BNB margin, the SDK routes through openMarketTradeBNB with the
margin sent as msg.value; the router wraps it to WBNB inside the
transaction (verifiable in the receipt's WBNB Deposit event). For ERC-20
margin, the intent compiler prepends an approve() transaction.
Markets (v1)¶
| Market | pairBase (BSC) |
|---|---|
| BTC/USD | 0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c (BTCB) |
| ETH/USD | 0x2170Ed0880ac9A755fd29B2688956BD959F933F8 (ETH-bsc) |
| BNB/USD | 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c (WBNB) |
ApolloX also lists synthetic equity markets (NVDA, TSLA, etc.) with ApolloX-issued pairBase contracts. These are out of v1 scope pending gateway-side equity-oracle support; see the design doc follow-ups.
Known Limitations¶
- Keeper delay: Position state is not immediately available after the open
call. Wait for the keeper-settled
OpenMarketTradeevent before relying on filled position data. - Refunds: If the keeper-quoted oracle price violates the trader's
acceptable price (slippage limit), the keeper emits
PendingTradeRefundinstead ofOpenMarketTrade, and the trader's margin is returned. Your strategy must handle this case (pollgetPendingTradegoing to zero without a correspondinggetPositionByHashV2populated). - No SL/TP in v1: The intent vocabulary doesn't yet carry stop-loss / take-profit. The contract supports them; the integration is deferred to a cross-venue (PCS Perps + GMX V2 + Hyperliquid) vocabulary RFC.
- No Arbitrum: ApolloX has separate deployments per chain. v1 ships BSC only.
- Full-close only via intents:
PerpCloseIntent(position_id=<tradeHash>)compiles tocloseTrade(bytes32). Partial closes (size_usdset) are rejected at compile time — ApolloX'scloseTrade(bytes32)selector always flattens the whole position. Strategies persist thetradeHashemitted at open and pass it back asposition_id.build_close_transaction(trade_hash)remains available for manual transaction construction.
API Reference¶
almanak.framework.connectors.pancakeswap_perps
¶
Backwards-compatibility shim for pancakeswap_perps.
PancakeSwap Perps is powered by Aster (formerly ApolloX, rebranded March 2025).
The canonical connector moved to almanak.framework.connectors.aster_perps
(PRD: docs/internal/discussions/aster-dex-integration-20260418.md · VIB-3044).
This shim re-exports every previously-public symbol. Legacy consumers using
from almanak.framework.connectors.pancakeswap_perps import ... continue to
work unchanged. The shim binds broker_id=2 (PancakeSwap attribution) in
every config it constructs, which preserves pre-rebrand behaviour byte-for-byte.
Deprecation: importing from this module emits DeprecationWarning once per
process. New strategies must import from aster_perps directly and set
protocol="aster_perps" on intents.
SELECTOR_CLOSE_TRADE
module-attribute
¶
closeTrade(bytes32)
SELECTOR_OPEN_MARKET_TRADE
module-attribute
¶
openMarketTrade((address,bool,address,uint96,uint80,uint64,uint64,uint64,uint24))
SELECTOR_OPEN_MARKET_TRADE_BNB
module-attribute
¶
openMarketTradeBNB((address,bool,address,uint96,uint80,uint64,uint64,uint64,uint24))
CloseTradeReceivedEvent
dataclass
¶
TradingCloseFacet.CloseTradeReceived — payout leg of a close.
CloseTradeSuccessfulEvent
dataclass
¶
CloseTradeSuccessfulEvent(
user: str,
trade_hash: str,
close_price: int,
funding_fee: int,
close_fee: int,
pnl: int,
holding_fee: int,
log_index: int = 0,
)
TradingCloseFacet.CloseTradeSuccessful — fires when keeper fills a close.
MarketPendingTradeEvent
dataclass
¶
MarketPendingTradeEvent(
user: str,
trade_hash: str,
pair_base: str,
is_long: bool,
token_in: str,
amount_in: int,
qty: int,
price: int,
stop_loss: int,
take_profit: int,
broker: int,
log_index: int = 0,
)
TradingPortalFacet.MarketPendingTrade — fires on user-signed openMarketTrade.
OpenMarketTradeEvent
dataclass
¶
OpenMarketTradeEvent(
user: str,
trade_hash: str,
entry_price: int,
pair_base: str,
token_in: str,
margin: int,
qty: int,
is_long: bool,
open_fee: int,
execution_fee: int,
timestamp: int,
log_index: int = 0,
)
TradingOpenFacet.OpenMarketTrade — fires when keeper fills a pending open.
OpenTradeStruct
dataclass
¶
OpenTradeStruct(
pair_base: str,
is_long: bool,
token_in: str,
amount_in: int,
qty: int,
price: int,
broker: int,
stop_loss: int = 0,
take_profit: int = 0,
)
Python mirror of the Aster openMarketTrade input struct.
All integer fields use the on-wire units the contract expects
- amountIn: collateral-token smallest units (wei-equivalent)
- qty: 10-decimal fixed-point (e.g. 0.15 BTC = 1500000000)
- price: 8-decimal fixed-point limit / acceptable price
- stopLoss: 8-decimal fixed-point (0 = no SL)
- takeProfit: 8-decimal fixed-point (0 = no TP)
- broker: uint24 broker id (PancakeSwap = 2, raw Aster = 0)
ParsedReceipt
dataclass
¶
ParsedReceipt(
market_pending_trades: list[
MarketPendingTradeEvent
] = list(),
open_market_trades: list[OpenMarketTradeEvent] = list(),
pending_trade_refunds: list[
PendingTradeRefundEvent
] = list(),
close_trade_successful: list[
CloseTradeSuccessfulEvent
] = list(),
close_trade_received: list[
CloseTradeReceivedEvent
] = list(),
)
Aggregate view of decoded Aster Perps events in a receipt.
PendingTradeRefundEvent
dataclass
¶
TradingOpenFacet.PendingTradeRefund — fires when keeper refunds a pending trade.
PerpOpenOrderResult
dataclass
¶
PerpOpenOrderResult(
success: bool,
error: str | None = None,
tx: AsterPerpsTx | None = None,
pair_base: str | None = None,
margin_token_address: str | None = None,
qty: int = 0,
limit_price: int = 0,
native: bool = False,
amount_in_wei: int = 0,
)
Adapter output for a compiled PERP_OPEN.
属性:
| 名称 | 类型 | 描述 |
|---|---|---|
success |
bool
|
False if validation fails before tx construction. |
error |
str | None
|
description when success is False. |
tx |
AsterPerpsTx | None
|
the built transaction. |
pair_base |
str | None
|
resolved pairBase address for the market. |
margin_token_address |
str | None
|
resolved margin-token address (or NATIVE sentinel). |
qty |
int
|
computed on-wire qty (uint80, 10-decimal). |
limit_price |
int
|
computed acceptable limit price (uint64, 8-decimal). |
native |
bool
|
True if the transaction uses openMarketTradeBNB (value-carrying). |
amount_in_wei |
int
|
margin amount in token-wei (matches OpenTradeStruct.amountIn). |
encode_close_trade_calldata
¶
Encode calldata for closeTrade(bytes32).
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
trade_hash
|
str | bytes
|
32-byte position identifier (hex string or raw bytes). |
必需 |
返回:
| 类型 | 描述 |
|---|---|
bytes
|
4-byte selector + abi-encoded bytes32. |
encode_get_pending_trade_calldata
¶
Encode calldata for the getPendingTrade(bytes32) view.
encode_get_position_by_hash_calldata
¶
Encode calldata for the getPositionByHashV2(bytes32) view.
encode_open_market_trade_calldata
¶
Encode calldata for openMarketTrade or openMarketTradeBNB.
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
trade
|
OpenTradeStruct
|
populated OpenTradeStruct |
必需 |
native
|
bool
|
when True uses openMarketTradeBNB (native BNB margin via msg.value), when False uses openMarketTrade (ERC20 margin, requires prior approve). |
False
|
返回:
| 类型 | 描述 |
|---|---|
bytes
|
4-byte selector + abi-encoded tuple. |
get_margin_token_address
¶
Resolve a margin-token symbol (WBNB/USDT/USDC) to its BSC ERC-20 address.
Accepts either a symbol or a 0x-prefixed EVM address (passed through after validation). For native BNB, pass symbol='BNB' or 'NATIVE' and the NATIVE_BNB_ADDRESS sentinel is returned (the caller must use encode_open_market_trade_calldata(native=True)).
Symbol resolution goes through the framework's unified
:func:almanak.framework.data.tokens.get_token_resolver so the connector
stays in sync with the rest of the token-metadata surface (aliases,
on-chain fallbacks, disk cache). The local
:data:ASTER_PERPS_TOKENS allowlist is only consulted to reject
tokens that the Aster router does not accept as margin; the address
for allowed symbols comes from the resolver.
参数:
| 名称 | 类型 | 描述 | 默认 |
|---|---|---|---|
symbol
|
str
|
Margin token symbol (e.g. 'WBNB', 'USDT', 'USDC', 'BNB', 'NATIVE') or a 0x-prefixed EVM address. |
必需 |
chain
|
str
|
Chain key (default 'bsc'). |
'bsc'
|
token_resolver
|
Optional |
None
|
引发:
| 类型 | 描述 |
|---|---|
ValueError
|
If the symbol is not one of Aster Perps' supported margin tokens. |
TokenNotFoundError
|
If the resolver cannot resolve an allowed symbol to
an on-chain address on |
get_pair_base
¶
Resolve a market symbol (e.g. 'BTC/USD') to the on-chain pairBase address.
Accepts either a registered symbol (v1: BTC/USD, ETH/USD, BNB/USD) or a 0x-prefixed
EVM address (passed through after validation — lets PerpOpenIntent.market carry
the pairBase address directly for synthetic / non-registered markets).
Raises ValueError if the symbol is not registered and is not a valid address. Non-crypto markets (NVDA, TSLA, ...) use synthetic ApolloX-issued pairBases and are deferred to v2 per the design doc; callers wanting to hit them today must pass the pairBase address explicitly.
get_router_address
¶
Return the Aster Perps router address for the given chain.
slippage_to_limit_price
¶
Convert (mark_price, slippage_fraction) into the price field the router expects.
For longs: acceptable price = mark_price * (1 + slippage) — trader willing to pay up to this. For shorts: acceptable price = mark_price * (1 - slippage) — trader willing to receive down to this.
Returned value is uint64 with 8-decimal scaling.
usd_size_to_qty
¶
Convert a USD notional to on-wire qty (uint80, 10-decimal fixed-point).
qty = size_usd / mark_price, then scale to 1e10 (see QTY_DECIMALS).
Raises ValueError on non-positive inputs (keeps the connector fail-fast — silently coercing zero/negative sizes into bogus positions is exactly the class of bug the 'no quick patches' guardrail forbids).
PancakeSwapPerpsConfig
¶
PancakeSwapPerpsConfig(
chain: str = "bsc",
wallet_address: str | None = None,
broker_id: int = PANCAKESWAP_PERPS_BROKER_ID,
) -> AsterPerpsConfig
Backwards-compat factory mimicking the old dataclass signature.
The legacy PancakeSwapPerpsConfig had broker_id defaulted to 2. The
canonical AsterPerpsConfig requires broker_id with no default so
raw-aster callers are forced to pick 0 explicitly. This factory preserves
the old default (=2) for every call that comes through the shim.
build_open_transaction
¶
build_open_transaction(
*,
chain: str = "bsc",
wallet_address: str | None = None,
broker_id: int = PANCAKESWAP_PERPS_BROKER_ID,
**open_kwargs,
) -> PerpOpenOrderResult
Legacy build_open_transaction signature: defaults broker_id=2 (PCS).
build_close_transaction
¶
build_close_transaction(
*,
trade_hash,
chain: str = "bsc",
wallet_address: str | None = None,
broker_id: int = PANCAKESWAP_PERPS_BROKER_ID,
) -> AsterPerpsTx
Legacy build_close_transaction signature: defaults broker_id=2 (PCS).