token conditional swaps, fixes from review (#653)
* tcs fixes from review, and easy to use helper functions * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> * Fixes from review Signed-off-by: microwavedcola1 <microwavedcola@gmail.com> --------- Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
66786410c3
commit
776545fcdd
|
@ -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';
|
||||
|
|
|
@ -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')!,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<TransactionSignature> {
|
||||
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<TransactionSignature> {
|
||||
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<TransactionSignature> {
|
||||
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<TransactionSignature> {
|
||||
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<TransactionSignature> {
|
||||
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<TransactionSignature> {
|
||||
// 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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue