first
This commit is contained in:
parent
b01f124a16
commit
fbdda70b98
|
@ -0,0 +1,104 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
35
README.md
35
README.md
|
@ -1 +1,34 @@
|
|||
# market-marker-ts
|
||||
# Mango Markets Market Maker
|
||||
## UNDER CONSTRUCTION - DO NOT USE
|
||||
|
||||
## Setup
|
||||
To run the market maker you will need:
|
||||
* A Solana account with some SOL deposited to cover transaction fees
|
||||
* A Mango Account with some collateral deposited and a name (tip: use the UI)
|
||||
* Your wallet keypair saved as a JSON file
|
||||
* `node` and `yarn`
|
||||
* A clone of this repository
|
||||
* Dependencies installed with `yarn install`
|
||||
|
||||
## Environment Variables
|
||||
| Variable | Default | Description |
|
||||
| -------- | ------- | ----------- |
|
||||
| `ENDPOINT_URL` | `https://mango.rpcpool.com` | Your RPC node endpoint |
|
||||
| `KEYPAIR` | `${HOME}/.config/solana/id.json` | The location of your wallet keypair |
|
||||
| `PARAMS` | `params/default.json` | path to params file |
|
||||
|
||||
|
||||
## Market Maker Params
|
||||
### See params/default.json for an example
|
||||
| `group` | `mainnet.1` | Name of the group in ids.json |
|
||||
| `interval` | `10000` | Milliseconds to wait before updating quotes |
|
||||
| `mangoAccountName` | N/A | The MangoAccount name you input when initializing the MangoAccount via UI |
|
||||
| `mangoAccountPubkey` | N/A | If no MangoAccount name, just pass in the pubkey |
|
||||
| `assets` | N/A | Mapping of symbols to trade and their specific params |
|
||||
| `size_perc` | `0.1` | The size of each order as a percentage of equity |
|
||||
| `charge` | `0.0010` | Half the quote width |
|
||||
| `lean_coeff` | `0.0005` | How much to move the quotes per unit size of inventory |
|
||||
| `bias` | `0` | Fixed amount to bias. Negative values bias downward. e.g. -0.0005 biases down 5bps |
|
||||
|
||||
|
||||
## Setup systemd
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "market-marker",
|
||||
"version": "1.0.0",
|
||||
"repository": "git@github.com:blockworks-foundation/market-maker-ts.git",
|
||||
"license": "MIT",
|
||||
"main": "lib/src/index.js",
|
||||
"types": "lib/src/index.d.ts",
|
||||
"scripts": {
|
||||
"mm": "ts-node src/mm.ts",
|
||||
"format": "prettier --check ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"@types/big.js": "^6.1.1",
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/jest": "^26.0.9",
|
||||
"@types/mocha": "^8.2.0",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/yargs": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||
"@typescript-eslint/parser": "^4.14.2",
|
||||
"chai": "^4.3.4",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"mocha": "^8.4.0",
|
||||
"prettier": "^2.0.5",
|
||||
"ts-node": "^9.1.1",
|
||||
"typedoc": "^0.22.5",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "git+https://github.com/blockworks-foundation/mango-client-v3.git",
|
||||
"@project-serum/anchor": "^0.16.2",
|
||||
"@project-serum/serum": "0.13.55",
|
||||
"@project-serum/sol-wallet-adapter": "^0.2.0",
|
||||
"@solana/spl-token": "^0.1.6",
|
||||
"@solana/web3.js": "1.21.0",
|
||||
"axios": "^0.21.1",
|
||||
"big.js": "^6.1.1",
|
||||
"bigint-buffer": "^1.1.5",
|
||||
"bn.js": "^5.2.0",
|
||||
"buffer-layout": "^1.2.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv-expand": "^5.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"bn.js": "5.1.3",
|
||||
"@types/bn.js": "5.1.0",
|
||||
"@solana/web3.js": "^1.21.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"group": "mainnet.1",
|
||||
"mangoAccountName": "test",
|
||||
"mangoAccountPubkey": "optional pubkey string if your mango account doesn't have name",
|
||||
"interval": 5000,
|
||||
"batch": 2,
|
||||
"assets": {
|
||||
"MNGO": {
|
||||
"perp": {
|
||||
"charge": 0.0020,
|
||||
"sizePerc": 0.02,
|
||||
"leanCoeff": 0.0010,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
|
||||
"BTC": {
|
||||
"perp": {
|
||||
"charge": 0.0005,
|
||||
"sizePerc": 0.1,
|
||||
"leanCoeff": 0.00025,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
"ETH": {
|
||||
"perp": {
|
||||
"charge": 0.0020,
|
||||
"sizePerc": 0.02,
|
||||
"leanCoeff": 0.0010,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
|
||||
"SOL": {
|
||||
"perp": {
|
||||
"charge": 0.0010,
|
||||
"sizePerc": 0.1,
|
||||
"leanCoeff": 0.0005,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
"SRM": {
|
||||
"perp": {
|
||||
"charge": 0.0020,
|
||||
"sizePerc": 0.02,
|
||||
"leanCoeff": 0.0010,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
"RAY": {
|
||||
"perp": {
|
||||
"charge": 0.0020,
|
||||
"sizePerc": 0.02,
|
||||
"leanCoeff": 0.0010,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
"FTT": {
|
||||
"perp": {
|
||||
"charge": 0.0020,
|
||||
"sizePerc": 0.02,
|
||||
"leanCoeff": 0.0010,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
},
|
||||
|
||||
"ADA": {
|
||||
"perp": {
|
||||
"charge": 0.0010,
|
||||
"sizePerc": 0.1,
|
||||
"leanCoeff": 0.0005,
|
||||
"bias": 0.0,
|
||||
"requoteThresh": 0.0002,
|
||||
"takeSpammers": true,
|
||||
"spammerCharge": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,521 @@
|
|||
import {
|
||||
Account,
|
||||
Commitment,
|
||||
Connection,
|
||||
Transaction,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { BN } from 'bn.js';
|
||||
import {
|
||||
BookSide,
|
||||
BookSideLayout,
|
||||
Cluster,
|
||||
Config,
|
||||
getMultipleAccounts,
|
||||
getPerpMarketByBaseSymbol,
|
||||
GroupConfig,
|
||||
IDS,
|
||||
makeCancelAllPerpOrdersInstruction,
|
||||
makePlacePerpOrderInstruction,
|
||||
MangoAccount,
|
||||
MangoAccountLayout,
|
||||
MangoCache,
|
||||
MangoCacheLayout,
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
ONE_BN,
|
||||
PerpMarket,
|
||||
PerpMarketConfig,
|
||||
sleep,
|
||||
zeroKey,
|
||||
} from '@blockworks-foundation/mango-client';
|
||||
import { OpenOrders } from '@project-serum/serum';
|
||||
import path from 'path';
|
||||
import { loadMangoAccountWithName, loadMangoAccountWithPubkey } from './utils';
|
||||
|
||||
const params = JSON.parse(
|
||||
fs.readFileSync(
|
||||
process.env.PARAMS || path.resolve(__dirname, '../params/default.json'),
|
||||
'utf-8',
|
||||
),
|
||||
);
|
||||
|
||||
const payer = new Account(
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json',
|
||||
'utf-8',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const config = new Config(IDS);
|
||||
|
||||
const groupIds = config.getGroupWithName(params.group) as GroupConfig;
|
||||
if (!groupIds) {
|
||||
throw new Error(`Group ${params.group} not found`);
|
||||
}
|
||||
const cluster = groupIds.cluster as Cluster;
|
||||
const mangoProgramId = groupIds.mangoProgramId;
|
||||
const mangoGroupKey = groupIds.publicKey;
|
||||
|
||||
const control = { isRunning: true, interval: params.interval };
|
||||
|
||||
type MarketContext = {
|
||||
marketName: string;
|
||||
params: any;
|
||||
config: PerpMarketConfig;
|
||||
market: PerpMarket;
|
||||
marketIndex: number;
|
||||
bids: BookSide;
|
||||
asks: BookSide;
|
||||
};
|
||||
/**
|
||||
* Load MangoCache, MangoAccount and Bids and Asks for all PerpMarkets using only
|
||||
* one RPC call.
|
||||
*/
|
||||
async function loadAccountAndMarketState(
|
||||
connection: Connection,
|
||||
group: MangoGroup,
|
||||
oldMangoAccount: MangoAccount,
|
||||
marketContexts: MarketContext[],
|
||||
): Promise<{
|
||||
cache: MangoCache;
|
||||
mangoAccount: MangoAccount;
|
||||
marketContexts: MarketContext[];
|
||||
}> {
|
||||
const inBasketOpenOrders = oldMangoAccount
|
||||
.getOpenOrdersKeysInBasket()
|
||||
.filter((pk) => !pk.equals(zeroKey));
|
||||
|
||||
const allAccounts = [
|
||||
group.mangoCache,
|
||||
oldMangoAccount.publicKey,
|
||||
...inBasketOpenOrders,
|
||||
...marketContexts.map((marketContext) => marketContext.market.bids),
|
||||
...marketContexts.map((marketContext) => marketContext.market.asks),
|
||||
];
|
||||
|
||||
const accountInfos = await getMultipleAccounts(connection, allAccounts);
|
||||
|
||||
const cache = new MangoCache(
|
||||
accountInfos[0].publicKey,
|
||||
MangoCacheLayout.decode(accountInfos[0].accountInfo.data),
|
||||
);
|
||||
|
||||
const mangoAccount = new MangoAccount(
|
||||
accountInfos[1].publicKey,
|
||||
MangoAccountLayout.decode(accountInfos[1].accountInfo.data),
|
||||
);
|
||||
const openOrdersAis = accountInfos.slice(2, 2 + inBasketOpenOrders.length);
|
||||
for (let i = 0; i < openOrdersAis.length; i++) {
|
||||
const ai = openOrdersAis[i];
|
||||
const marketIndex = mangoAccount.spotOpenOrders.findIndex((soo) =>
|
||||
soo.equals(ai.publicKey),
|
||||
);
|
||||
mangoAccount.spotOpenOrdersAccounts[marketIndex] =
|
||||
OpenOrders.fromAccountInfo(
|
||||
ai.publicKey,
|
||||
ai.accountInfo,
|
||||
group.dexProgramId,
|
||||
);
|
||||
}
|
||||
|
||||
accountInfos
|
||||
.slice(
|
||||
2 + inBasketOpenOrders.length,
|
||||
2 + inBasketOpenOrders.length + marketContexts.length,
|
||||
)
|
||||
.forEach((ai, i) => {
|
||||
marketContexts[i].bids = new BookSide(
|
||||
ai.publicKey,
|
||||
marketContexts[i].market,
|
||||
BookSideLayout.decode(ai.accountInfo.data),
|
||||
);
|
||||
});
|
||||
|
||||
accountInfos
|
||||
.slice(
|
||||
2 + inBasketOpenOrders.length + marketContexts.length,
|
||||
2 + inBasketOpenOrders.length + 2 * marketContexts.length,
|
||||
)
|
||||
.forEach((ai, i) => {
|
||||
marketContexts[i].asks = new BookSide(
|
||||
ai.publicKey,
|
||||
marketContexts[i].market,
|
||||
BookSideLayout.decode(ai.accountInfo.data),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
cache,
|
||||
mangoAccount,
|
||||
marketContexts,
|
||||
};
|
||||
}
|
||||
async function fullMarketMaker() {
|
||||
const connection = new Connection(
|
||||
process.env.ENDPOINT_URL || config.cluster_urls[cluster],
|
||||
'processed' as Commitment,
|
||||
);
|
||||
const client = new MangoClient(connection, mangoProgramId);
|
||||
|
||||
// load group
|
||||
const mangoGroup = await client.getMangoGroup(mangoGroupKey);
|
||||
|
||||
// load mangoAccount
|
||||
let mangoAccount: MangoAccount;
|
||||
if (params.mangoAccountName) {
|
||||
mangoAccount = await loadMangoAccountWithName(
|
||||
client,
|
||||
mangoGroup,
|
||||
payer,
|
||||
params.mangoAccountName,
|
||||
);
|
||||
} else if (params.mangoAccountPubkey) {
|
||||
mangoAccount = await loadMangoAccountWithPubkey(
|
||||
client,
|
||||
mangoGroup,
|
||||
payer,
|
||||
params.mangoAccountPubkey,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Please add mangoAccountName or mangoAccountPubkey to params file',
|
||||
);
|
||||
}
|
||||
|
||||
const marketContexts: MarketContext[] = [];
|
||||
for (const baseSymbol in params.assets) {
|
||||
const perpMarketConfig = getPerpMarketByBaseSymbol(
|
||||
groupIds,
|
||||
baseSymbol,
|
||||
) as PerpMarketConfig;
|
||||
const perpMarket = await client.getPerpMarket(
|
||||
perpMarketConfig.publicKey,
|
||||
perpMarketConfig.baseDecimals,
|
||||
perpMarketConfig.quoteDecimals,
|
||||
);
|
||||
marketContexts.push({
|
||||
marketName: perpMarketConfig.name,
|
||||
params: params.assets[baseSymbol].perp,
|
||||
config: perpMarketConfig,
|
||||
market: perpMarket,
|
||||
marketIndex: perpMarketConfig.marketIndex,
|
||||
bids: await perpMarket.loadBids(connection),
|
||||
asks: await perpMarket.loadAsks(connection),
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
console.log('Caught keyboard interrupt. Canceling orders');
|
||||
control.isRunning = false;
|
||||
onExit(client, payer, mangoGroup, mangoAccount, marketContexts);
|
||||
});
|
||||
|
||||
while (control.isRunning) {
|
||||
try {
|
||||
const state = await loadAccountAndMarketState(
|
||||
connection,
|
||||
mangoGroup,
|
||||
mangoAccount,
|
||||
marketContexts,
|
||||
);
|
||||
mangoAccount = state.mangoAccount;
|
||||
|
||||
let j = 0;
|
||||
let tx = new Transaction();
|
||||
const txProms: any[] = [];
|
||||
for (let i = 0; i < marketContexts.length; i++) {
|
||||
const instrSet = makeMarketUpdateInstructions(
|
||||
mangoGroup,
|
||||
state.cache,
|
||||
mangoAccount,
|
||||
marketContexts[i],
|
||||
);
|
||||
|
||||
if (instrSet.length > 0) {
|
||||
instrSet.forEach((ix) => tx.add(ix));
|
||||
j++;
|
||||
if (j === params.batch) {
|
||||
txProms.push(client.sendTransaction(tx, payer, []));
|
||||
tx = new Transaction();
|
||||
j = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tx.instructions.length) {
|
||||
txProms.push(client.sendTransaction(tx, payer, []));
|
||||
}
|
||||
if (txProms.length) {
|
||||
const txids = await Promise.all(txProms);
|
||||
txids.forEach((txid) => console.log(`success ${txid.toString()}`));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
console.log(
|
||||
`${new Date().toUTCString()} sleeping for ${control.interval / 1000}s`,
|
||||
);
|
||||
await sleep(control.interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeMarketUpdateInstructions(
|
||||
group: MangoGroup,
|
||||
cache: MangoCache,
|
||||
mangoAccount: MangoAccount,
|
||||
marketContext: MarketContext,
|
||||
): TransactionInstruction[] {
|
||||
// Right now only uses the perp
|
||||
const marketIndex = marketContext.marketIndex;
|
||||
const market = marketContext.market;
|
||||
const bids = marketContext.bids;
|
||||
const asks = marketContext.asks;
|
||||
const fairValue = group.getPrice(marketIndex, cache).toNumber();
|
||||
const equity = mangoAccount.computeValue(group, cache).toNumber();
|
||||
const perpAccount = mangoAccount.perpAccounts[marketIndex];
|
||||
// TODO look at event queue as well for unprocessed fills
|
||||
const basePos = perpAccount.getBasePositionUi(market);
|
||||
|
||||
const sizePerc = marketContext.params.sizePerc;
|
||||
const leanCoeff = marketContext.params.leanCoeff;
|
||||
const charge = marketContext.params.charge;
|
||||
const bias = marketContext.params.bias;
|
||||
const requoteThresh = marketContext.params.requoteThresh;
|
||||
const takeSpammers = marketContext.params.takeSpammers;
|
||||
const spammerCharge = marketContext.params.spammerCharge;
|
||||
const size = (equity * sizePerc) / fairValue;
|
||||
const lean = (-leanCoeff * basePos) / size;
|
||||
const bidPrice = fairValue * (1 - charge + lean + bias);
|
||||
const askPrice = fairValue * (1 + charge + lean + bias);
|
||||
// TODO volatility adjustment
|
||||
|
||||
const [modelBidPrice, nativeBidSize] = market.uiToNativePriceQuantity(
|
||||
bidPrice,
|
||||
size,
|
||||
);
|
||||
const [modelAskPrice, nativeAskSize] = market.uiToNativePriceQuantity(
|
||||
askPrice,
|
||||
size,
|
||||
);
|
||||
|
||||
const bestBid = bids.getBest();
|
||||
const bestAsk = asks.getBest();
|
||||
const bookAdjBid =
|
||||
bestAsk !== undefined
|
||||
? BN.min(bestAsk.priceLots.sub(ONE_BN), modelBidPrice)
|
||||
: modelBidPrice;
|
||||
const bookAdjAsk =
|
||||
bestBid !== undefined
|
||||
? BN.max(bestBid.priceLots.add(ONE_BN), modelAskPrice)
|
||||
: modelAskPrice;
|
||||
|
||||
// TODO use order book to requote if size has changed
|
||||
const openOrders = mangoAccount
|
||||
.getPerpOpenOrders()
|
||||
.filter((o) => o.marketIndex === marketIndex);
|
||||
let moveOrders = openOrders.length === 0 || openOrders.length > 2;
|
||||
for (const o of openOrders) {
|
||||
console.log(
|
||||
`${o.side} ${o.price.toString()} -> ${
|
||||
o.side === 'buy' ? bookAdjBid.toString() : bookAdjAsk.toString()
|
||||
}`,
|
||||
);
|
||||
|
||||
if (o.side === 'buy') {
|
||||
if (
|
||||
Math.abs(o.price.toNumber() / bookAdjBid.toNumber() - 1) > requoteThresh
|
||||
) {
|
||||
moveOrders = true;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
Math.abs(o.price.toNumber() / bookAdjAsk.toNumber() - 1) > requoteThresh
|
||||
) {
|
||||
moveOrders = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start building the transaction
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
/*
|
||||
Clear 1 lot size orders at the top of book that bad people use to manipulate the price
|
||||
*/
|
||||
if (
|
||||
takeSpammers &&
|
||||
bestBid !== undefined &&
|
||||
bestBid.sizeLots.eq(ONE_BN) &&
|
||||
bestBid.priceLots.toNumber() / modelAskPrice.toNumber() - 1 >
|
||||
spammerCharge * charge + 0.0005
|
||||
) {
|
||||
console.log(`${marketContext.marketName} taking best bid spammer`);
|
||||
const takerSell = makePlacePerpOrderInstruction(
|
||||
mangoProgramId,
|
||||
group.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
cache.publicKey,
|
||||
market.publicKey,
|
||||
market.bids,
|
||||
market.asks,
|
||||
market.eventQueue,
|
||||
mangoAccount.getOpenOrdersKeysInBasket(),
|
||||
bestBid.priceLots,
|
||||
ONE_BN,
|
||||
new BN(Date.now()),
|
||||
'sell',
|
||||
'ioc',
|
||||
);
|
||||
instructions.push(takerSell);
|
||||
} else if (
|
||||
takeSpammers &&
|
||||
bestAsk !== undefined &&
|
||||
bestAsk.sizeLots.eq(ONE_BN) &&
|
||||
modelBidPrice.toNumber() / bestAsk.priceLots.toNumber() - 1 >
|
||||
spammerCharge * charge + 0.0005
|
||||
) {
|
||||
console.log(`${marketContext.marketName} taking best ask spammer`);
|
||||
const takerBuy = makePlacePerpOrderInstruction(
|
||||
mangoProgramId,
|
||||
group.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
cache.publicKey,
|
||||
market.publicKey,
|
||||
market.bids,
|
||||
market.asks,
|
||||
market.eventQueue,
|
||||
mangoAccount.getOpenOrdersKeysInBasket(),
|
||||
bestAsk.priceLots,
|
||||
ONE_BN,
|
||||
new BN(Date.now()),
|
||||
'buy',
|
||||
'ioc',
|
||||
);
|
||||
instructions.push(takerBuy);
|
||||
}
|
||||
if (moveOrders) {
|
||||
// cancel all, requote
|
||||
const cancelAllInstr = makeCancelAllPerpOrdersInstruction(
|
||||
mangoProgramId,
|
||||
group.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
market.publicKey,
|
||||
market.bids,
|
||||
market.asks,
|
||||
new BN(20),
|
||||
);
|
||||
|
||||
const placeBidInstr = makePlacePerpOrderInstruction(
|
||||
mangoProgramId,
|
||||
group.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
cache.publicKey,
|
||||
market.publicKey,
|
||||
market.bids,
|
||||
market.asks,
|
||||
market.eventQueue,
|
||||
mangoAccount.getOpenOrdersKeysInBasket(),
|
||||
bookAdjBid,
|
||||
nativeBidSize,
|
||||
new BN(Date.now()),
|
||||
'buy',
|
||||
'postOnlySlide',
|
||||
);
|
||||
|
||||
const placeAskInstr = makePlacePerpOrderInstruction(
|
||||
mangoProgramId,
|
||||
group.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
cache.publicKey,
|
||||
market.publicKey,
|
||||
market.bids,
|
||||
market.asks,
|
||||
market.eventQueue,
|
||||
mangoAccount.getOpenOrdersKeysInBasket(),
|
||||
bookAdjAsk,
|
||||
nativeAskSize,
|
||||
new BN(Date.now()),
|
||||
'sell',
|
||||
'postOnlySlide',
|
||||
);
|
||||
instructions.push(cancelAllInstr);
|
||||
instructions.push(placeBidInstr);
|
||||
instructions.push(placeAskInstr);
|
||||
} else {
|
||||
console.log(
|
||||
`${marketContext.marketName} Not requoting. No need to move orders`,
|
||||
);
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
async function onExit(
|
||||
client: MangoClient,
|
||||
payer: Account,
|
||||
group: MangoGroup,
|
||||
mangoAccount: MangoAccount,
|
||||
marketContexts: MarketContext[],
|
||||
) {
|
||||
await sleep(control.interval);
|
||||
mangoAccount = await client.getMangoAccount(
|
||||
mangoAccount.publicKey,
|
||||
group.dexProgramId,
|
||||
);
|
||||
let tx = new Transaction();
|
||||
const txProms: any[] = [];
|
||||
for (let i = 0; i < marketContexts.length; i++) {
|
||||
const mc = marketContexts[i];
|
||||
const cancelAllInstr = makeCancelAllPerpOrdersInstruction(
|
||||
mangoProgramId,
|
||||
group.publicKey,
|
||||
mangoAccount.publicKey,
|
||||
payer.publicKey,
|
||||
mc.market.publicKey,
|
||||
mc.market.bids,
|
||||
mc.market.asks,
|
||||
new BN(20),
|
||||
);
|
||||
tx.add(cancelAllInstr);
|
||||
if (tx.instructions === params.batch) {
|
||||
txProms.push(client.sendTransaction(tx, payer, []));
|
||||
tx = new Transaction();
|
||||
}
|
||||
}
|
||||
|
||||
if (tx.instructions.length) {
|
||||
txProms.push(client.sendTransaction(tx, payer, []));
|
||||
}
|
||||
const txids = await Promise.all(txProms);
|
||||
txids.forEach((txid) => {
|
||||
console.log(`cancel successful: ${txid.toString()}`);
|
||||
});
|
||||
process.exit();
|
||||
}
|
||||
|
||||
function startMarketMaker() {
|
||||
if (control.isRunning) {
|
||||
fullMarketMaker().finally(startMarketMaker);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', function (err, promise) {
|
||||
console.error(
|
||||
'Unhandled rejection (promise: ',
|
||||
promise,
|
||||
', reason: ',
|
||||
err,
|
||||
').',
|
||||
);
|
||||
});
|
||||
|
||||
startMarketMaker();
|
|
@ -0,0 +1,45 @@
|
|||
import { Account, PublicKey } from '@solana/web3.js';
|
||||
import {
|
||||
MangoAccount,
|
||||
MangoClient,
|
||||
MangoGroup,
|
||||
} from '@blockworks-foundation/mango-client';
|
||||
|
||||
export async function loadMangoAccountWithName(
|
||||
client: MangoClient,
|
||||
mangoGroup: MangoGroup,
|
||||
payer: Account,
|
||||
mangoAccountName: string,
|
||||
): Promise<MangoAccount> {
|
||||
const ownerAccounts = await client.getMangoAccountsForOwner(
|
||||
mangoGroup,
|
||||
payer.publicKey,
|
||||
true,
|
||||
);
|
||||
|
||||
for (const ownerAccount of ownerAccounts) {
|
||||
if (mangoAccountName === ownerAccount.name) {
|
||||
return ownerAccount;
|
||||
}
|
||||
}
|
||||
throw new Error(`mangoAccountName: ${mangoAccountName} not found`);
|
||||
}
|
||||
|
||||
export async function loadMangoAccountWithPubkey(
|
||||
client: MangoClient,
|
||||
mangoGroup: MangoGroup,
|
||||
payer: Account,
|
||||
mangoAccountPk: PublicKey,
|
||||
): Promise<MangoAccount> {
|
||||
const mangoAccount = await client.getMangoAccount(
|
||||
mangoAccountPk,
|
||||
mangoGroup.dexProgramId,
|
||||
);
|
||||
|
||||
if (!mangoAccount.owner.equals(payer.publicKey)) {
|
||||
throw new Error(
|
||||
`Invalid MangoAccount owner: ${mangoAccount.owner.toString()}; expected: ${payer.publicKey.toString()}`,
|
||||
);
|
||||
}
|
||||
return mangoAccount;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"target": "es6",
|
||||
"lib": ["es2019"],
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noImplicitAny": false,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"]
|
||||
}
|
Loading…
Reference in New Issue