Trading
The trading API is all about positions. The basic lifecycle here is:
- Open a position, either:
- By opening a position directly with a market order
- Placing a limit order and waiting for the price trigger to open the position
- Query the status of the position
- If desired, update the poisition with activities such as:
- Increase position size by adding collateral
- Modifying the take profit price
- Decreasing leverage to decrease the position size
- Close the position
We'll walk through these step by step. Before getting into the API, however, it's important for you to understand the meaning of the parameters for position. You can get familiar with these from the beta web interface or by reading our documentation, specifically the slides on:
As a very terse summary for those who haven't read the above, here's an overview of the parameters needed to open a position:
- Required
- Deposit collateral: how much of the collateral asset we want to deposit. This is provided by sending the funds to the market contract (either as native coins or CW20), not an extra execute message field.
- Direction: are you trying to open a long or a short?
- Leverage: how much additional exposure are you trying to achieve?
- Take profit override: this represents the total potential profit in terms of the quote currency. This will determine how much collateral is borrowed from the liquidity pool and impact your take profit price.
- Optional
- Stop loss price
- Limit order trigger price (if placing a limit order)
- Slippage tolerance
Opening a position
Let's start off by opening a 5x leveraged long:
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"
import { GasPrice } from "@cosmjs/stargate"
// Osmosis testnet RPC endpoint
// retrieved from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L123
const rpcEndpoint = "https://rpc.osmotest5.osmosis.zone"
// Our personal wallet seed phrase
const seedPhrase =
"hen punch extra relax craft bicycle iron core purity tissue talk impact kitchen inhale slush hip amateur during ranch inspire much correct century where"
// ATOM_USD contract address on Osmosis testnet
const marketAddress =
"osmo1kus6tmx9ggmvgg5tf88ukgxcz0ynakx38hyjy0sjgahvp7d3ut2qqtfhf4"
// Testnet CW20 contract for ATOM collateral
const collateralAddress =
"osmo1g68w56vq9q0vr2mdlzp2l80j0lelnffkn7y3zhek5pavtxzznuks8wkv6k"
const runAll = async () => {
const signer = await DirectSecp256k1HdWallet.fromMnemonic(seedPhrase, {
// This is the prefix used by Osmosis testnet
prefix: "osmo"
})
const accounts = await signer.getAccounts()
const walletAddress = accounts[0].address
const client = await SigningCosmWasmClient.connectWithSigner(
rpcEndpoint,
signer,
// By specifying the cost of gas, we can let cosmjs determine the amount of
// gas funds to include with transactions
// this default is taken from https://github.com/cosmos/chain-registry/blob/aecb225b17d1cb5a3826887735cafeb7e62934d0/testnets/osmosistestnet/chain.json#L21
{ gasPrice: GasPrice.fromString("0.025uosmo") }
)
const msg = Buffer.from(
JSON.stringify({
open_position: { leverage: "5", direction: "long", take_profit: "+Inf" },
})
).toString("base64");
const execResult = await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "5000000", msg } },
"auto"
);
console.log(execResult);
}
runAll()
Since we're sending funds into this contract, we use the CW20 send method and base64-encoding of the message. We use the "+Inf"
to represent unbound take profit. This option is only available in collateral-is-base markets when opening a long. Besides "+Inf"
, we can also provide price values, regardless of the market type.
The events from the transaction above should be part of the deferred execution transaction and provide a lot of information on the actions we just took. For example, the wasm-position-open
event gives us a bunch of parameters of the open position, including the position ID:
{
"type": "wasm-position-open",
"attributes": [
{
"key": "_contract_address",
"value": "osmo1g2ml4wt93w72nmdysr99ptcvjxureqggjwrdmmx3d3fn75xuf9ts57atrx"
},
{
"key": "active-collateral",
"value": "4.936079717760488888"
},
{
"key": "contract_name",
"value": "levana.finance:market"
},
{
"key": "contract_version",
"value": "0.1.0-beta.15"
},
{
"key": "counter-collateral",
"value": "20"
},
{
"key": "counter-leverage",
"value": "1.999999999999999999"
},
{
"key": "created-at",
"value": "1679889642.398222464"
},
{
"key": "deposit-collateral",
"value": "5"
},
{
"key": "deposit-collateral-usd",
"value": "56.065"
},
{
"key": "direction",
"value": "long"
},
{
"key": "levana_protocol",
"value": "perps"
},
{
"key": "leverage",
"value": "5.051798419713133722"
},
{
"key": "market-type",
"value": "collateral-base"
},
{
"key": "notional-size",
"value": "-224.260000000000001498"
},
{
"key": "notional-size-collateral",
"value": "-19.999999999999999999"
},
{
"key": "notional-size-usd",
"value": "-224.260000000000001498"
},
{
"key": "pos-id",
"value": "99"
},
{
"key": "pos-owner",
"value": "osmo1g4unz9tzpdlaluqgyt0jjq5kyzuylg2a36086t"
},
{
"key": "trading-fee",
"value": "0.019999999999999999"
},
{
"key": "trading-fee-usd",
"value": "0.224259999999999988"
}
]
}
You can use this data directly, and you can store your position IDs to query up-to-date information on them. But let's see how we would find our open positions and query them directly.
Query the open position
To get a list of open positions for a wallet, we use the NFT interface. Each position within Levana Perps is treated as a unique NFT that can be transferred or sold on secondary markets. We won't be using that functionality for now, just listing the positions.
const res = await client.queryContractSmart(marketAddress, {
nft_proxy: { nft_msg: { tokens: { owner: walletAddress } } },
});
console.log(res);
You can provide all parameters supported by the CW721 spec, such as start_after
and limit
to enumerate through a large number of positions. In our case, we have just one position:
{ "tokens": [ '99' ] }
The position ID is internally represented as a 64-bit unsigned integer, so in order to have perfect support across systems that don't have this type, it's encoded as a string for on-the-wire messaging.
Once we have this ID, we can use it to look up the position information.
const res = await client.queryContractSmart(marketAddress, {
positions: { position_ids: ["99"] },
});
console.log(res);
position_ids
supports an array of position IDs to query, and will return a JSON structure like this:
{
"positions": [
{
"owner": "osmo1g4unz9tzpdlaluqgyt0jjq5kyzuylg2a36086t",
"id": "99",
"direction_to_base": "long",
"leverage": "4.991495369223277244",
"counter_leverage": "1.999999999999999999",
"created_at": "1695387960608855254",
"liquifunded_at": "1695387960608855254",
"trading_fee_collateral": "0.04",
"trading_fee_usd": "0.28838",
"funding_fee_collateral": "-0.001523571652950209",
"funding_fee_usd": "-0.011006054395435988",
"borrow_fee_collateral": "0.000020579249505979",
"borrow_fee_usd": "0.00014866142924192",
"crank_fee_collateral": "0",
"crank_fee_usd": "0",
"delta_neutrality_fee_collateral": "0.000535571339138809",
"delta_neutrality_fee_usd": "0.004049209964128253",
"deposit_collateral": "5",
"deposit_collateral_usd": "36.0475",
"active_collateral": "5.000699304454821595",
"active_collateral_usd": "36.12430597108736481",
"counter_collateral": "19.960268116609483826",
"pnl_collateral": "0.000699304454821595",
"pnl_usd": "0.07680597108736481",
"dnf_on_close_collateral": "0.013100845148444754",
"notional_size": "-144.190000000000000258",
"notional_size_in_collateral": "-19.960268116609483825",
"position_size_base": "24.960967421064305419",
"position_size_usd": "180.314305971087364816",
"liquidation_price_base": "5.811276064335014016",
"liquidation_margin": {
"borrow": "0.013663018160800488",
"funding": "0.02204896776840991",
"delta_neutrality": "0.124862826369046529",
"crank": "0.001387058741937721"
},
"take_profit_price_base": null,
"entry_price_base": "7.209500000000000012",
"next_liquifunding": "1695408222608855254",
"stale_at": "1695416760608855254",
"stop_loss_override": null,
"take_profit_override": "+Inf"
}
],
"pending_close": [],
"closed": []
}
The top level JSON structure includes three keys:
positions
are positions which are currently open and active.pending_close
are positions which need to be closed because of a liquidation, stop loss, or take profit. Once the crank runs, this position should move toclosed
.closed
is positions that have been fully closed. We'll see some of that output towards the end of this chapter.
For more information on the meaning of each of these fields, please see the API reference docs.
Slippage tolerance
Slippage tolerance can be used to prevent a position from opening if the price has moved significantly from the time the trader tried to open the position. Let's simulate a failure by placing a slippage parameter when trying to open a short. To cause the failure, we'll put the expected price significantly higher than the current spot price:
const msg = Buffer.from(
JSON.stringify({
open_position: {
leverage: "5",
direction: "short",
take_profit: "3.5",
slippage_assert: { price: "100", tolerance: "0.01" },
},
})
).toString("base64");
const execResult = await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "5000000", msg } },
"auto"
);
This program produces the output:
Error: Query failed with (6): rpc error: code = Unknown desc = failed to execute message; message index: 0: dispatch: submessages: {
"id": "slippage_assert",
"domain": "market",
"description": "Slippage is exceeding provided tolerance. Slippage is 791.99920844611191%, max tolerance is 1%. Current price: 11.209. Asserted price: 100.",
"data": null
}: execute wasm contract failed [CosmWasm/[email protected]/x/wasm/keeper/keeper.go:425] With gas wanted: '0' and gas used: '225810' : unknown request
Our message asserted that the current price is 100 USD per ATOM, while the smart contract views the current price (at time of running) as 11.209. By stating 0.01
for the tolerance parameter, we're saying that the maximum the price can deviate against the trader is 1%. Therefore, this open position is rejected.
Stop losses
When you open a position, you can set a stop loss on the position. Let's actually open a short this time, and place a stop loss at a price above the current entry price but below the liquidation price:
const msg = Buffer.from(
JSON.stringify({
open_position: {
leverage: "5",
direction: "short",
take_profit: "2.5",
slippage_assert: { price: "11.209", tolerance: "0.01" },
stop_loss_override: "11.5",
},
})
).toString("base64");
const execResult = await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "5000000", msg } },
"auto"
);
console.log(JSON.stringify(execResult));
Looking at the event output (which we won't show here for brevity), the position ID is "245155". We can query the open position to see the liquidation and stop loss prices:
const res = await client.queryContractSmart(marketAddress, {
positions: { position_ids: ["245155"] },
});
const pos = res.positions[0];
console.log(`Unrealized PnL: ${pos.pnl_usd}`);
console.log(`Liquidation price: ${pos.liquidation_price_base}`);
console.log(`Stop loss: ${pos.stop_loss_override}`);
console.log(`Take profit: ${pos.take_profit_override}`);
This outputs:
Unrealized PnL: -0.276837137536798332
Liquidation price: 13.055139080302147435
Stop loss: 11.5
Our position starts off with negative PnL since we had to pay fees from our collateral to open the position. Our stop loss price is exactly the 11.5 value we specified above, and our liquidation price is calculated to be about 13.06. We can modify our stop loss price if we want:
const msg = Buffer.from(
JSON.stringify({
update_position_stop_loss_price: {
id: "4854",
stop_loss: "remove",
},
})
).toString("base64");
await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "5000000", msg } },
"auto"
);
With that change in place, we now see our updated stop loss price:
Unrealized PnL: -0.199828868790545317
Liquidation price: 13.037564011474705624
Stop loss: null
Placing a limit order
When you open an order using the open_position
API, you create a market order that uses the current spot price. You can instead place a limit order. With limit orders, you're guaranteed to get the limit price you request or better. It's also possible that the order will be cancelled if, at the time of opening, there are any issues preventing the order from being placed, such as insufficient liquidity in the liquidity pool.
The parameters for placing a limit order look very similar to placing a market order, with two differences:
- Limit orders require a
trigger_price
parameter - Limit orders do not support a
slippage_assert
parameter, since you specify the entry price yourself
Let's place a short limit order by setting a trigger price above the current spot price:
const msg = Buffer.from(
JSON.stringify({
place_limit_order: {
leverage: "5",
direction: "short",
take_profit: "2.5",
trigger_price: "11.159",
},
})
).toString("base64");
const execResult = await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "5000000", msg } },
"auto"
);
console.log(JSON.stringify(execResult));
Instead of receiving a position ID, we receive an order ID in the events. The wasm-place-limit-order
event from the above execution gave me:
{
"type": "wasm-place-limit-order",
"attributes": [
{
"key": "_contract_address",
"value": "osmo1g2ml4wt93w72nmdysr99ptcvjxureqggjwrdmmx3d3fn75xuf9ts57atrx"
},
{
"key": "contract_name",
"value": "levana.finance:market"
},
{
"key": "contract_version",
"value": "0.1.0-beta.15"
},
{
"key": "deposit-collateral",
"value": "4.999103621369666548"
},
{
"key": "deposit-collateral-usd",
"value": "55.770000000000000009"
},
{
"key": "direction",
"value": "short"
},
{
"key": "levana_protocol",
"value": "perps"
},
{
"key": "leverage-to-base",
"value": "-5"
},
{
"key": "market-type",
"value": "collateral-base"
},
{
"key": "order-id",
"value": "187"
},
{
"key": "pos-owner",
"value": "osmo1g4unz9tzpdlaluqgyt0jjq5kyzuylg2a36086t"
},
{
"key": "trigger-price",
"value": "11.159"
}
]
}
We can use the order-id
of 187 above to check the status of our limit order:
const res = await client.queryContractSmart(marketAddress, {
limit_order: { order_id: 187 },
});
console.log(JSON.stringify(res));
Resulting in:
{
"order_id": 187,
"trigger_price": "11.159",
"collateral": "4.999103621369666548",
"leverage": "5",
"direction": "short",
"stop_loss_override": null,
"take_profit_override": null
}
And if desired we can cancel our limit order:
await client.execute(
walletAddress,
marketAddress,
{ cancel_limit_order: { order_id: 187 } },
"auto"
);
Add collateral
Let's get back to our existing open positions. Levana Perps provides a lot of flexibility to update existing positions. The first update mechanism we'll look at is adding collateral. When adding collateral, you get to decide whether the position should be updated by:
- Increasing the position size, exposing you further to price movements, while keeping the leverage the same
- Decrease the leverage, reducing your risk of liquidation, but not giving you more price exposure
We can see the former with the code:
const msg = Buffer.from(
JSON.stringify({
update_position_add_collateral_impact_size: {
id: "99",
},
})
).toString("base64");
await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "1000000", msg } },
"auto"
);
And the latter with:
const msg = Buffer.from(
JSON.stringify({
update_position_add_collateral_impact_leverage: {
id: "99",
},
})
).toString("base64");
const execResult = await client.execute(
walletAddress,
collateralAddress,
{ send: { contract: marketAddress, amount: "1000000", msg } },
"auto"
);
Remove collateral
You can also choose to remove collateral, with the same decision to either decrease position size or increase leverage. This can be useful if you've experienced price movement in your favor and would like to take some profits.
Decreasing size:
await client.execute(
walletAddress,
marketAddress,
{
update_position_remove_collateral_impact_size: {
id: "99",
amount: "0.75",
},
},
"auto"
);
Increasing leverage:
const execResult = await client.execute(
walletAddress,
marketAddress,
{
update_position_remove_collateral_impact_leverage: {
id: "99",
amount: "0.75",
},
},
"auto"
);
Note that when passing in amount
here, we use the decimal-encoded version of the value. 0.75
means 0.75 ATOM, or 750000uatom
.
Change leverage and modify the take profit price
For our final set of updates, we can change leverage without adding or removing collateral. Updating the leverage will modify the position size (higher leverage means larger position), while modifying the take profit price will impact your take profit price and borrow fees.
await client.execute(
walletAddress,
marketAddress,
{
update_position_leverage: {
id: "99",
leverage: "15",
},
},
"auto"
);
await client.execute(
walletAddress,
marketAddress,
{
update_position_take_profit_price: {
id: "99",
price: "2.3",
},
},
"auto"
);
Closing a position
And finally, when we're done with a position, we can close it using the close_position
message:
await client.execute(
walletAddress,
marketAddress,
{ close_position: { id: "99" } },
"auto"
);
Once a position is closed, we can still query it:
const res = await client.queryContractSmart(marketAddress, {
positions: { position_ids: ["99"] },
});
console.log(JSON.stringify(res));
But now that our position is closed, it will appear in the closed
field instead of the positions
field:
{
"positions": [],
"pending_close": [],
"closed": [
{
"owner": "osmo1g4unz9tzpdlaluqgyt0jjq5kyzuylg2a36086t",
"id": "99",
"direction_to_base": "long",
"created_at": "1679889642398222464",
"liquifunded_at": "1679898724933076589",
"trading_fee_collateral": "0.076983773508679359",
"trading_fee_usd": "0.859852989798127206",
"funding_fee_collateral": "0.000039121444204281",
"funding_fee_usd": "0.000435428475475964",
"borrow_fee_collateral": "0.000810696199482583",
"borrow_fee_usd": "0.009022695213614841",
"crank_fee_collateral": "0",
"crank_fee_usd": "0",
"delta_neutrality_fee_collateral": "0.037906133680913301",
"delta_neutrality_fee_usd": "0.425282703238582748",
"deposit_collateral": "5.5",
"active_collateral": "5.283818160624093971",
"pnl_collateral": "-0.216181839375906029",
"pnl_usd": "-2.665508418238231754",
"notional_size": "-829.985329366266828543",
"entry_price_base": "11.213",
"close_time": "1679898724933076589",
"settlement_time": "1679898724933076589",
"reason": "direct"
}
]
}
Again, you can get more information on the meaning of each of these fields by looking at the message documentation.
With all that covered, you should now be set up to begin programmatic trading with Levana Perps!