-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
2,587 additions
and
555 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
**/node_modules/** | ||
protocol |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
Oops, something went wrong.