Skip to content

Commit

Permalink
add exampleVaultClient
Browse files Browse the repository at this point in the history
  • Loading branch information
wphan committed Oct 21, 2023
1 parent 8aa6624 commit f35af4c
Show file tree
Hide file tree
Showing 12 changed files with 2,587 additions and 555 deletions.
7 changes: 7 additions & 0 deletions ts/exampleVaultStrategy/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# this is the account listed as delegate of the vault
# create with `solana-keygen pubkey /path/to/solana_private_key.json`
DELEGATE_PRIVATE_KEY=/path/to/solana_private_key.json
VAULT_ADDRESS=


RPC_HTTP_URL=https://api.mainnet-beta.solana.com
37 changes: 37 additions & 0 deletions ts/exampleVaultStrategy/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": {
"browser": true
},
"ignorePatterns": ["**/lib", "**/node_modules", "migrations"],
"plugins": [],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
2,
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/no-empty-function": 0,
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
"no-prototype-builtins": "off",
"semi": 2
},
"settings": {
"react": {
"version": "detect"
}
}
}
1 change: 1 addition & 0 deletions ts/exampleVaultStrategy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
2 changes: 2 additions & 0 deletions ts/exampleVaultStrategy/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/node_modules/**
protocol
9 changes: 9 additions & 0 deletions ts/exampleVaultStrategy/.prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 80,
tabWidth: 2,
useTabs: true,
bracketSameLine: false,
};
51 changes: 51 additions & 0 deletions ts/exampleVaultStrategy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Example Vault Strategy

This is an example of a trading strategy that trades the funds in a vault.

## Prerequisites
1) initialize a vault on chain
* see the [wiki](https://github.com/drift-labs/drift-vaults/wiki/Initialize-A-Vault)
* and [cli README](../sdk/cli/README.md)
2) get your vault address, there are multiple ways:
* `yarn cli derive-vault-address --vault-name="your vault name"`
* note it down during initialization
* [streamlit](https://driftv2.herokuapp.com/?tab=Vaults)
3) the private key to the address listed as the __delegate__ of the vault
* by default this is the authority who initialized the vault
* it can be changed with `yarn cli manager-update-delegate`

## Explanation

### How the account permissions work

Accounts on drift can have a __delegate__. The vault has a drift account, and is able to assign a delegate to it. The delegate is able to sign transactions on behalf of the vault. This is how the strategy will be trading the vault funds.


### Strategy

Assumptions on the vault setup:
* vault has some redeem period, so depositors will not withdraw funds immediately
* the deposit token is spot marketIndex 0 (USDC)
* only provides liquidity on SOL-PERP

This vault strategy is a simple market making strategy that quotes around the current oracle price with a 10 bps edge. It provides liquidity with 20% of the vault's available balance (less any pending withdraws).

### Usage

1) install dependencies
```
git clone [email protected]:drift-labs/drift-vaults.git
cd ts/exampleVaultStrategy
yarn
yarn install
```
2) set environment variables in a new .env file
```
cp .env.example .env
```
3) run the strategy
```
yarn run dev
```
35 changes: 35 additions & 0 deletions ts/exampleVaultStrategy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@drift-labs/example-vault-strategy",
"version": "0.0.0",
"author": "pwhan",
"main": "lib/index.js",
"license": "Apache-2.0",
"dependencies": {
"@drift-labs/sdk": "2.42.0-beta.10",
"dotenv": "^10.0.0"
},
"devDependencies": {
"@types/node": "^20.8.7",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^4.28.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "3.0.1",
"ts-node": "^10.9.1"
},
"scripts": {
"build": "yarn clean && tsc",
"clean": "rm -rf lib",
"start": "node lib/index.js",
"dev": "NODE_OPTIONS=--max-old-space-size=8192 ts-node src/index.ts",
"prettify": "prettier --check './src/**/*.ts'",
"prettify:fix": "prettier --write './src/**/*.ts'",
"lint": "eslint . --ext ts --quiet",
"lint:fix": "eslint . --ext ts --fix",
"test": "mocha -r ts-node/register ./src/**/*.test.ts"
},
"engines": {
"node": ">=18"
}
}
239 changes: 239 additions & 0 deletions ts/exampleVaultStrategy/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import {
AddressLookupTableAccount,
ComputeBudgetProgram,
Connection,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import * as anchor from '@coral-xyz/anchor';
import {
BASE_PRECISION,
BN,
DriftClient,
FastSingleTxSender,
MarketType,
PRICE_PRECISION,
PositionDirection,
PostOnlyParams,
TEN,
convertToNumber,
getLimitOrderParams,
getOrderParams,
} from '@drift-labs/sdk';

import { VAULT_PROGRAM_ID, Vault, VaultClient } from '../../sdk/src';
import { IDL } from '../../sdk/src/types/drift_vaults';

import { calculateAccountValueUsd, getWallet } from './utils';

import dotenv from 'dotenv';
dotenv.config();

const PERP_MARKET_TO_MM = 0; // SOL-PERP
const MM_EDGE_BPS = 10;
const BPS_BASE = 10000;
const PCT_ACCOUNT_VALUE_TO_QUOTE = 0.1; // quote 10% of account value per side
const SUFFICIENT_QUOTE_CHANGE_BPS = 2; // only requote if quote price changes by 2 bps

const stateCommitment = 'confirmed';

const delegatePrivateKey = process.env.DELEGATE_PRIVATE_KEY;
if (!delegatePrivateKey) {
throw new Error('DELEGATE_PRIVATE_KEY not set');
}

const [_, delegateWallet] = getWallet(delegatePrivateKey);

const connection = new Connection(process.env.RPC_HTTP_URL!, {
wsEndpoint: process.env.RPC_WS_URL,
commitment: stateCommitment,
});
console.log(`Wallet: ${delegateWallet.publicKey.toBase58()}`);
console.log(`RPC endpoint: ${process.env.RPC_HTTP_URL}`);
console.log(`WS endpoint: ${process.env.RPC_WS_URL}`);

if (!process.env.RPC_HTTP_URL) {
throw new Error('must set RPC_HTTP_URL not set');
}

const vaultAddressString = process.env.VAULT_ADDRESS;
if (!vaultAddressString) {
throw new Error('must set VAULT_ADDRESS not set');
}
const vaultAddress = new PublicKey(vaultAddressString);

const driftClient = new DriftClient({
connection,
wallet: delegateWallet,
env: 'mainnet-beta',
opts: {
commitment: stateCommitment,
skipPreflight: false,
preflightCommitment: stateCommitment,
},
authority: vaultAddress, // this is the vault's address with a drift account
activeSubAccountId: 0, // vault should only have subaccount 0
subAccountIds: [0],
txSender: new FastSingleTxSender({
connection,
wallet: delegateWallet,
opts: {
commitment: stateCommitment,
skipPreflight: false,
preflightCommitment: stateCommitment,
},
timeout: 3000,
blockhashRefreshInterval: 1000,
}),
});
let driftLookupTableAccount: AddressLookupTableAccount | undefined;

const vaultProgramId = VAULT_PROGRAM_ID;
const vaultProgram = new anchor.Program(
IDL,
vaultProgramId,
driftClient.provider
);
const driftVault = new VaultClient({
driftClient: driftClient as any,
program: vaultProgram as any,
cliMode: false,
});
let vault: Vault | undefined;

async function updateVaultAccount() {
// makes RPC request to fetch vault state
vault = await driftVault.getVault(vaultAddress);
}

let lastBid: number | undefined;
let lastAsk: number | undefined;
function sufficientQuoteChange(newBid: number, newAsk: number): boolean {
if (lastBid === undefined || lastAsk === undefined) {
return true;
}
const bidDiff = newBid / lastBid - 1;
const askDiff = newAsk / lastAsk - 1;

if (
Math.abs(bidDiff) > SUFFICIENT_QUOTE_CHANGE_BPS / BPS_BASE ||
Math.abs(askDiff) > SUFFICIENT_QUOTE_CHANGE_BPS / BPS_BASE
) {
return true;
}

return false;
}

async function runMmLoop() {
const user = driftClient.getUser();
if (!vault) {
console.log(`Vault has not been loaded yet`);
return;
}
const usdcSpotMarket = driftClient.getSpotMarketAccount(0);
if (!usdcSpotMarket) {
throw new Error(`No spot market found for USDC`);
}
const usdcPrecision = TEN.pow(new BN(usdcSpotMarket.decimals));
const vaultWithdrawalsRequested = convertToNumber(
vault.totalWithdrawRequested,
usdcPrecision
);
const currentAccountValue = calculateAccountValueUsd(user);
const accessibleAccountValue =
currentAccountValue - vaultWithdrawalsRequested;
console.log(
`Current vault equity: ${currentAccountValue}, withdrawals requested: ${vaultWithdrawalsRequested}`
);

const perpOracle = driftClient.getOracleDataForPerpMarket(PERP_MARKET_TO_MM);

const oraclePriceNumber = convertToNumber(perpOracle.price, PRICE_PRECISION);
const baseToQuote =
(accessibleAccountValue * PCT_ACCOUNT_VALUE_TO_QUOTE) / oraclePriceNumber;

const newBid = oraclePriceNumber * (1 - MM_EDGE_BPS / BPS_BASE);
const newAsk = oraclePriceNumber * (1 + MM_EDGE_BPS / BPS_BASE);
console.log(`New bid: ${newBid}, new ask: ${newAsk}`);

// only requote on sufficient change
if (!sufficientQuoteChange(newBid, newAsk)) {
console.log(`Not re-quoting, insufficient change`);
return;
}

// cancel orders and place new ones
const ixs: Array<TransactionInstruction> = [];
ixs.push(
ComputeBudgetProgram.setComputeUnitLimit({
units: 1_400_000,
})
);
ixs.push(
await driftClient.getCancelOrdersIx(
MarketType.PERP,
PERP_MARKET_TO_MM,
null
)
);
ixs.push(
await driftClient.getPlaceOrdersIx([
getOrderParams(
getLimitOrderParams({
marketType: MarketType.PERP,
marketIndex: PERP_MARKET_TO_MM,
direction: PositionDirection.LONG,
baseAssetAmount: new BN(baseToQuote * BASE_PRECISION.toNumber()),
price: new BN(newBid * PRICE_PRECISION.toNumber()),
postOnly: PostOnlyParams.SLIDE, // will adjust crossing orders s.t. they don't cross
})
),
getOrderParams(
getLimitOrderParams({
marketType: MarketType.PERP,
marketIndex: PERP_MARKET_TO_MM,
direction: PositionDirection.SHORT,
baseAssetAmount: new BN(baseToQuote * BASE_PRECISION.toNumber()),
price: new BN(newAsk * PRICE_PRECISION.toNumber()),
postOnly: PostOnlyParams.SLIDE, // will adjust crossing orders s.t. they don't cross
})
),
])
);
const txSig = await driftClient.txSender.sendVersionedTransaction(
await driftClient.txSender.getVersionedTransaction(
ixs,
[driftLookupTableAccount!],
[],
driftClient.opts
)
);
console.log(
`Requoting ${baseToQuote} SOL, ${newBid} @ ${newAsk}, oracle: ${oraclePriceNumber}, tx: https://solscan.io/tx/${txSig.txSig}`
);

lastBid = newBid;
lastAsk = newAsk;
}

async function main() {
await driftClient.subscribe();
driftLookupTableAccount = await driftClient.fetchMarketLookupTableAccount();
await updateVaultAccount();

console.log(`Starting Basic Vault Strategy`);
console.log(` Vault: ${vaultAddress.toBase58()}`);
console.log(` Trading as delegate: ${delegateWallet.publicKey.toBase58()}`);

// run mm loop every 10s
setInterval(runMmLoop, 10000);

// update vault account in the background, it's less critical
setInterval(updateVaultAccount, 60000);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Loading

0 comments on commit f35af4c

Please sign in to comment.