diff --git a/ts/client/scripts/liqtest/liqtest-make-candidates.ts b/ts/client/scripts/liqtest/liqtest-make-candidates.ts index 4efa8d3be..b64220520 100644 --- a/ts/client/scripts/liqtest/liqtest-make-candidates.ts +++ b/ts/client/scripts/liqtest/liqtest-make-candidates.ts @@ -1,6 +1,5 @@ import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; -import { assert } from 'console'; import fs from 'fs'; import { Bank } from '../../src/accounts/bank'; import { MangoAccount } from '../../src/accounts/mangoAccount'; diff --git a/ts/client/scripts/liqtest/liqtest-make-tcs-candidates.ts b/ts/client/scripts/liqtest/liqtest-make-tcs-candidates.ts index 987965eff..66f54d9ee 100644 --- a/ts/client/scripts/liqtest/liqtest-make-tcs-candidates.ts +++ b/ts/client/scripts/liqtest/liqtest-make-tcs-candidates.ts @@ -1,19 +1,9 @@ import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js'; -import { assert } from 'console'; import fs from 'fs'; import { Bank } from '../../src/accounts/bank'; import { MangoAccount } from '../../src/accounts/mangoAccount'; -import { - PerpMarket, - PerpOrderSide, - PerpOrderType, -} from '../../src/accounts/perp'; -import { - Serum3OrderType, - Serum3SelfTradeBehavior, - Serum3Side, -} from '../../src/accounts/serum3'; +import { PerpMarket } from '../../src/accounts/perp'; import { Builder } from '../../src/builder'; import { MangoClient } from '../../src/client'; import { @@ -181,7 +171,7 @@ async function main() { accounts2.find((account) => account.name == 'LIQTEST, LIQEE1'), ); await client.accountExpandV2(group, account, 4, 4, 4, 4, 4); - await client.tokenConditionalSwapCreate( + await client.tokenConditionalSwapCreateOld( group, account, MINTS.get('SOL')!, @@ -203,7 +193,7 @@ async function main() { accounts2.find((account) => account.name == 'LIQTEST, LIQEE2'), ); await client.accountExpandV2(group, account, 4, 4, 4, 4, 4); - await client.tokenConditionalSwapCreate( + await client.tokenConditionalSwapCreateOld( group, account, MINTS.get('SOL')!, @@ -225,7 +215,7 @@ async function main() { accounts2.find((account) => account.name == 'LIQTEST, LIQEE3'), ); await client.accountExpandV2(group, account, 4, 4, 4, 4, 4); - await client.tokenConditionalSwapCreate( + await client.tokenConditionalSwapCreateOld( group, account, MINTS.get('SOL')!, diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index bd32f21a7..7f71ec370 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -11,6 +11,7 @@ import { toNativeI80F48, toUiDecimals, toUiDecimalsForQuote, + toUiSellPerBuyTokenPrice, } from '../utils'; import { Bank, TokenIndex } from './bank'; import { Group } from './group'; @@ -1845,9 +1846,10 @@ export class TokenConditionalSwap { ): number { const buyBank = this.getBuyToken(group); const sellBank = this.getSellToken(group); - const sellTokenPerBuyTokenUi = toUiDecimals( + const sellTokenPerBuyTokenUi = toUiSellPerBuyTokenPrice( sellTokenPerBuyTokenNative, - sellBank.mintDecimals - buyBank.mintDecimals, + sellBank, + buyBank, ); // Below are workarounds to know when to show an inverted price in ui diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 53c5cb08e..885985ae1 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -66,12 +66,7 @@ import { TokenEditParams, buildIxGate, } from './clientIxParamBuilder'; -import { - MANGO_V4_ID, - OPENBOOK_PROGRAM_ID, - RUST_U64_MAX, - USDC_MINT, -} from './constants'; +import { MANGO_V4_ID, OPENBOOK_PROGRAM_ID, RUST_U64_MAX } from './constants'; import { Id } from './ids'; import { IDL, MangoV4 } from './mango_v4'; import { I80F48 } from './numbers/I80F48'; @@ -82,10 +77,13 @@ import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddress, toNative, + toNativeSellPerBuyTokenPrice, } from './utils'; import { sendTransaction } from './utils/rpc'; import { NATIVE_MINT, TOKEN_PROGRAM_ID } from './utils/spl'; +export const DEFAULT_TOKEN_CONDITIONAL_SWAP_COUNT = 8; + export enum AccountRetriever { Scanning, Fixed, @@ -3374,64 +3372,230 @@ export class MangoClient { return await this.sendAndConfirmTransactionForGroup(group, [ix]); } - /** - * Example: - * For a stop loss on SOL, assuming SOL/USDC pair - * priceLowerLimit - e.g. - * - * @param group - * @param account - * @param sellMintPk - would be SOL mint - * @param priceLowerLimit - priceLowerLimit would be greater than priceUpperLimit e.g. if SOL is at 25$, then priceLowerLimit could be 22$ - * @param buyMintPk - would be USDC mint - * @param priceUpperLimit - priceLowerLimit would be greater than priceUpperLimit e.g. if SOL is at 25$, then priceUpperLimit could be 0$ - * @param maxSell - max ui amount of tokens to sell, e.g. account.getTokenBalanceUi(solBank) - * @param pricePremiumFraction - premium in % the liquidator earns for executing this stop loss, this can be the slippage usually found for a particular size plus some buffer - * @param expiryTimestamp - is epoch in seconds at which this stop loss should expire, set null if you want it to never expire - * @returns - */ - public async tokenConditionalSwapStopLoss( + public async tcsTakeProfitOnDeposit( group: Group, account: MangoAccount, - sellMintPk: PublicKey, - priceLowerLimit: number, - buyMintPk: PublicKey | null, - priceUpperLimit: number | null, - maxSell: number | null, - pricePremiumFraction: number | null, + sellBank: Bank, + buyBank: Bank, + thresholdPriceUi: number, + thresholdPriceInSellPerBuyToken: boolean, + maxSellUi: number | null, + pricePremium: number | null, expiryTimestamp: number | null, ): Promise { - const buyBank: Bank = group.getFirstBankByMint(buyMintPk ?? USDC_MINT); - const sellBank: Bank = group.getFirstBankByMint(sellMintPk); + if (account.getTokenBalanceUi(sellBank) < 0) { + throw new Error( + `Only allowed to take profits on deposits! Current balance ${account.getTokenBalanceUi( + sellBank, + )}`, + ); + } - priceUpperLimit = priceUpperLimit ?? 0; - maxSell = maxSell ?? account.getTokenBalanceUi(sellBank); - pricePremiumFraction = group.getPriceImpactByTokenIndex( - sellBank.tokenIndex, - maxSell * sellBank.uiPrice, + return await this.tokenConditionalSwapCreate( + group, + account, + sellBank, + buyBank, + thresholdPriceUi, + thresholdPriceInSellPerBuyToken, + Number.MAX_SAFE_INTEGER, + maxSellUi ?? account.getTokenBalanceUi(sellBank), + 'TakeProfitOnDeposit', + pricePremium, + true, + false, + expiryTimestamp, ); - pricePremiumFraction = - pricePremiumFraction > 0 ? pricePremiumFraction : 0.3; + } + + public async tcsStopLossOnDeposit( + group: Group, + account: MangoAccount, + sellBank: Bank, + buyBank: Bank, + thresholdPriceUi: number, + thresholdPriceInSellPerBuyToken: boolean, + maxSellUi: number | null, + pricePremium: number | null, + expiryTimestamp: number | null, + ): Promise { + if (account.getTokenBalanceUi(sellBank) < 0) { + throw new Error( + `Only allowed to set a stop loss on deposits! Current balance ${account.getTokenBalanceUi( + sellBank, + )}`, + ); + } + + return await this.tokenConditionalSwapCreate( + group, + account, + sellBank, + buyBank, + thresholdPriceUi, + thresholdPriceInSellPerBuyToken, + Number.MAX_SAFE_INTEGER, + maxSellUi ?? account.getTokenBalanceUi(sellBank), + 'StopLossOnDeposit', + pricePremium, + true, + false, + expiryTimestamp, + ); + } + + public async tcsTakeProfitOnBorrow( + group: Group, + account: MangoAccount, + sellBank: Bank, + buyBank: Bank, + thresholdPriceUi: number, + thresholdPriceInSellPerBuyToken: boolean, + maxBuyUi: number | null, + pricePremium: number | null, + allowMargin: boolean | null, + expiryTimestamp: number | null, + ): Promise { + if (account.getTokenBalanceUi(buyBank) > 0) { + throw new Error( + `Only allowed to take profits on borrows! Current balance ${account.getTokenBalanceUi( + buyBank, + )}`, + ); + } + + return await this.tokenConditionalSwapCreate( + group, + account, + sellBank, + buyBank, + thresholdPriceUi, + thresholdPriceInSellPerBuyToken, + maxBuyUi ?? -account.getTokenBalanceUi(buyBank), + Number.MAX_SAFE_INTEGER, + 'TakeProfitOnBorrow', + pricePremium, + false, + allowMargin ?? false, + expiryTimestamp, + ); + } + + public async tcsStopLossOnBorrow( + group: Group, + account: MangoAccount, + sellBank: Bank, + buyBank: Bank, + thresholdPriceUi: number, + thresholdPriceInSellPerBuyToken: boolean, + maxBuyUi: number | null, + pricePremium: number | null, + allowMargin: boolean | null, + expiryTimestamp: number | null, + ): Promise { + if (account.getTokenBalanceUi(buyBank) > 0) { + throw new Error( + `Only allowed to set stop loss on borrows! Current balance ${account.getTokenBalanceUi( + buyBank, + )}`, + ); + } + + return await this.tokenConditionalSwapCreate( + group, + account, + sellBank, + buyBank, + thresholdPriceUi, + thresholdPriceInSellPerBuyToken, + maxBuyUi ?? -account.getTokenBalanceUi(buyBank), + Number.MAX_SAFE_INTEGER, + 'StopLossOnBorrow', + pricePremium, + false, + allowMargin ?? false, + expiryTimestamp, + ); + } + + public async tokenConditionalSwapCreate( + group: Group, + account: MangoAccount, + sellBank: Bank, + buyBank: Bank, + thresholdPriceUi: number, + thresholdPriceInSellPerBuyToken: boolean, + maxBuyUi: number, + maxSellUi: number, + tcsIntention: + | 'TakeProfitOnDeposit' + | 'StopLossOnDeposit' + | 'TakeProfitOnBorrow' + | 'StopLossOnBorrow' + | null, + pricePremium: number | null, + allowCreatingDeposits: boolean, + allowCreatingBorrows: boolean, + expiryTimestamp: number | null, + ): Promise { + const maxBuy = toNative(maxBuyUi, buyBank.mintDecimals); + const maxSell = toNative(maxSellUi, sellBank.mintDecimals); + + if (!thresholdPriceInSellPerBuyToken) { + thresholdPriceUi = 1 / thresholdPriceUi; + } + + let lowerLimit, upperLimit; + const thresholdPrice = toNativeSellPerBuyTokenPrice( + thresholdPriceUi, + sellBank, + buyBank, + ); + const sellTokenPerBuyTokenPrice = buyBank.price + .div(sellBank.price) + .toNumber(); + + if ( + tcsIntention == 'TakeProfitOnDeposit' || + tcsIntention == 'StopLossOnBorrow' || + (tcsIntention == null && thresholdPrice > sellTokenPerBuyTokenPrice) + ) { + lowerLimit = thresholdPrice; + upperLimit = Number.MAX_SAFE_INTEGER; + } else { + lowerLimit = 0; + upperLimit = thresholdPrice; + } + const expiryTimestampBn = expiryTimestamp !== null ? new BN(expiryTimestamp) : U64_MAX_BN; + if (!pricePremium) { + const buyTokenPriceImpact = group.getPriceImpactByTokenIndex( + buyBank.tokenIndex, + 5000, + ); + const sellTokenPriceImpact = group.getPriceImpactByTokenIndex( + sellBank.tokenIndex, + 5000, + ); + pricePremium = + ((1 + buyTokenPriceImpact / 100) * (1 + sellTokenPriceImpact / 100) - + 1) * + 100; + } + const pricePremiumFraction = pricePremium > 0 ? pricePremium / 100 : 0.03; + const tcsIx = await this.program.methods .tokenConditionalSwapCreate( - U64_MAX_BN, - toNative(maxSell, sellBank.mintDecimals), + maxBuy, + maxSell, expiryTimestampBn, - (1 / priceLowerLimit) * - Math.pow(10, sellBank.mintDecimals - buyBank.mintDecimals), - (1 / priceUpperLimit) * - Math.pow(10, sellBank.mintDecimals - buyBank.mintDecimals), - pricePremiumFraction != null - ? pricePremiumFraction / 100 - : group.getPriceImpactByTokenIndex( - sellBank.tokenIndex, - maxSell * sellBank.uiPrice, - ), - true, - false, + lowerLimit, + upperLimit, + pricePremiumFraction, + allowCreatingDeposits, + allowCreatingBorrows, ) .accounts({ group: group.publicKey, @@ -3452,7 +3616,7 @@ export class MangoClient { account.serum3.length, account.perps.length, account.perpOpenOrders.length, - 8, + DEFAULT_TOKEN_CONDITIONAL_SWAP_COUNT, ), ); } @@ -3461,66 +3625,7 @@ export class MangoClient { return await this.sendAndConfirmTransactionForGroup(group, ixs); } - // public async tokenConditionalSwapBuyLimit( - // group: Group, - // account: MangoAccount, - // buyMintPk: PublicKey, - // sellMintPk: PublicKey, - // maxBuy: number, - // expiryTimestamp: number | null, - // priceLowerLimit: number, // Note: priceLowerLimit should be lower than priceUpperLimit - // priceUpperLimit: number, - // pricePremiumFraction: number, - // ): Promise { - // const buyBank: Bank = group.getFirstBankByMint(buyMintPk); - // const sellBank: Bank = group.getFirstBankByMint(sellMintPk); - // priceLowerLimit = - // priceLowerLimit * - // Math.pow(10, sellBank.mintDecimals - buyBank.mintDecimals); - // priceUpperLimit = - // priceUpperLimit * - // Math.pow(10, sellBank.mintDecimals - buyBank.mintDecimals); - - // const tcsIx = await this.program.methods - // .tokenConditionalSwapCreate( - // toNative(maxBuy, buyBank.mintDecimals), - // U64_MAX_BN, - // expiryTimestamp !== null ? new BN(expiryTimestamp) : U64_MAX_BN, - // priceLowerLimit, - // priceUpperLimit, - // pricePremiumFraction / 100, - // true, - // false, - // ) - // .accounts({ - // group: group.publicKey, - // account: account.publicKey, - // authority: (this.program.provider as AnchorProvider).wallet.publicKey, - // buyBank: buyBank.publicKey, - // sellBank: sellBank.publicKey, - // }) - // .instruction(); - - // const ixs: TransactionInstruction[] = []; - // if (account.tokenConditionalSwaps.length == 0) { - // ixs.push( - // await this.accountExpandV2Ix( - // group, - // account, - // account.tokens.length, - // account.serum3.length, - // account.perps.length, - // account.perpOpenOrders.length, - // 8, - // ), - // ); - // } - // ixs.push(tcsIx); - - // return await this.sendAndConfirmTransactionForGroup(group, ixs); - // } - - public async tokenConditionalSwapCreate( + public async tokenConditionalSwapCreateRaw( group: Group, account: MangoAccount, buyMintPk: PublicKey, diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index f926d126e..1b9541539 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -9,6 +9,7 @@ import { VersionedTransaction, } from '@solana/web3.js'; import BN from 'bn.js'; +import { Bank } from './accounts/bank'; import { I80F48 } from './numbers/I80F48'; import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from './utils/spl'; @@ -38,6 +39,22 @@ export function toNative(uiAmount: number, decimals: number): BN { return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0)); } +export function toNativeSellPerBuyTokenPrice( + price: number, + sellBank: Bank, + buyBank: Bank, +): number { + return price * Math.pow(10, sellBank.mintDecimals - buyBank.mintDecimals); +} + +export function toUiSellPerBuyTokenPrice( + price: number, + sellBank: Bank, + buyBank: Bank, +): number { + return toUiDecimals(price, sellBank.mintDecimals - buyBank.mintDecimals); +} + export function toUiDecimals( nativeAmount: BN | I80F48 | number, decimals: number,