Cex style api (#15)
* a simple cex style client #14 * add empty line Signed-off-by: feierabend654 <feierabend654@gmail.com> * fixes from code review Signed-off-by: feierabend654 <feierabend654@gmail.com> * make margin account pk mandatory Signed-off-by: feierabend654 <feierabend654@gmail.com> * load attached keypair Signed-off-by: feierabend654 <feierabend654@gmail.com> * try actions for yarn test Signed-off-by: feierabend654 <feierabend654@gmail.com> * refactor Signed-off-by: feierabend654 <feierabend654@gmail.com> * more refactoring Signed-off-by: feierabend654 <feierabend654@gmail.com> * export simple client Signed-off-by: feierabend654 <feierabend654@gmail.com> * remove testing code Signed-off-by: feierabend654 <feierabend654@gmail.com> * detect and throw error Signed-off-by: feierabend654 <feierabend654@gmail.com> Co-authored-by: Maximilian Schneider <mail@maximilianschneider.net>
This commit is contained in:
parent
6eaf842b9d
commit
3a2dbc889c
|
@ -0,0 +1,13 @@
|
||||||
|
name: CI
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn test-simple-client
|
|
@ -22,6 +22,7 @@
|
||||||
"prepare": "run-s clean build",
|
"prepare": "run-s clean build",
|
||||||
"shell": "node -e \"$(< shell)\" -i --experimental-repl-await",
|
"shell": "node -e \"$(< shell)\" -i --experimental-repl-await",
|
||||||
"test": "mocha -r ts-node/register tests/Stateless.test.ts --timeout 0",
|
"test": "mocha -r ts-node/register tests/Stateless.test.ts --timeout 0",
|
||||||
|
"test-simple-client": "mocha -r ts-node/register tests/simpleclient.test.ts --timeout 0",
|
||||||
"test:build": "run-s build",
|
"test:build": "run-s build",
|
||||||
"test:lint": "eslint src",
|
"test:lint": "eslint src",
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import IDS from './ids.json';
|
import IDS from './ids.json';
|
||||||
export { IDS }
|
export { IDS }
|
||||||
export { MangoClient, MangoGroup, MarginAccount, tokenToDecimals } from './client';
|
export { MangoClient, MangoGroup, MarginAccount, tokenToDecimals } from './client';
|
||||||
|
export { SimpleClient } from './simpleclient';
|
||||||
export { MangoIndexLayout, MarginAccountLayout, MangoGroupLayout } from './layout';
|
export { MangoIndexLayout, MarginAccountLayout, MangoGroupLayout } from './layout';
|
||||||
export * from './layout';
|
export * from './layout';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
|
@ -0,0 +1,699 @@
|
||||||
|
import { MangoClient, MangoGroup, MarginAccount } from './client';
|
||||||
|
import IDS from './ids.json';
|
||||||
|
import {
|
||||||
|
Account,
|
||||||
|
Commitment,
|
||||||
|
Connection,
|
||||||
|
PublicKey,
|
||||||
|
TransactionSignature,
|
||||||
|
} from '@solana/web3.js';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { Market, OpenOrders, Orderbook } from '@project-serum/serum';
|
||||||
|
import { Order } from '@project-serum/serum/lib/market';
|
||||||
|
import { ceilToDecimal, groupBy, nativeToUi } from './utils';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
// github issue - https://github.com/blockworks-foundation/mango-client-ts/issues/14
|
||||||
|
|
||||||
|
type TokenSymbol = string;
|
||||||
|
type SpotMarketSymbol = string;
|
||||||
|
type OpenOrdersAsString = string;
|
||||||
|
|
||||||
|
interface OpenOrderForPnl {
|
||||||
|
nativeQuantityReleased: number;
|
||||||
|
nativeQuantityPaid: number;
|
||||||
|
side: 'sell' | 'buy';
|
||||||
|
size: number;
|
||||||
|
openOrders: OpenOrdersAsString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MarketBalance {
|
||||||
|
constructor(
|
||||||
|
public baseTokenSymbol: string,
|
||||||
|
public orders: number,
|
||||||
|
public unsettled: number,
|
||||||
|
public quoteTokenSymbol: string,
|
||||||
|
public quoteOrders: number,
|
||||||
|
public quoteUnsettled: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MarginAccountBalance {
|
||||||
|
constructor(
|
||||||
|
public tokenSymbol: string,
|
||||||
|
public deposited: number,
|
||||||
|
public borrowed: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Balance {
|
||||||
|
constructor(
|
||||||
|
public marginAccountPublicKey: string,
|
||||||
|
public marginAccountBalances: MarginAccountBalance[],
|
||||||
|
public marketBalances: MarketBalance[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FetchMarketSymbol {
|
||||||
|
constructor(public symbol: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FetchMarket {
|
||||||
|
constructor(public symbols: FetchMarketSymbol[]) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Ticker {
|
||||||
|
constructor(
|
||||||
|
public symbol: string,
|
||||||
|
public price: number,
|
||||||
|
public timeMs: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Ohlcv {
|
||||||
|
constructor(
|
||||||
|
public timeS: number,
|
||||||
|
public open: number,
|
||||||
|
public high: number,
|
||||||
|
public low: number,
|
||||||
|
public close: number,
|
||||||
|
public volume: number,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resolution =
|
||||||
|
| '1'
|
||||||
|
| '3'
|
||||||
|
| '5'
|
||||||
|
| '15'
|
||||||
|
| '30'
|
||||||
|
| '60'
|
||||||
|
| '120'
|
||||||
|
| '180'
|
||||||
|
| '240'
|
||||||
|
| '1D';
|
||||||
|
|
||||||
|
class EmptyOrderBookError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'EmptyOrderBookError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a simpler more cex-style client with sensible (hopefully ;)) defaults
|
||||||
|
*/
|
||||||
|
export class SimpleClient {
|
||||||
|
private constructor(
|
||||||
|
private client: MangoClient,
|
||||||
|
private connection: Connection,
|
||||||
|
private programId: PublicKey,
|
||||||
|
private dexProgramId: PublicKey,
|
||||||
|
private mangoGroup: MangoGroup,
|
||||||
|
private markets: Market[],
|
||||||
|
private mangoGroupTokenMappings: Map<TokenSymbol, PublicKey>,
|
||||||
|
private spotMarketMappings: Map<SpotMarketSymbol, PublicKey>,
|
||||||
|
private payer: Account,
|
||||||
|
private marginAccountPk: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static async create(marginAccountPk: string) {
|
||||||
|
const cluster = process.env.CLUSTER || 'mainnet-beta';
|
||||||
|
const mangoGroupName = 'BTC_ETH_SOL_SRM_USDC';
|
||||||
|
|
||||||
|
const clusterIds = IDS[cluster];
|
||||||
|
const programId = new PublicKey(IDS[cluster].mango_program_id);
|
||||||
|
const dexProgramId = new PublicKey(clusterIds.dex_program_id);
|
||||||
|
const mangoGroupIds = clusterIds.mango_groups[mangoGroupName];
|
||||||
|
|
||||||
|
// connection
|
||||||
|
const connection = new Connection(
|
||||||
|
IDS.cluster_urls[cluster],
|
||||||
|
'processed' as Commitment,
|
||||||
|
);
|
||||||
|
|
||||||
|
// client
|
||||||
|
const client = new MangoClient();
|
||||||
|
|
||||||
|
// payer
|
||||||
|
const keyPairPath =
|
||||||
|
process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json';
|
||||||
|
const payer = new Account(
|
||||||
|
JSON.parse(fs.readFileSync(keyPairPath, 'utf-8')),
|
||||||
|
);
|
||||||
|
|
||||||
|
// mangoGroup
|
||||||
|
const mangoGroupPk = new PublicKey(
|
||||||
|
clusterIds.mango_groups[mangoGroupName].mango_group_pk,
|
||||||
|
);
|
||||||
|
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
|
||||||
|
|
||||||
|
// markets
|
||||||
|
const markets = await Promise.all(
|
||||||
|
mangoGroup.spotMarkets.map((pk) =>
|
||||||
|
Market.load(
|
||||||
|
connection,
|
||||||
|
pk,
|
||||||
|
{ skipPreflight: true, commitment: 'singleGossip' },
|
||||||
|
dexProgramId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// token mappings
|
||||||
|
const mangoGroupTokenMappings = new Map<TokenSymbol, PublicKey>();
|
||||||
|
const mangoGroupSymbols: [string, string][] = Object.entries(
|
||||||
|
mangoGroupIds.symbols,
|
||||||
|
);
|
||||||
|
for (const [tokenName, tokenMint] of mangoGroupSymbols) {
|
||||||
|
mangoGroupTokenMappings[tokenName] = new PublicKey(tokenMint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// market mappings
|
||||||
|
const mangoGroupSportMarketMappings = new Map<
|
||||||
|
SpotMarketSymbol,
|
||||||
|
PublicKey
|
||||||
|
>();
|
||||||
|
const mangoGroupSpotMarketSymbols: [SpotMarketSymbol, string][] =
|
||||||
|
Object.entries(mangoGroupIds.spot_market_symbols);
|
||||||
|
for (const [spotMarketSymbol, address] of mangoGroupSpotMarketSymbols) {
|
||||||
|
mangoGroupSportMarketMappings[spotMarketSymbol] = new PublicKey(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SimpleClient(
|
||||||
|
client,
|
||||||
|
connection,
|
||||||
|
programId,
|
||||||
|
dexProgramId,
|
||||||
|
mangoGroup,
|
||||||
|
markets,
|
||||||
|
mangoGroupTokenMappings,
|
||||||
|
mangoGroupSportMarketMappings,
|
||||||
|
payer,
|
||||||
|
marginAccountPk,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// private
|
||||||
|
|
||||||
|
private getMarketForSymbol(marketSymbol: SpotMarketSymbol): Market {
|
||||||
|
if (Object.keys(this.spotMarketMappings).indexOf(marketSymbol) === -1) {
|
||||||
|
throw new Error(`unknown spot market ${marketSymbol}`);
|
||||||
|
}
|
||||||
|
const marketAddress = this.spotMarketMappings[marketSymbol];
|
||||||
|
const market = this.markets.find((market) =>
|
||||||
|
market.publicKey.equals(marketAddress),
|
||||||
|
);
|
||||||
|
if (market === undefined) {
|
||||||
|
throw new Error(`market not found for ${market}`);
|
||||||
|
}
|
||||||
|
return market;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMarginAccountForOwner(): Promise<MarginAccount> {
|
||||||
|
return await this.client.getMarginAccount(
|
||||||
|
this.connection,
|
||||||
|
new PublicKey(this.marginAccountPk),
|
||||||
|
this.dexProgramId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOpenOrdersAccountForSymbol(
|
||||||
|
marketSymbol: SpotMarketSymbol,
|
||||||
|
): Promise<OpenOrders | undefined> {
|
||||||
|
const market = this.getMarketForSymbol(marketSymbol);
|
||||||
|
const marketIndex = this.mangoGroup.getMarketIndex(market!);
|
||||||
|
const marginAccount = await this.getMarginAccountForOwner();
|
||||||
|
return marginAccount.openOrdersAccounts[marketIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cancelOrder(
|
||||||
|
marginAccount: MarginAccount,
|
||||||
|
market: Market,
|
||||||
|
order: Order,
|
||||||
|
): Promise<TransactionSignature> {
|
||||||
|
return this.client.cancelOrder(
|
||||||
|
this.connection,
|
||||||
|
this.programId,
|
||||||
|
this.mangoGroup,
|
||||||
|
marginAccount,
|
||||||
|
this.payer,
|
||||||
|
market,
|
||||||
|
order,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cancelOrdersForMarginAccount(
|
||||||
|
marginAccount: MarginAccount,
|
||||||
|
symbol?: SpotMarketSymbol,
|
||||||
|
clientId?: string,
|
||||||
|
) {
|
||||||
|
let orders;
|
||||||
|
let market;
|
||||||
|
|
||||||
|
if (symbol === undefined) {
|
||||||
|
for (const spotMarketSymbol of Object.keys(this.spotMarketMappings)) {
|
||||||
|
market = this.getMarketForSymbol(spotMarketSymbol);
|
||||||
|
orders = await this.getOpenOrders(spotMarketSymbol);
|
||||||
|
await orders.map((order) =>
|
||||||
|
this.cancelOrder(marginAccount, market, order),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
market = this.getMarketForSymbol(symbol!);
|
||||||
|
orders = await this.getOpenOrders(symbol!);
|
||||||
|
// note: clientId could not even belong to his margin account
|
||||||
|
// in that case ordersToCancel would be empty
|
||||||
|
const ordersToCancel =
|
||||||
|
clientId !== undefined
|
||||||
|
? orders.filter((o) => o.clientId.toString() === clientId)
|
||||||
|
: orders;
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
ordersToCancel.map((order) =>
|
||||||
|
this.cancelOrder(marginAccount, market, order),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// public
|
||||||
|
|
||||||
|
async placeOrder(
|
||||||
|
symbol: SpotMarketSymbol,
|
||||||
|
type: 'market' | 'limit',
|
||||||
|
side: 'buy' | 'sell',
|
||||||
|
quantity: number,
|
||||||
|
price?: number,
|
||||||
|
orderType?: 'ioc' | 'postOnly' | 'limit',
|
||||||
|
): Promise<string> {
|
||||||
|
if (!symbol.trim()) {
|
||||||
|
throw new Error(`invalid symbol ${symbol}`);
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(quantity) || quantity <= 0) {
|
||||||
|
throw new Error(`invalid quantity ${quantity}`);
|
||||||
|
}
|
||||||
|
if ((type === 'limit' && !Number.isFinite(price)) || price! <= 0) {
|
||||||
|
throw new Error(`invalid price ${price}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'market') {
|
||||||
|
const orderBook = await this.getOrderBook(symbol);
|
||||||
|
let acc = 0;
|
||||||
|
let selectedOrder;
|
||||||
|
if (orderBook.length === 0) {
|
||||||
|
throw new EmptyOrderBookError(
|
||||||
|
'Empty order book encountered when placing a market order!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const order of orderBook) {
|
||||||
|
acc += order.size;
|
||||||
|
if (acc >= quantity) {
|
||||||
|
selectedOrder = order;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (side === 'buy') {
|
||||||
|
price = selectedOrder.price * 1.05;
|
||||||
|
} else {
|
||||||
|
price = selectedOrder.price * 0.95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const market = this.getMarketForSymbol(symbol);
|
||||||
|
|
||||||
|
const marginAccount = await this.getMarginAccountForOwner();
|
||||||
|
|
||||||
|
const clientId = new BN(Date.now());
|
||||||
|
|
||||||
|
orderType = orderType === undefined ? 'limit' : orderType;
|
||||||
|
|
||||||
|
await this.client.placeOrder(
|
||||||
|
this.connection,
|
||||||
|
this.programId,
|
||||||
|
this.mangoGroup,
|
||||||
|
marginAccount,
|
||||||
|
market,
|
||||||
|
this.payer,
|
||||||
|
side,
|
||||||
|
price!,
|
||||||
|
quantity,
|
||||||
|
orderType,
|
||||||
|
clientId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return clientId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOpenOrders(
|
||||||
|
symbol: SpotMarketSymbol,
|
||||||
|
clientId?: string,
|
||||||
|
): Promise<Order[]> {
|
||||||
|
const openOrderAccount = await this.getOpenOrdersAccountForSymbol(symbol);
|
||||||
|
if (openOrderAccount === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let orders: Order[] = await this.getOrderBook(symbol);
|
||||||
|
orders = orders.filter((o) =>
|
||||||
|
openOrderAccount.address.equals(o.openOrdersAddress),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clientId) {
|
||||||
|
return orders.filter(
|
||||||
|
(o) => o.clientId && o.clientId.toString() === clientId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrders(symbol?: SpotMarketSymbol, clientId?: string) {
|
||||||
|
const marginAccount = await this.getMarginAccountForOwner();
|
||||||
|
await this.cancelOrdersForMarginAccount(marginAccount, symbol, clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTradeHistory(symbol: SpotMarketSymbol): Promise<OpenOrderForPnl[]> {
|
||||||
|
if (!symbol.trim()) {
|
||||||
|
throw new Error(`invalid symbol ${symbol}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const openOrdersAccount = await this.getOpenOrdersAccountForSymbol(symbol);
|
||||||
|
if (openOrdersAccount === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g. https://stark-fjord-45757.herokuapp.com/trades/open_orders/G5rZ4Qfv5SxpJegVng5FuZftDrJkzLkxQUNjEXuoczX5
|
||||||
|
// {
|
||||||
|
// "id": 2267328,
|
||||||
|
// "loadTimestamp": "2021-04-28T03:36:20.573Z",
|
||||||
|
// "address": "C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4",
|
||||||
|
// "programId": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
|
||||||
|
// "baseCurrency": "BTC",
|
||||||
|
// "quoteCurrency": "USDT",
|
||||||
|
// "fill": true,
|
||||||
|
// "out": false,
|
||||||
|
// "bid": false,
|
||||||
|
// "maker": false,
|
||||||
|
// "openOrderSlot": "6",
|
||||||
|
// "feeTier": "0",
|
||||||
|
// "nativeQuantityReleased": "93207112",
|
||||||
|
// "nativeQuantityPaid": "1700",
|
||||||
|
// "nativeFeeOrRebate": "205508",
|
||||||
|
// "orderId": "9555524110645989995606320",
|
||||||
|
// "openOrders": "G5rZ4Qfv5SxpJegVng5FuZftDrJkzLkxQUNjEXuoczX5",
|
||||||
|
// "clientOrderId": "0",
|
||||||
|
// "uuid": "0040cdbdb0667fd5f75c2538e4097c5090e7b15d8cf9a5e7db7a54c3c212d27a",
|
||||||
|
// "source": "1",
|
||||||
|
// "baseTokenDecimals": 6,
|
||||||
|
// "quoteTokenDecimals": 6,
|
||||||
|
// "side": "sell",
|
||||||
|
// "price": 54948.6,
|
||||||
|
// "feeCost": 0.205508,
|
||||||
|
// "size": 0.0017
|
||||||
|
// }
|
||||||
|
const response = await fetch(
|
||||||
|
`https://stark-fjord-45757.herokuapp.com/trades/open_orders/${openOrdersAccount.address.toBase58()}`,
|
||||||
|
);
|
||||||
|
const parsedResponse = await response.json();
|
||||||
|
const trades: OpenOrderForPnl[] = parsedResponse?.data
|
||||||
|
? parsedResponse.data
|
||||||
|
: [];
|
||||||
|
return trades
|
||||||
|
.filter((trade) =>
|
||||||
|
openOrdersAccount.address.equals(new PublicKey(trade.openOrders)),
|
||||||
|
)
|
||||||
|
.map((trade) => ({ ...trade, marketName: symbol }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns balances, simple and mango specific details
|
||||||
|
*/
|
||||||
|
async getBalance(): Promise<Balance> {
|
||||||
|
const marginAccount = await this.getMarginAccountForOwner();
|
||||||
|
|
||||||
|
const marginAccountBalances = Object.keys(this.mangoGroupTokenMappings).map(
|
||||||
|
(tokenSymbol) => {
|
||||||
|
const tokenIndex = this.mangoGroup.getTokenIndex(
|
||||||
|
this.mangoGroupTokenMappings[tokenSymbol],
|
||||||
|
);
|
||||||
|
|
||||||
|
const decimals = this.mangoGroup.mintDecimals[tokenIndex];
|
||||||
|
const uiDeposit = marginAccount.getUiDeposit(
|
||||||
|
this.mangoGroup,
|
||||||
|
tokenIndex,
|
||||||
|
);
|
||||||
|
const uiDepositDisplay = ceilToDecimal(uiDeposit, decimals);
|
||||||
|
const uiBorrow = marginAccount.getUiBorrow(this.mangoGroup, tokenIndex);
|
||||||
|
const uiBorrowDisplay = ceilToDecimal(uiBorrow, decimals);
|
||||||
|
|
||||||
|
return new MarginAccountBalance(
|
||||||
|
tokenSymbol,
|
||||||
|
uiDepositDisplay,
|
||||||
|
uiBorrowDisplay,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const marketBalances = this.mangoGroup.spotMarkets.map((marketPk) => {
|
||||||
|
const market = this.markets.find((market) =>
|
||||||
|
market.publicKey.equals(marketPk),
|
||||||
|
);
|
||||||
|
if (market === undefined) {
|
||||||
|
throw new Error(`market for ${marketPk.toBase58()} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = Object.entries(this.mangoGroupTokenMappings).find(
|
||||||
|
(entry) => {
|
||||||
|
return entry[1].equals(market.baseMintAddress);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenIndex = this.mangoGroup.getTokenIndex(market.baseMintAddress);
|
||||||
|
const openOrders: OpenOrders =
|
||||||
|
marginAccount.openOrdersAccounts[tokenIndex]!;
|
||||||
|
const nativeBaseFree = openOrders?.baseTokenFree || new BN(0);
|
||||||
|
const nativeBaseLocked = openOrders
|
||||||
|
? openOrders.baseTokenTotal.sub(nativeBaseFree)
|
||||||
|
: new BN(0);
|
||||||
|
const nativeBaseUnsettled = openOrders?.baseTokenFree || new BN(0);
|
||||||
|
const orders = nativeToUi(
|
||||||
|
nativeBaseLocked.toNumber(),
|
||||||
|
this.mangoGroup.mintDecimals[tokenIndex],
|
||||||
|
);
|
||||||
|
const unsettled = nativeToUi(
|
||||||
|
nativeBaseUnsettled.toNumber(),
|
||||||
|
this.mangoGroup.mintDecimals[tokenIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
const quoteToken = Object.entries(this.mangoGroupTokenMappings).find(
|
||||||
|
(entry) => {
|
||||||
|
return entry[1].equals(market.quoteMintAddress);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const quoteCurrencyIndex = this.mangoGroup.getTokenIndex(
|
||||||
|
market.quoteMintAddress,
|
||||||
|
);
|
||||||
|
const nativeQuoteFree = openOrders?.quoteTokenFree || new BN(0);
|
||||||
|
const nativeQuoteLocked = openOrders
|
||||||
|
? openOrders!.quoteTokenTotal.sub(nativeQuoteFree)
|
||||||
|
: new BN(0);
|
||||||
|
const nativeQuoteUnsettled = openOrders?.quoteTokenFree || new BN(0);
|
||||||
|
const ordersQuote = nativeToUi(
|
||||||
|
nativeQuoteLocked.toNumber(),
|
||||||
|
this.mangoGroup.mintDecimals[quoteCurrencyIndex],
|
||||||
|
);
|
||||||
|
const unsettledQuote = nativeToUi(
|
||||||
|
nativeQuoteUnsettled.toNumber(),
|
||||||
|
this.mangoGroup.mintDecimals[quoteCurrencyIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
return new MarketBalance(
|
||||||
|
token![0],
|
||||||
|
orders,
|
||||||
|
unsettled,
|
||||||
|
quoteToken![0],
|
||||||
|
ordersQuote,
|
||||||
|
unsettledQuote,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Balance(
|
||||||
|
marginAccount.publicKey.toBase58(),
|
||||||
|
marginAccountBalances,
|
||||||
|
marketBalances,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPnl() {
|
||||||
|
// grab trade history
|
||||||
|
let tradeHistory: OpenOrderForPnl[] = [];
|
||||||
|
for (const [spotMarketSymbol, unused] of Object.entries(
|
||||||
|
this.spotMarketMappings,
|
||||||
|
)) {
|
||||||
|
const tradeHistoryForSymbol = await this.getTradeHistory(
|
||||||
|
spotMarketSymbol,
|
||||||
|
);
|
||||||
|
tradeHistory = tradeHistory.concat(tradeHistoryForSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profitAndLoss = {};
|
||||||
|
|
||||||
|
// compute profit and loss for all markets
|
||||||
|
const groupedTrades = groupBy(tradeHistory, (trade) => trade.marketName);
|
||||||
|
groupedTrades.forEach((val, key) => {
|
||||||
|
profitAndLoss[key] = val.reduce(
|
||||||
|
(acc, current) =>
|
||||||
|
(current.side === 'sell' ? current.size * -1 : current.size) + acc,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// compute profit and loss for usdc
|
||||||
|
const totalNativeUsdc = tradeHistory.reduce((acc, current) => {
|
||||||
|
const usdcAmount =
|
||||||
|
current.side === 'sell'
|
||||||
|
? current.nativeQuantityReleased
|
||||||
|
: current.nativeQuantityPaid * -1;
|
||||||
|
return usdcAmount + acc;
|
||||||
|
}, 0);
|
||||||
|
(profitAndLoss as any).USDC = nativeToUi(
|
||||||
|
totalNativeUsdc,
|
||||||
|
this.mangoGroup.mintDecimals[2],
|
||||||
|
);
|
||||||
|
|
||||||
|
// compute final pnl
|
||||||
|
let total = 0;
|
||||||
|
const prices = await this.mangoGroup.getPrices(this.connection);
|
||||||
|
const assetIndex = {
|
||||||
|
'BTC/USDC': 0,
|
||||||
|
'BTC/WUSDC': 0,
|
||||||
|
'ETH/USDC': 1,
|
||||||
|
'ETH/WUSDC': 1,
|
||||||
|
'SOL/USDC': 1,
|
||||||
|
'SOL/WUSDC': 1,
|
||||||
|
'SRM/USDC': 1,
|
||||||
|
'SRM/WUSDC': 1,
|
||||||
|
USDC: 2,
|
||||||
|
WUSDC: 2,
|
||||||
|
};
|
||||||
|
for (const assetName of Object.keys(profitAndLoss)) {
|
||||||
|
total = total + profitAndLoss[assetName] * prices[assetIndex[assetName]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return total.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns available markets
|
||||||
|
*/
|
||||||
|
async getMarkets(): Promise<FetchMarket> {
|
||||||
|
const fetchMarketSymbols = Object.keys(this.spotMarketMappings).map(
|
||||||
|
(spotMarketSymbol) => new FetchMarketSymbol(spotMarketSymbol),
|
||||||
|
);
|
||||||
|
return new FetchMarket(fetchMarketSymbols);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns tickers i.e. symbol, closing price, time of closing price
|
||||||
|
*/
|
||||||
|
async getTickers(symbol?: SpotMarketSymbol): Promise<Ticker[]> {
|
||||||
|
let ohlcvs;
|
||||||
|
let latestOhlcv;
|
||||||
|
|
||||||
|
const to = Date.now();
|
||||||
|
// use a sufficiently large window to ensure that we get data back
|
||||||
|
const toMinus20Mins = to - 20 * 60 * 1000;
|
||||||
|
const oneMinute = '1';
|
||||||
|
|
||||||
|
if (symbol === undefined) {
|
||||||
|
const tickers: Ticker[] = [];
|
||||||
|
for (const zymbol of Object.keys(this.spotMarketMappings)) {
|
||||||
|
ohlcvs = await this.getOhlcv(zymbol, oneMinute, toMinus20Mins, to);
|
||||||
|
latestOhlcv = ohlcvs[ohlcvs.length - 1];
|
||||||
|
tickers.push(
|
||||||
|
new Ticker(zymbol, latestOhlcv.close, latestOhlcv.timeS * 1000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return tickers;
|
||||||
|
}
|
||||||
|
|
||||||
|
ohlcvs = await this.getOhlcv(symbol, oneMinute, toMinus20Mins, to);
|
||||||
|
latestOhlcv = ohlcvs[ohlcvs.length - 1];
|
||||||
|
return [new Ticker(symbol, latestOhlcv.close, latestOhlcv.timeS * 1000)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrderBook(symbol: SpotMarketSymbol): Promise<Order[]> {
|
||||||
|
const market = this.getMarketForSymbol(symbol);
|
||||||
|
|
||||||
|
const bidData = (await this.connection.getAccountInfo(market.bidsAddress))
|
||||||
|
?.data;
|
||||||
|
const bidOrderBook = bidData
|
||||||
|
? Orderbook.decode(market, Buffer.from(bidData))
|
||||||
|
: [];
|
||||||
|
const askData = (await this.connection.getAccountInfo(market.asksAddress))
|
||||||
|
?.data;
|
||||||
|
const askOrderBook = askData
|
||||||
|
? Orderbook.decode(market, Buffer.from(askData))
|
||||||
|
: [];
|
||||||
|
return [...bidOrderBook, ...askOrderBook];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns ohlcv in ascending order for time
|
||||||
|
*/
|
||||||
|
async getOhlcv(
|
||||||
|
spotMarketSymbol: SpotMarketSymbol,
|
||||||
|
resolution: Resolution,
|
||||||
|
fromEpochMs: number,
|
||||||
|
toEpochMs: number,
|
||||||
|
): Promise<Ohlcv[]> {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://serum-history.herokuapp.com/tv/history` +
|
||||||
|
`?symbol=${spotMarketSymbol}&resolution=${resolution}` +
|
||||||
|
`&from=${fromEpochMs / 1000}&to=${toEpochMs / 1000}`,
|
||||||
|
);
|
||||||
|
const { t, o, h, l, c, v } = await response.json();
|
||||||
|
const ohlcvs: Ohlcv[] = [];
|
||||||
|
for (let i = 0; i < t.length; i++) {
|
||||||
|
ohlcvs.push(new Ohlcv(t[i], o[i], h[i], l[i], c[i], v[i]));
|
||||||
|
}
|
||||||
|
return ohlcvs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// async debug() {
|
||||||
|
// const marginAccountForOwner = await this.getMarginAccountForOwner();
|
||||||
|
// console.log(
|
||||||
|
// `margin account - ${marginAccountForOwner.publicKey.toBase58()}`,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const balance = await this.getBalance();
|
||||||
|
// balance.marginAccountBalances.map((bal) => {
|
||||||
|
// console.log(
|
||||||
|
// ` - balance for ${bal.tokenSymbol}, deposited ${bal.deposited}, borrowed ${bal.borrowed}`,
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// for (const symbol of Object.keys(this.spotMarketMappings)) {
|
||||||
|
// const openOrdersAccountForSymbol =
|
||||||
|
// await this.getOpenOrdersAccountForSymbol(symbol);
|
||||||
|
// if (openOrdersAccountForSymbol === undefined) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// console.log(
|
||||||
|
// ` - symbol ${symbol}, open orders account ${openOrdersAccountForSymbol?.publicKey.toBase58()}`,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// const openOrders = await this.getOpenOrders(symbol);
|
||||||
|
// if (openOrders === undefined) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// for (const order of openOrders) {
|
||||||
|
// console.log(` - orderId ${order.orderId}, clientId ${order.clientId}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
28
src/utils.ts
28
src/utils.ts
|
@ -126,7 +126,7 @@ export async function awaitTransactionSignatureConfirmation(
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// console.log('REST null result for', txid, result);
|
// console.log('REST null result for', txid, result);
|
||||||
} else if (result.err) {
|
} else if (result.err) {
|
||||||
console.log('REST error for', txid, result);
|
console.log('REST error for', txid, result.err);
|
||||||
done = true;
|
done = true;
|
||||||
reject(result.err);
|
reject(result.err);
|
||||||
} else if (!(result.confirmations || confirmLevels.includes(result.confirmationStatus))) {
|
} else if (!(result.confirmations || confirmLevels.includes(result.confirmationStatus))) {
|
||||||
|
@ -384,3 +384,29 @@ export function decodeRecentEvents(
|
||||||
|
|
||||||
return { header, nodes };
|
return { header, nodes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupBy(list, keyGetter) {
|
||||||
|
const map = new Map()
|
||||||
|
list.forEach((item) => {
|
||||||
|
const key = keyGetter(item)
|
||||||
|
const collection = map.get(key)
|
||||||
|
if (!collection) {
|
||||||
|
map.set(key, [item])
|
||||||
|
} else {
|
||||||
|
collection.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function ceilToDecimal(value: number, decimals: number | undefined | null) {
|
||||||
|
return decimals
|
||||||
|
? Math.ceil(value * 10 ** decimals) / 10 ** decimals
|
||||||
|
: Math.ceil(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
[250,253,108,223,245,245,148,32,18,101,60,90,40,180,51,28,249,204,1,97,208,157,54,7,97,16,14,74,203,208,123,80,32,70,167,192,19,246,135,63,173,62,228,103,149,244,108,120,196,226,78,63,164,107,250,88,118,206,50,193,213,137,249,82]
|
|
@ -0,0 +1,265 @@
|
||||||
|
import { Balance, SimpleClient } from '../src/simpleclient';
|
||||||
|
import { Account, Connection, PublicKey } from '@solana/web3.js';
|
||||||
|
import {
|
||||||
|
createMangoGroupSymbolMappings,
|
||||||
|
createTokenAccountWithBalance,
|
||||||
|
createWalletAndRequestAirdrop,
|
||||||
|
getOrderSizeAndPrice,
|
||||||
|
performSingleDepositOrWithdrawal,
|
||||||
|
} from './test_utils';
|
||||||
|
import { MangoClient } from '../src';
|
||||||
|
import IDS from '../src/ids.json';
|
||||||
|
import { Market } from '@project-serum/serum';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
import expect from 'expect';
|
||||||
|
|
||||||
|
process.env.CLUSTER = 'devnet';
|
||||||
|
process.env.KEYPAIR = __dirname + '/' + 'id.json.bak';
|
||||||
|
|
||||||
|
const marketSymbol = 'BTC/USDC';
|
||||||
|
const lowPriceThatDoesntTrigger = 1;
|
||||||
|
const marginAccountPk = 'tBhXVv9JVJL8tApoBxEcKEgs7Ngd1FgdYGwBTarN4Ux';
|
||||||
|
const keyFilePath =
|
||||||
|
process.env.KEYPAIR || os.homedir() + '/.config/solana/id.json';
|
||||||
|
|
||||||
|
describe('test simple client', async () => {
|
||||||
|
(!fs.existsSync(keyFilePath) ? it : it.skip)(
|
||||||
|
'boostrap wallet and funds for trading',
|
||||||
|
async () => {
|
||||||
|
async function getSpotMarketDetails(
|
||||||
|
mangoGroupSpotMarket: any,
|
||||||
|
): Promise<any> {
|
||||||
|
const [spotMarketSymbol, spotMarketAddress] = mangoGroupSpotMarket;
|
||||||
|
const [baseSymbol, quoteSymbol] = spotMarketSymbol.split('/');
|
||||||
|
const spotMarket = await Market.load(
|
||||||
|
connection,
|
||||||
|
new PublicKey(spotMarketAddress),
|
||||||
|
{ skipPreflight: true, commitment: 'singleGossip' },
|
||||||
|
dexProgramId,
|
||||||
|
);
|
||||||
|
return { spotMarket, baseSymbol, quoteSymbol };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. mango specific setup
|
||||||
|
const cluster = 'devnet';
|
||||||
|
const clusterIds = IDS[cluster];
|
||||||
|
const dexProgramId = new PublicKey(clusterIds.dex_program_id);
|
||||||
|
const connection = new Connection(
|
||||||
|
IDS.cluster_urls[cluster],
|
||||||
|
'singleGossip',
|
||||||
|
);
|
||||||
|
const client = new MangoClient();
|
||||||
|
const mangoProgramId = new PublicKey(clusterIds.mango_program_id);
|
||||||
|
const mangoGroupName = 'BTC_ETH_SOL_SRM_USDC';
|
||||||
|
const mangoGroupIds = clusterIds.mango_groups[mangoGroupName];
|
||||||
|
const mangoGroupSpotMarkets: [string, string][] = Object.entries(
|
||||||
|
mangoGroupIds.spot_market_symbols,
|
||||||
|
);
|
||||||
|
const mangoGroupPk = new PublicKey(mangoGroupIds.mango_group_pk);
|
||||||
|
const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk);
|
||||||
|
const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(
|
||||||
|
connection,
|
||||||
|
mangoGroupIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. create wallet
|
||||||
|
let owner = await createWalletAndRequestAirdrop(connection, 100);
|
||||||
|
fs.writeFileSync(keyFilePath, `[${owner.secretKey.toString()}]`);
|
||||||
|
owner = new Account(JSON.parse(fs.readFileSync(keyFilePath, 'utf-8')));
|
||||||
|
|
||||||
|
// 3. get margin accounts, if none exist then init one
|
||||||
|
const prices = await mangoGroup.getPrices(connection);
|
||||||
|
let marginAccounts = await client.getMarginAccountsForOwner(
|
||||||
|
connection,
|
||||||
|
mangoProgramId,
|
||||||
|
mangoGroup,
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
marginAccounts.sort((a, b) =>
|
||||||
|
a.computeValue(mangoGroup, prices) > b.computeValue(mangoGroup, prices)
|
||||||
|
? -1
|
||||||
|
: 1,
|
||||||
|
);
|
||||||
|
if (marginAccounts.length === 0) {
|
||||||
|
await client.initMarginAccount(
|
||||||
|
connection,
|
||||||
|
mangoProgramId,
|
||||||
|
mangoGroup,
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
marginAccounts = await client.getMarginAccountsForOwner(
|
||||||
|
connection,
|
||||||
|
mangoProgramId,
|
||||||
|
mangoGroup,
|
||||||
|
owner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. deposit usdc
|
||||||
|
await createTokenAccountWithBalance(
|
||||||
|
connection,
|
||||||
|
owner,
|
||||||
|
'USDC',
|
||||||
|
mangoGroupTokenMappings,
|
||||||
|
clusterIds.faucets,
|
||||||
|
100_000,
|
||||||
|
);
|
||||||
|
await performSingleDepositOrWithdrawal(
|
||||||
|
connection,
|
||||||
|
owner,
|
||||||
|
client,
|
||||||
|
mangoGroup,
|
||||||
|
mangoProgramId,
|
||||||
|
'USDC',
|
||||||
|
mangoGroupTokenMappings,
|
||||||
|
marginAccounts[0],
|
||||||
|
'deposit',
|
||||||
|
100_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. deposit base currency for each spot market
|
||||||
|
for (const mangoGroupSpotMarket of mangoGroupSpotMarkets) {
|
||||||
|
const { spotMarket, baseSymbol, quoteSymbol } =
|
||||||
|
await getSpotMarketDetails(mangoGroupSpotMarket);
|
||||||
|
const [orderSize, orderPrice, _] = await getOrderSizeAndPrice(
|
||||||
|
connection,
|
||||||
|
spotMarket,
|
||||||
|
mangoGroupTokenMappings,
|
||||||
|
baseSymbol,
|
||||||
|
quoteSymbol,
|
||||||
|
'buy',
|
||||||
|
);
|
||||||
|
const neededQuoteAmount = orderPrice * orderSize;
|
||||||
|
const neededQuoteAmountForAllTrades = neededQuoteAmount * 128;
|
||||||
|
await createTokenAccountWithBalance(
|
||||||
|
connection,
|
||||||
|
owner,
|
||||||
|
baseSymbol,
|
||||||
|
mangoGroupTokenMappings,
|
||||||
|
clusterIds.faucets,
|
||||||
|
neededQuoteAmountForAllTrades,
|
||||||
|
);
|
||||||
|
await performSingleDepositOrWithdrawal(
|
||||||
|
connection,
|
||||||
|
owner,
|
||||||
|
client,
|
||||||
|
mangoGroup,
|
||||||
|
mangoProgramId,
|
||||||
|
baseSymbol,
|
||||||
|
mangoGroupTokenMappings,
|
||||||
|
marginAccounts[0],
|
||||||
|
'deposit',
|
||||||
|
neededQuoteAmountForAllTrades,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function cleanup(sc: SimpleClient) {
|
||||||
|
await sc.cancelOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
(process.env.CLUSTER === 'devnet' ? it : it.skip)(
|
||||||
|
'clean slate for devnet',
|
||||||
|
async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
await cleanup(sc);
|
||||||
|
const orders = await sc.getOpenOrders(marketSymbol);
|
||||||
|
expect(orders.length).toBe(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('test place order', async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
|
||||||
|
const ordersBeforePlacement = await sc.getOpenOrders(marketSymbol);
|
||||||
|
expect(ordersBeforePlacement.length).toBe(0);
|
||||||
|
|
||||||
|
await sc.placeOrder(
|
||||||
|
marketSymbol,
|
||||||
|
'limit',
|
||||||
|
'buy',
|
||||||
|
0.0001,
|
||||||
|
lowPriceThatDoesntTrigger,
|
||||||
|
);
|
||||||
|
await sc.placeOrder(
|
||||||
|
marketSymbol,
|
||||||
|
'limit',
|
||||||
|
'buy',
|
||||||
|
0.0002,
|
||||||
|
lowPriceThatDoesntTrigger,
|
||||||
|
);
|
||||||
|
const ordersAfterPlacement = await sc.getOpenOrders(marketSymbol);
|
||||||
|
expect(ordersAfterPlacement.length).toBe(2);
|
||||||
|
|
||||||
|
await cleanup(sc);
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
it('test cancel a specific order', async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
const txClientId = await sc.placeOrder(
|
||||||
|
marketSymbol,
|
||||||
|
'limit',
|
||||||
|
'buy',
|
||||||
|
0.0001,
|
||||||
|
lowPriceThatDoesntTrigger,
|
||||||
|
);
|
||||||
|
const ordersBeforeCancellation = await sc.getOpenOrders(marketSymbol);
|
||||||
|
|
||||||
|
await sc.cancelOrders(marketSymbol, txClientId);
|
||||||
|
|
||||||
|
const ordersAfterCancellation = await sc.getOpenOrders(marketSymbol);
|
||||||
|
|
||||||
|
expect(ordersAfterCancellation.length).toBe(
|
||||||
|
ordersBeforeCancellation.length - 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test balances', async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
const balance: Balance = await sc.getBalance();
|
||||||
|
balance.marginAccountBalances.map((marginAccountBalance) => {
|
||||||
|
expect(marginAccountBalance.deposited).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(process.env.CLUSTER === 'mainnet-beta' ? it : it.skip)(
|
||||||
|
'test trade history',
|
||||||
|
async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
const tradeHistory = await sc.getTradeHistory(marketSymbol);
|
||||||
|
expect(tradeHistory.length).toBeGreaterThan(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
(process.env.CLUSTER === 'mainnet-beta' ? it : it.skip)(
|
||||||
|
'test tickers',
|
||||||
|
async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
const tickers = await sc.getTickers();
|
||||||
|
expect(tickers.length).toBeGreaterThan(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
(process.env.CLUSTER === 'mainnet-beta' ? it : it.skip)(
|
||||||
|
'test ohlcv',
|
||||||
|
async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
const to = Date.now();
|
||||||
|
const yday = to - 24 * 60 * 60 * 1000;
|
||||||
|
const ohlcvs = await sc.getOhlcv(marketSymbol, '1D', yday, to);
|
||||||
|
expect(ohlcvs.length).toBeGreaterThan(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
(process.env.CLUSTER === 'mainnet-beta' ? it : it.skip)(
|
||||||
|
'test pnl',
|
||||||
|
async () => {
|
||||||
|
const sc = await SimpleClient.create(marginAccountPk);
|
||||||
|
const pnl = await sc.getPnl();
|
||||||
|
expect(pnl).toBeGreaterThan(0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
|
@ -10,7 +10,10 @@
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"jsx": "react"
|
"jsx": "react",
|
||||||
|
"lib": [
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["./tests/**/*", "./src/**/*"],
|
"include": ["./tests/**/*", "./src/**/*"],
|
||||||
"exclude": ["./tests/**/*.test.js", "./src/**/*.test.js", "node_modules", "**/node_modules"]
|
"exclude": ["./tests/**/*.test.js", "./src/**/*.test.js", "node_modules", "**/node_modules"]
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"defaultSeverity": "error",
|
||||||
|
"extends": [
|
||||||
|
"tslint:recommended"
|
||||||
|
],
|
||||||
|
"jsRules": {},
|
||||||
|
"rules": {},
|
||||||
|
"rulesDirectory": []
|
||||||
|
}
|
Loading…
Reference in New Issue