Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2021-09-16 23:27:15 +02:00
parent 37fcaa8bcb
commit 44f3426ef4
37 changed files with 3460 additions and 1352 deletions

6
.gitignore vendored
View File

@ -1 +1,5 @@
node_modules
mango-service-v3/node_modules
mango-service-v3/run-docker.sh
mango-service-v3/run-local.sh
py/.venv
py/.idea

View File

@ -1,5 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run pretty-quick --staged
npm run lint

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,11 @@
FROM node:16
WORKDIR /usr
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . .
ENV PORT=3000
CMD [ "yarn", "dev" ]

View File

@ -0,0 +1,26 @@
# Pre-requisites
* expects your private key to be present in ~/.config/solana/id.json
# How to run while developing
* `yarn install`
* `PORT=3000 GROUP=mainnet.1 CLUSTER_URL=https://api.mainnet-beta.solana.com nodemon ./src/server.ts`, you probably want to choose a private RPC node instead of the `https://api.mainnet-beta.solana.com` where one quickly ends up with 429s
# How to run using docker
* `docker build . -t microwavedcola/mango-service-v3`
* `docker run -p 8080:3000 -e GROUP=mainnet.1 -e CLUSTER_URL=https://api.mainnet-beta.solana.com -v ~/.config:/root/.config microwavedcola/mango-service-v3`
# Todos
- missing endpoints
- funding rates?
- maker taker fees
- populate still undefined fields in various endpoints
- validation of smart contract based contraints in order placing endpoint
- todos sprinkled over code
- identify which endpoints are still slow
- when null vs when undefined as return field value,- doublecheck for every endpoint/dto
- how often to load/reload certain mango things e.g. account, cache, rootbanks, etc.?
- docker container docs
- integration with freqtrade and/or ccxt https://github.com/ccxt/ccxt/blob/master/js/ftx.js
- integration with tradingview or https://github.com/thibaultyou/tradingview-alerts-processor/blob/master/docs/2_Alerts.md & https://www.tradingview.com/support/solutions/43000529348-about-webhooks/
- cleanup tsconfig.json
- add pre commit tools e.g. husky/pre-commit for code formatting and linting

View File

@ -0,0 +1,34 @@
{
"name": "mango-service-v3",
"version": "0.0.1",
"description": "REST API for trading against mango markets",
"main": "./src/server.ts",
"scripts": {
"build": "tsc",
"dev": "ts-node ./src/server.ts",
"lint": "tslint -p tsconfig.json -c tslint.json"
},
"author": "microwavedcola1",
"license": "MIT",
"dependencies": {
"@blockworks-foundation/mango-client": "^3.0.13",
"big.js": "^6.1.1",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"express-validator": "^6.12.1",
"lodash": "^4.17.21",
"node-fetch": "^2.5.0",
"pino": "^6.13.2",
"pino-pretty": "^7.0.0"
},
"devDependencies": {
"@types/big.js": "^6.1.2",
"@types/express": "^4.17.13",
"@types/node-fetch": "^2.5.0",
"@types/pino": "^6.3.11",
"nodemon": "^2.0.12",
"prettier": "^2.3.2",
"ts-node": "^10.2.1",
"typescript": "^4.4.2"
}
}

View File

@ -1,7 +1,7 @@
import {
getMarketByPublicKey,
PerpMarket,
ZERO_BN,
ZERO_BN
} from "@blockworks-foundation/mango-client";
import BN from "bn.js";
import Controller from "controller.interface";
@ -9,35 +9,43 @@ import { NextFunction, Request, Response, Router } from "express";
import MangoSimpleClient from "mango.simple.client";
class AccountController implements Controller {
public path = "/positions";
public path = "/api/positions";
public router = Router();
constructor(public mangoMarkets: MangoSimpleClient) {
constructor(public mangoSimpleClient: MangoSimpleClient) {
this.initializeRoutes();
}
private initializeRoutes() {
this.router.get(this.path, this.getPositions);
// GET /positions
this.router.get(this.path, this.fetchPerpPositions);
}
private getPositions = async (
private fetchPerpPositions = async (
request: Request,
response: Response,
next: NextFunction
) => {
const groupConfig = this.mangoMarkets.mangoGroupConfig;
const mangoGroup = this.mangoMarkets.mangoGroup;
const mangoAccount = await this.mangoMarkets.mangoAccount.reload(
this.mangoMarkets.connection,
this.mangoMarkets.mangoGroup.dexProgramId
);
const mangoCache = await this.mangoMarkets.mangoGroup.loadCache(
this.mangoMarkets.connection
);
const groupConfig = this.mangoSimpleClient.mangoGroupConfig;
const mangoGroup = this.mangoSimpleClient.mangoGroup;
const allMarkets = await this.mangoMarkets.getAllMarkets();
const mangoAccountFills = await this.mangoMarkets.getAllFills(true);
// (re)load+fetch things
const [mangoAccount, mangoCache, allMarkets, mangoAccountPerpFills] =
await Promise.all([
// a new perp account might have been created since the last fetch
this.mangoSimpleClient.mangoAccount.reload(
this.mangoSimpleClient.connection,
this.mangoSimpleClient.mangoGroup.dexProgramId
),
// in-order to use the fresh'est price
this.mangoSimpleClient.mangoGroup.loadCache(
this.mangoSimpleClient.connection
),
this.mangoSimpleClient.fetchAllMarkets(),
this.mangoSimpleClient.fetchAllPerpFills(),
]);
// find perp accounts with non zero positions
const perpAccounts = mangoAccount
? groupConfig.perpMarkets.map((m) => {
return {
@ -50,10 +58,11 @@ class AccountController implements Controller {
({ perpAccount }) => !perpAccount.basePosition.eq(new BN(0))
);
// compute perp position details
const postionDtos = filteredPerpAccounts.map(
({ perpAccount, marketIndex }, index) => {
const perpMarketInfo =
this.mangoMarkets.mangoGroup.perpMarkets[marketIndex];
this.mangoSimpleClient.mangoGroup.perpMarkets[marketIndex];
const marketConfig = getMarketByPublicKey(
groupConfig,
perpMarketInfo.perpMarket
@ -61,8 +70,8 @@ class AccountController implements Controller {
const perpMarket = allMarkets[
perpMarketInfo.perpMarket.toBase58()
] as PerpMarket;
const perpTradeHistory = mangoAccountFills.filter(
(t) => t.marketName === marketConfig.name
const perpTradeHistory = mangoAccountPerpFills.filter(
(t) => t.address === marketConfig.publicKey.toBase58()
);
let breakEvenPrice;
@ -79,10 +88,10 @@ class AccountController implements Controller {
const pnl =
breakEvenPrice !== null
? perpMarket.baseLotsToNumber(perpAccount.basePosition) *
(this.mangoMarkets.mangoGroup
(this.mangoSimpleClient.mangoGroup
.getPrice(marketIndex, mangoCache)
.toNumber() -
parseFloat(breakEvenPrice))
parseFloat(breakEvenPrice.toString()))
: null;
let entryPrice;
@ -103,18 +112,18 @@ class AccountController implements Controller {
),
cumulativeBuySize: undefined,
cumulativeSellSize: undefined,
entryPrice: entryPrice,
entryPrice,
estimatedLiquidationPrice: undefined,
future: marketConfig.baseSymbol,
future: marketConfig.name,
initialMarginRequirement: undefined,
longOrderSize: undefined,
maintenanceMarginRequirement: undefined,
netSize: undefined,
netSize: perpMarket.baseLotsToNumber(perpAccount.basePosition),
openSize: undefined,
realizedPnl: undefined,
recentAverageOpenPrice: undefined,
recentBreakEvenPrice: breakEvenPrice,
recentPnl: pnl,
recentBreakEvenPrice: breakEvenPrice!=null?breakEvenPrice.toNumber():null,
recentPnl: undefined,
shortOrderSize: undefined,
side: perpAccount.basePosition.gt(ZERO_BN) ? "long" : "short",
size: Math.abs(perpMarket.baseLotsToNumber(perpAccount.basePosition)),

View File

@ -1,30 +1,32 @@
import bodyParser from "body-parser";
import Controller from "controller.interface";
import express from "express";
import AccountController from "./account.controller";
import CoinController from "./coin.controller";
import MangoSimpleClient from "./mango.simple.client";
import MarketsController from "./markets.controller";
import OrdersController from "./orders.controller";
import WalletController from "./wallet.controller";
import Controller from "controller.interface";
import express from "express";
import log from "loglevel";
const bodyParser = require("body-parser");
class App {
public app: express.Application;
public mangoMarkets: MangoSimpleClient;
public mangoSimpleClient: MangoSimpleClient;
constructor() {
this.app = express();
MangoSimpleClient.create().then((mangoSimpleClient) => {
this.mangoMarkets = mangoSimpleClient;
this.mangoSimpleClient = mangoSimpleClient;
this.app.use(bodyParser.json({ limit: "50mb" }));
this.initializeControllers([
new WalletController(this.mangoMarkets),
new OrdersController(this.mangoMarkets),
new MarketsController(this.mangoMarkets),
new AccountController(this.mangoMarkets),
new CoinController(this.mangoSimpleClient),
new WalletController(this.mangoSimpleClient),
new OrdersController(this.mangoSimpleClient),
new MarketsController(this.mangoSimpleClient),
new AccountController(this.mangoSimpleClient),
]);
});
}
@ -36,9 +38,7 @@ class App {
}
public listen() {
this.app.listen(process.env.PORT, () => {
log.info(`App listening on the port ${process.env.PORT}`);
});
this.app.listen(process.env.PORT || 3000);
}
public getServer() {

View File

@ -0,0 +1,66 @@
import Controller from "controller.interface";
import { NextFunction, Request, Response, Router } from "express";
import MangoSimpleClient from "mango.simple.client";
class CoinController implements Controller {
public path = "/api/coins";
public router = Router();
constructor(public mangoSimpleClient: MangoSimpleClient) {
this.initializeRoutes();
}
private initializeRoutes() {
// GET /coins
this.router.get(this.path, this.getCoins);
}
private getCoins = async (
request: Request,
response: Response,
next: NextFunction
) => {
const coinDtos = this.mangoSimpleClient.mangoGroupConfig.tokens.map(
(tokenConfig) => {
return {
name: tokenConfig.symbol, // todo add full name to tokencofig
id: tokenConfig.symbol,
} as CoinDto;
}
);
response.send({ success: true, result: coinDtos } as CoinsDto);
};
}
export default CoinController;
/// Dtos
// e.g.
// {
// "success": true,
// "result": [
// {
// "name": "Bitcoin",
// "id": "BTC"
// },
// {
// "name": "Ethereum",
// "id": "ETH"
// },
// {
// "name": "Tether",
// "id": "USDT"
// },
// ]
// }
interface CoinsDto {
success: true;
result: CoinDto[];
}
interface CoinDto {
name: string;
id: string;
}

View File

@ -0,0 +1,10 @@
export interface BadParamError {
value: string;
msg: string;
param: string;
location: string;
}
export interface BadRequestError {
msg: string;
}

View File

@ -1,26 +1,32 @@
import { zipDict } from "./utils";
import { logger, zipDict } from "./utils";
import {
BookSide,
BookSideLayout,
Config,
getAllMarkets,
getMarketByBaseSymbolAndKind,
getMarketByPublicKey,
getMultipleAccounts,
GroupConfig,
MangoAccount,
MangoClient,
MangoGroup,
PerpMarketLayout,
MarketConfig,
MangoAccount,
PerpMarket,
BookSide,
BookSideLayout,
PerpMarketLayout,
PerpOrder,
getMarketByBaseSymbolAndKind,
} from "@blockworks-foundation/mango-client";
import { Market, Orderbook } from "@project-serum/serum";
import { Order } from "@project-serum/serum/lib/market";
import { Account, Commitment, Connection } from "@solana/web3.js";
import { AccountInfo, PublicKey } from "@solana/web3.js";
import {
Account,
AccountInfo,
Commitment,
Connection,
PublicKey,
} from "@solana/web3.js";
import fs from "fs";
import fetch from "node-fetch";
import os from "os";
import { OrderInfo } from "types";
@ -29,49 +35,65 @@ class MangoSimpleClient {
public mangoGroupConfig: GroupConfig,
public connection: Connection,
public client: MangoClient,
// todo: load/reload/cache markets often
public mangoGroup: MangoGroup,
public owner: Account,
// todo: load/reload/cache mangoAccount and various load* methods often
public mangoAccount: MangoAccount
) {}
) {
}
static async create() {
const groupName = process.env.GROUP || "devnet.1";
const groupName = process.env.GROUP || "mainnet.1";
const clusterUrl =
process.env.CLUSTER_URL || "https://api.mainnet-beta.solana.com";
logger.info(`Creating mango client for ${groupName} using ${clusterUrl}`);
const mangoGroupConfig: GroupConfig = Config.ids().groups.filter(
(group) => group.name == groupName
(group) => group.name === groupName
)[0];
const connection = new Connection(
process.env.CLUSTER_URL || "https://api.devnet.solana.com",
"processed" as Commitment
);
const connection = new Connection(clusterUrl, "processed" as Commitment);
const mangoClient = new MangoClient(
connection,
mangoGroupConfig.mangoProgramId
);
logger.info(`- fetching mango group`);
const mangoGroup = await mangoClient.getMangoGroup(
mangoGroupConfig.publicKey
);
const user = new Account(
logger.info(`- loading root banks`);
await mangoGroup.loadRootBanks(connection);
logger.info(`- loading cache`);
await mangoGroup.loadCache(connection);
const owner = new Account(
JSON.parse(
process.env.KEYPAIR ||
fs.readFileSync(
os.homedir() + "/.config/solana/mainnet.json",
"utf-8"
)
fs.readFileSync(os.homedir() + "/.config/solana/id.json", "utf-8")
)
);
const mangoAccounts = await mangoClient.getMangoAccountsForOwner(
mangoGroup,
user.publicKey
);
logger.info(`- fetching mango accounts for ${owner.publicKey.toBase58()}`);
let mangoAccounts;
try {
mangoAccounts = await mangoClient.getMangoAccountsForOwner(
mangoGroup,
owner.publicKey
);
} catch (error) {
logger.error(
`- error retrieving mango accounts for ${owner.publicKey.toBase58()}`
);
process.exit(1);
}
if (!mangoAccounts.length) {
throw new Error(`No mango account found ${user.publicKey.toBase58()}`);
logger.error(`- no mango account found ${owner.publicKey.toBase58()}`);
process.exit(1);
}
const sortedMangoAccounts = mangoAccounts
@ -80,28 +102,35 @@ class MangoSimpleClient {
a.publicKey.toBase58() > b.publicKey.toBase58() ? 1 : -1
);
const chosenMangoAccount = sortedMangoAccounts[0];
const debugAccounts = sortedMangoAccounts
.map((mangoAccount) => mangoAccount.publicKey.toBase58())
.join(", ");
logger.info(
`- found mango accounts ${debugAccounts}, using ${chosenMangoAccount.publicKey.toBase58()}`
);
return new MangoSimpleClient(
// todo: these things might get stale as time goes
mangoGroupConfig,
connection,
mangoClient,
mangoGroup,
user,
sortedMangoAccounts[0]
owner,
chosenMangoAccount
);
}
///
/// public
public async getAllMarkets(
onlyMarket?: string
public async fetchAllMarkets(
marketName?: string
): Promise<Partial<Record<string, Market | PerpMarket>>> {
let allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
let allMarketPks = allMarketConfigs.map((m) => m.publicKey);
if (onlyMarket !== undefined) {
if (marketName !== undefined) {
allMarketConfigs = allMarketConfigs.filter(
(marketConfig) => marketConfig.name === onlyMarket
(marketConfig) => marketConfig.name === marketName
);
allMarketPks = allMarketConfigs.map((m) => m.publicKey);
}
@ -112,7 +141,7 @@ class MangoSimpleClient {
);
const allMarketAccounts = allMarketConfigs.map((config, i) => {
if (config.kind == "spot") {
if (config.kind === "spot") {
const decoded = Market.getLayout(
this.mangoGroupConfig.mangoProgramId
).decode(allMarketAccountInfos[i].accountInfo.data);
@ -124,7 +153,7 @@ class MangoSimpleClient {
this.mangoGroupConfig.serumProgramId
);
}
if (config.kind == "perp") {
if (config.kind === "perp") {
const decoded = PerpMarketLayout.decode(
allMarketAccountInfos[i].accountInfo.data
);
@ -143,9 +172,9 @@ class MangoSimpleClient {
);
}
public async getAllBidsAndAsks(
public async fetchAllBidsAndAsks(
filterForMangoAccount: boolean = false,
onlyMarket?: string
marketName?: string
): Promise<OrderInfo[][]> {
this.mangoAccount.loadOpenOrders(
this.connection,
@ -155,9 +184,9 @@ class MangoSimpleClient {
let allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
let allMarketPks = allMarketConfigs.map((m) => m.publicKey);
if (onlyMarket !== undefined) {
if (marketName !== undefined) {
allMarketConfigs = allMarketConfigs.filter(
(marketConfig) => marketConfig.name === onlyMarket
(marketConfig) => marketConfig.name === marketName
);
allMarketPks = allMarketConfigs.map((m) => m.publicKey);
}
@ -177,7 +206,7 @@ class MangoSimpleClient {
}
);
const markets = await this.getAllMarkets(onlyMarket);
const markets = await this.fetchAllMarkets(marketName);
return Object.entries(markets).map(([address, market]) => {
const marketConfig = getMarketByPublicKey(this.mangoGroupConfig, address);
@ -199,51 +228,110 @@ class MangoSimpleClient {
});
}
public async getAllFills(
filterForMangoAccount: boolean = false
): Promise<any[]> {
let allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
const allMarkets = await this.getAllMarkets();
public async fetchAllSpotFills(): Promise<any[]> {
const allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
const allMarkets = await this.fetchAllMarkets();
let mangoAccountFills: any[] = [];
// merge
// 1. latest fills from on-chain
let allRecentMangoAccountSpotFills: any[] = [];
// 2. historic from off-chain REST service
let allButRecentMangoAccountSpotFills: any[] = [];
allMarketConfigs.map((config, i) => {
if (config.kind == "spot") {
for (const config of allMarketConfigs) {
if (config.kind === "spot") {
const openOrdersAccount =
this.mangoAccount.spotOpenOrdersAccounts[config.marketIndex];
const mangoAccountFills_ = allMarkets[config.publicKey.toBase58()]
// todo: what if we want to fetch the 10001 position?
if (openOrdersAccount === undefined) {
continue;
}
const response = await fetch(
`https://event-history-api.herokuapp.com/trades/open_orders/${openOrdersAccount.publicKey.toBase58()}`
);
const responseJson = await response.json();
allButRecentMangoAccountSpotFills =
allButRecentMangoAccountSpotFills.concat(
responseJson?.data ? responseJson.data : []
);
const recentMangoAccountSpotFills: any[] = await allMarkets[
config.publicKey.toBase58()
]
.loadFills(this.connection, 10000)
.then((fills) => {
if (filterForMangoAccount) {
fills = fills.filter((fill) => {
return openOrdersAccount?.publicKey
? fill.openOrders.equals(openOrdersAccount?.publicKey)
: false;
});
}
fills = fills.filter((fill) => {
return openOrdersAccount?.publicKey
? fill.openOrders.equals(openOrdersAccount?.publicKey)
: false;
});
return fills.map((fill) => ({ ...fill, marketName: config.name }));
});
mangoAccountFills = mangoAccountFills.concat(mangoAccountFills_);
allRecentMangoAccountSpotFills = allRecentMangoAccountSpotFills.concat(
recentMangoAccountSpotFills
);
}
if (config.kind == "perp") {
const mangoAccountFills_ = allMarkets[config.publicKey.toBase58()]
}
const newMangoAccountSpotFills = allRecentMangoAccountSpotFills.filter(
(fill: any) =>
!allButRecentMangoAccountSpotFills.flat().find((t: any) => {
if (t.orderId) {
return t.orderId === fill.orderId?.toString();
} else {
return t.seqNum === fill.seqNum?.toString();
}
})
);
return [...newMangoAccountSpotFills, ...allButRecentMangoAccountSpotFills];
}
public async fetchAllPerpFills(): Promise<any[]> {
const allMarketConfigs = getAllMarkets(this.mangoGroupConfig);
const allMarkets = await this.fetchAllMarkets();
// merge
// 1. latest fills from on-chain
let allRecentMangoAccountPerpFills: any[] = [];
// 2. historic from off-chain REST service
const response = await fetch(
`https://event-history-api.herokuapp.com/perp_trades/${this.mangoAccount.publicKey.toBase58()}`
);
const responseJson = await response.json();
const allButRecentMangoAccountPerpFills = responseJson?.data || [];
for (const config of allMarketConfigs) {
if (config.kind === "perp") {
const recentMangoAccountPerpFills: any[] = await allMarkets[
config.publicKey.toBase58()
]
.loadFills(this.connection)
.then((fills) => {
if (filterForMangoAccount) {
fills = fills.filter(
(fill) =>
fill.taker.equals(this.mangoAccount.publicKey) ||
fill.maker.equals(this.mangoAccount.publicKey)
);
}
fills = fills.filter(
(fill) =>
fill.taker.equals(this.mangoAccount.publicKey) ||
fill.maker.equals(this.mangoAccount.publicKey)
);
return fills.map((fill) => ({ ...fill, marketName: config.name }));
});
mangoAccountFills = mangoAccountFills.concat(mangoAccountFills_);
}
});
return mangoAccountFills;
allRecentMangoAccountPerpFills = allRecentMangoAccountPerpFills.concat(
recentMangoAccountPerpFills
);
}
}
const newMangoAccountPerpFills = allRecentMangoAccountPerpFills.filter(
(fill: any) =>
!allButRecentMangoAccountPerpFills.flat().find((t: any) => {
if (t.orderId) {
return t.orderId === fill.orderId?.toString();
} else {
return t.seqNum === fill.seqNum?.toString();
}
})
);
return [...newMangoAccountPerpFills, ...allButRecentMangoAccountPerpFills];
}
public async placeOrder(
@ -252,16 +340,18 @@ class MangoSimpleClient {
side: "buy" | "sell",
quantity: number,
price?: number,
orderType: "ioc" | "postOnly" | "limit" = "limit"
orderType: "ioc" | "postOnly" | "limit" = "limit",
clientOrderId?: number
): Promise<void> {
if (type === "market") {
// todo
throw new Error("Not implemented!");
}
if (market.includes("PERP")) {
const perpMarketConfig = getMarketByBaseSymbolAndKind(
this.mangoGroupConfig,
market.split("/")[0],
market.split("-")[0],
"perp"
);
const perpMarket = await this.mangoGroup.loadPerpMarket(
@ -279,7 +369,8 @@ class MangoSimpleClient {
side,
price,
quantity,
orderType
orderType,
clientOrderId
);
} else {
const spotMarketConfig = getMarketByBaseSymbolAndKind(
@ -308,42 +399,43 @@ class MangoSimpleClient {
}
public async cancelAllOrders(): Promise<void> {
const orders = await (await this.getAllBidsAndAsks(true)).flat();
// todo: this would fetch a market for every call, cache markets
const orderInfo = Promise.all(
orders.map((orderInfo) => this.cancelOrder(orderInfo))
const allMarkets = await this.fetchAllMarkets();
const orders = (await this.fetchAllBidsAndAsks(true)).flat();
// todo combine multiple cancels into one transaction
await Promise.all(
orders.map((orderInfo) =>
this.cancelOrder(
orderInfo,
allMarkets[orderInfo.market.account.publicKey.toBase58()]
)
)
);
}
public async cancelOrderByOrderId(orderId: string): Promise<void> {
const orders = await (await this.getAllBidsAndAsks(true)).flat();
const orderInfo = orders.filter(
(orderInfo) => orderInfo.order.orderId.toNumber().toString() === orderId
)[0];
await this.cancelOrder(orderInfo);
public async getOrderByOrderId(orderId: string): Promise<OrderInfo[]> {
const orders = (await this.fetchAllBidsAndAsks(true)).flat();
const orderInfos = orders.filter(
(orderInfo) => orderInfo.order.orderId.toString() === orderId
);
return orderInfos;
}
public async cancelOrderByClientId(clientId: string): Promise<void> {
const orders = await (await this.getAllBidsAndAsks(true)).flat();
const orderInfo = orders.filter(
public async getOrderByClientId(clientId: string): Promise<OrderInfo[]> {
const orders = await (await this.fetchAllBidsAndAsks(true)).flat();
const orderInfos = orders.filter(
(orderInfo) => orderInfo.order.clientId.toNumber().toString() === clientId
)[0];
await this.cancelOrder(orderInfo);
);
return orderInfos;
}
///
/// private
parseSpotOrders(
private parseSpotOrders(
market: Market,
config: MarketConfig,
accountInfos: { [key: string]: AccountInfo<Buffer> },
mangoAccount?: MangoAccount
): OrderInfo[] {
const openOrders = mangoAccount.spotOpenOrdersAccounts[config.marketIndex];
if (!openOrders) return [];
const bidData = accountInfos[market["_decoded"].bids.toBase58()]?.data;
const askData = accountInfos[market["_decoded"].asks.toBase58()]?.data;
@ -354,6 +446,9 @@ class MangoSimpleClient {
let openOrdersForMarket = [...bidOrderBook, ...askOrderBook];
if (mangoAccount !== undefined) {
const openOrders =
mangoAccount.spotOpenOrdersAccounts[config.marketIndex];
if (!openOrders) return [];
openOrdersForMarket = openOrdersForMarket.filter((o) =>
o.openOrdersAddress.equals(openOrders.address)
);
@ -361,11 +456,11 @@ class MangoSimpleClient {
return openOrdersForMarket.map<OrderInfo>((order) => ({
order,
market: { account: market, config: config },
market: { account: market, config },
}));
}
parsePerpOpenOrders(
private parsePerpOpenOrders(
market: PerpMarket,
config: MarketConfig,
accountInfos: { [key: string]: AccountInfo<Buffer> },
@ -392,28 +487,30 @@ class MangoSimpleClient {
return openOrdersForMarket.map<OrderInfo>((order) => ({
order,
market: { account: market, config: config },
market: { account: market, config },
}));
}
async cancelOrder(orderInfo: OrderInfo) {
public async cancelOrder(orderInfo: OrderInfo, market?: Market | PerpMarket) {
if (orderInfo.market.config.kind === "perp") {
const perpMarketConfig = getMarketByBaseSymbolAndKind(
this.mangoGroupConfig,
orderInfo.market.config.baseSymbol,
"perp"
);
const perpMarket = await this.mangoGroup.loadPerpMarket(
this.connection,
perpMarketConfig.marketIndex,
perpMarketConfig.baseDecimals,
perpMarketConfig.quoteDecimals
);
if (market === undefined) {
market = await this.mangoGroup.loadPerpMarket(
this.connection,
perpMarketConfig.marketIndex,
perpMarketConfig.baseDecimals,
perpMarketConfig.quoteDecimals
);
}
await this.client.cancelPerpOrder(
this.mangoGroup,
this.mangoAccount,
this.owner,
perpMarket,
market as PerpMarket,
orderInfo.order as PerpOrder
);
} else {
@ -422,17 +519,19 @@ class MangoSimpleClient {
orderInfo.market.config.baseSymbol,
"spot"
);
const spotMarket = await Market.load(
this.connection,
spotMarketConfig.publicKey,
undefined,
this.mangoGroupConfig.serumProgramId
);
if (market === undefined) {
market = await Market.load(
this.connection,
spotMarketConfig.publicKey,
undefined,
this.mangoGroupConfig.serumProgramId
);
}
await this.client.cancelSpotOrder(
this.mangoGroup,
this.mangoAccount,
this.owner,
spotMarket,
market as Market,
orderInfo.order as Order
);
}

View File

@ -0,0 +1,549 @@
import {
getAllMarkets,
getTokenBySymbol,
MarketConfig,
PerpMarket
} from "@blockworks-foundation/mango-client";
import { Market } from "@project-serum/serum";
import Big from "big.js";
import { BadParamError } from "dtos";
import { NextFunction, Request, Response, Router } from "express";
import { param, query, validationResult } from "express-validator";
import fetch from "node-fetch";
import { OrderInfo } from "types";
import Controller from "./controller.interface";
import MangoSimpleClient from "./mango.simple.client";
import { isValidMarket } from "./utils";
class MarketsController implements Controller {
public path = "/api/markets";
public router = Router();
constructor(public mangoSimpleClient: MangoSimpleClient) {
this.initializeRoutes();
}
private initializeRoutes() {
// GET /markets
// todo fetch vs get naming
this.router.get(this.path, this.fetchMarkets);
// GET /markets/{market_name}
// todo fetch or get as prefix for methods?
this.router.get(
`${this.path}/:market_name`,
param("market_name").custom(isValidMarket),
this.fetchMarket
);
// GET /markets/{market_name}/orderbook?depth={depth}
this.router.get(
`${this.path}/:market_name/orderbook`,
param("market_name").custom(isValidMarket),
query("depth", "Depth should be a number between 20 and 100!")
.optional()
.isInt({ max: 100, min: 20 }),
this.getOrderBook
);
// GET /markets/{market_name}/trades
this.router.get(
`${this.path}/:market_name/trades`,
param("market_name").custom(isValidMarket),
this.getTrades
);
// GET /markets/{market_name}/candles?resolution={resolution}&start_time={start_time}&end_time={end_time}
this.router.get(
`${this.path}/:market_name/candles`,
param("market_name").custom(isValidMarket),
this.getCandles
);
}
private fetchMarkets = async (
request: Request,
response: Response,
next: NextFunction
) => {
response.send({
success: true,
result: await this.fetchMarketsInternal(),
} as MarketsDto);
};
private fetchMarket = async (
request: Request,
response: Response,
next: NextFunction
) => {
const errors = validationResult(request);
if (!errors.isEmpty()) {
return response.status(400).json({ errors: errors.array() as BadParamError[] });
}
const marketName = request.params.market_name;
response.send({
success: true,
result: await this.fetchMarketsInternal(marketName),
} as MarketsDto);
};
private async fetchMarketsInternal(
marketName?: string
): Promise<MarketDto[]> {
let allMarketConfigs = getAllMarkets(
this.mangoSimpleClient.mangoGroupConfig
);
if (marketName !== undefined) {
allMarketConfigs = allMarketConfigs.filter(
(marketConfig) => marketConfig.name === marketName
);
}
const allMarkets = await this.mangoSimpleClient.fetchAllMarkets(marketName);
return Promise.all(
allMarketConfigs.map((marketConfig) =>
this.computeMarketLatestDetails(marketConfig, allMarkets)
)
);
}
private async computeMarketLatestDetails(
marketConfig: MarketConfig,
allMarkets: Partial<Record<string, Market | PerpMarket>>
): Promise<MarketDto> {
const market = allMarkets[marketConfig.publicKey.toBase58()];
const [
volume,
change1h,
change24h,
changeBod,
ordersInfo, // used for latest bid+ask
tradesResponse, // used for latest trade+price
] = await Promise.all([
getVolumeForMarket(marketConfig),
getChange1H(marketConfig),
getChange24H(marketConfig),
getChangeBod(marketConfig),
(await this.mangoSimpleClient.fetchAllBidsAndAsks(
false,
marketConfig.name
)) as OrderInfo[][],
fetch(
// todo: replace toBase58 with toString() everywhere
`https://serum-history.herokuapp.com/trades/address/${marketConfig.publicKey.toBase58()}`
),
]);
// latest bid+ask
const bids = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side === "buy")
.sort((b1, b2) => b2.order.price - b1.order.price);
const asks = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side === "sell")
.sort((a1, a2) => a1.order.price - a2.order.price);
// latest trade+price
const parsedTradesResponse = (await tradesResponse.json()) as any;
let lastPrice;
if ("s" in parsedTradesResponse && parsedTradesResponse["s"] === "error") {
lastPrice = null;
} else {
lastPrice = parsedTradesResponse["data"][0]["price"];
}
// size increments
let minOrderSize;
if (market instanceof Market && market.minOrderSize) {
minOrderSize = market.minOrderSize;
} else if (market instanceof PerpMarket) {
const token = getTokenBySymbol(
this.mangoSimpleClient.mangoGroupConfig,
marketConfig.baseSymbol
);
minOrderSize = new Big(market.baseLotSize.toString())
.div(new Big(10).pow(token.decimals))
.toNumber();
}
// price increment
let tickSize = 1;
if (market instanceof Market) {
tickSize = market.tickSize;
} else if (market instanceof PerpMarket) {
const baseDecimals = getTokenBySymbol(
this.mangoSimpleClient.mangoGroupConfig,
marketConfig.baseSymbol
).decimals;
const quoteDecimals = getTokenBySymbol(
this.mangoSimpleClient.mangoGroupConfig,
this.mangoSimpleClient.mangoGroupConfig.quoteSymbol
).decimals;
const nativeToUi = new Big(10).pow(baseDecimals - quoteDecimals);
const lotsToNative = new Big(market.quoteLotSize.toString()).div(
new Big(market.baseLotSize.toString())
);
tickSize = lotsToNative.mul(nativeToUi).toNumber();
}
return {
name: marketConfig.name,
baseCurrency: marketConfig.baseSymbol,
quoteCurrency: "USDC",
quoteVolume24h: volume,
change1h,
change24h,
changeBod,
highLeverageFeeExempt: undefined,
minProvideSize: undefined,
type: marketConfig.name.includes("PERP") ? "futures" : "spot",
underlying: marketConfig.baseSymbol,
enabled: undefined,
ask: asks.length > 0 ? asks[0].order.price : null,
bid: bids.length > 0 ? bids[0].order.price : null,
last: lastPrice,
postOnly: undefined,
price: lastPrice,
priceIncrement: tickSize,
sizeIncrement: minOrderSize,
restricted: undefined,
volumeUsd24h: volume,
} as MarketDto;
}
private getOrderBook = async (
request: Request,
response: Response,
next: NextFunction
) => {
const errors = validationResult(request);
if (!errors.isEmpty()) {
return response.status(400).json({ errors: errors.array() as BadParamError[] });
}
const marketName = request.params.market_name;
const depth = Number(request.query.depth) || 20;
const ordersInfo = await this.mangoSimpleClient.fetchAllBidsAndAsks(
false,
marketName
);
const bids = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side === "buy")
.sort((b1, b2) => b2.order.price - b1.order.price);
const asks = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side === "sell")
.sort((a1, a2) => a1.order.price - a2.order.price);
response.send({
success: true,
result: {
asks: asks
.slice(0, depth)
.map((ask) => [ask.order.price, ask.order.size]),
bids: bids
.slice(0, depth)
.map((bid) => [bid.order.price, bid.order.size]),
},
} as OrdersDto);
};
private getTrades = async (
request: Request,
response: Response,
next: NextFunction
) => {
const errors = validationResult(request);
if (!errors.isEmpty()) {
return response.status(400).json({ errors: errors.array() as BadParamError[] });
}
const allMarketConfigs = getAllMarkets(
this.mangoSimpleClient.mangoGroupConfig
);
const marketName = request.params.market_name;
const marketPk = allMarketConfigs.filter(
(marketConfig) => marketConfig.name === marketName
)[0].publicKey;
const tradesResponse = await fetch(
`https://serum-history.herokuapp.com/trades/address/${marketPk.toBase58()}`
);
const parsedTradesResponse = (await tradesResponse.json()) as any;
let tradeDtos;
if ("s" in parsedTradesResponse && parsedTradesResponse["s"] === "error") {
tradeDtos = [];
} else {
tradeDtos = parsedTradesResponse["data"].map((trade: any) => {
return {
id: trade["orderId"],
liquidation: undefined,
price: trade["price"],
side: trade["side"],
size: trade["size"],
time: new Date(trade["time"]),
} as TradeDto;
})};
response.send({ success: true, result: tradeDtos } as TradesDto);
};
private getCandles = async (
request: Request,
response: Response,
next: NextFunction
) => {
const errors = validationResult(request);
if (!errors.isEmpty()) {
return response.status(400).json({ errors: errors.array() as BadParamError[] }); }
const marketName = request.params.market_name;
const resolution = String(request.query.resolution);
const fromEpochS = Number(request.query.start_time);
const toEpochS = Number(request.query.end_time);
const { t, o, h, l, c, v } = await getOhlcv(
marketName,
resolution,
fromEpochS,
toEpochS
);
const ohlcvDtos: OhlcvDto[] = [];
for (let i = 0; i < t.length; i++) {
ohlcvDtos.push({
time: t[i],
open: o[i],
high: h[i],
low: l[i],
close: c[i],
volume: v[i],
} as OhlcvDto);
}
response.send({ success: true, result: ohlcvDtos } as OhlcvsDto);
};
}
export default MarketsController;
/// helper functions
async function getChange24H(marketConfig: MarketConfig): Promise<number> {
const fromS =
new Date(new Date().getTime() - 24 * 60 * 60 * 1000).getTime() / 1000;
const toS = new Date(new Date().getTime()).getTime() / 1000;
const { t, o, h, l, c, v } = await getOhlcv(
marketConfig.name,
"1D",
fromS,
toS
);
return c ? (c[0] - o[0]) / o[0] : undefined;
}
async function getChange1H(marketConfig: MarketConfig): Promise<number> {
const fromS =
new Date(new Date().getTime() - 60 * 60 * 1000).getTime() / 1000;
const toS = new Date(new Date().getTime()).getTime() / 1000;
const { t, o, h, l, c, v } = await getOhlcv(
marketConfig.name,
"60",
fromS,
toS
);
return c ? (c[0] - o[0]) / o[0] : undefined;
}
async function getChangeBod(marketConfig: MarketConfig): Promise<number> {
const from = new Date();
from.setUTCHours(0, 0, 0, 0);
const fromS = from.getTime() / 1000;
const to = new Date();
const toS = to.getTime() / 1000;
const { t, o, h, l, c, v } = await getOhlcv(
marketConfig.name,
"1",
fromS,
toS
);
// todo double check this
return c ? (c[0] - o[o.length - 1]) / o[o.length - 1] : undefined;
}
async function getOhlcv(
market: string,
resolution: string,
fromS: number,
toS: number
) {
const historyResponse = await fetch(
`https://serum-history.herokuapp.com/tv/history` +
`?symbol=${market}&resolution=${resolution}&from=${fromS}&to=${toS}`
);
return historyResponse.json();
}
export async function getVolumeForMarket(
marketConfig: MarketConfig
): Promise<Number> {
const perpVolume = await fetch(
`https://event-history-api.herokuapp.com/stats/perps/${marketConfig.publicKey.toString()}`
);
const parsedPerpVolume = await perpVolume.json();
return Number(parsedPerpVolume?.data?.volume);
}
/// Dtos
// e.g.
// {
// "success": true,
// "result": [
// {
// "name": "BTC-0628",
// "baseCurrency": null,
// "quoteCurrency": null,
// "quoteVolume24h": 28914.76,
// "change1h": 0.012,
// "change24h": 0.0299,
// "changeBod": 0.0156,
// "highLeverageFeeExempt": false,
// "minProvideSize": 0.001,
// "type": "future",
// "underlying": "BTC",
// "enabled": true,
// "ask": 3949.25,
// "bid": 3949,
// "last": 3949.00,
// "postOnly": false,
// "price": 10579.52,
// "priceIncrement": 0.25,
// "sizeIncrement": 0.0001,
// "restricted": false,
// "volumeUsd24h": 28914.76
// }
// ]
// }
interface MarketsDto {
success: boolean;
result: MarketDto[];
}
interface MarketDto {
name: string;
baseCurrency: string;
quoteCurrency: string;
quoteVolume24h: number;
change1h: number;
change24h: number;
changeBod: number;
highLeverageFeeExempt: boolean;
minProvideSize: number;
type: string;
underlying: string;
enabled: boolean;
ask: number;
bid: number;
last: number;
postOnly: boolean;
price: number;
priceIncrement: number;
sizeIncrement: number;
restricted: boolean;
volumeUsd24h: number;
}
// e.g.
// {
// "success": true,
// "result": {
// "asks": [
// [
// 4114.25,
// 6.263
// ]
// ],
// "bids": [
// [
// 4112.25,
// 49.29
// ]
// ]
// }
// }
interface OrdersDto {
success: boolean;
result: {
asks: number[][];
bids: number[][];
};
}
// e.g.
// {
// "success": true,
// "result": [
// {
// "id": 3855995,
// "liquidation": false,
// "price": 3857.75,
// "side": "buy",
// "size": 0.111,
// "time": "2019-03-20T18:16:23.397991+00:00"
// }
// ]
// }
interface TradesDto {
success: boolean;
result: TradeDto[];
}
interface TradeDto {
id: string;
liquidation: boolean;
price: number;
side: string;
size: number;
time: Date;
}
// e.g.
// {
// "success": true,
// "result": [
// {
// "close": 11055.25,
// "high": 11089.0,
// "low": 11043.5,
// "open": 11059.25,
// "startTime": "2019-06-24T17:15:00+00:00",
// "volume": 464193.95725
// }
// ]
// }
interface OhlcvsDto {
success: boolean;
result: OhlcvDto[];
}
interface OhlcvDto {
time: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
}

View File

@ -0,0 +1,337 @@
import Controller from "./controller.interface";
import MangoSimpleClient from "./mango.simple.client";
import { OrderInfo } from "./types";
import { isValidMarket, logger } from "./utils";
import { PerpOrder } from "@blockworks-foundation/mango-client";
import { Order } from "@project-serum/serum/lib/market";
import { NextFunction, Request, Response, Router } from "express";
import { param, query, validationResult } from "express-validator";
import { BadParamError, BadRequestError } from "dtos";
class OrdersController implements Controller {
public path = "/api/orders";
public router = Router();
constructor(public mangoSimpleClient: MangoSimpleClient) {
this.initializeRoutes();
}
private initializeRoutes() {
// GET /orders?market={market_name}
this.router.get(
this.path,
query("market").custom(isValidMarket).optional(),
this.getOpenOrders
);
// POST /orders
this.router.post(this.path, this.placeOrder);
// // POST /orders/{order_id}/modify todo
// this.router.post(this.path, this.modifyOrder);
// DELETE /orders
this.router.delete(this.path, this.cancelAllOrders);
// DELETE /orders/{order_id}
this.router.delete(`${this.path}/:order_id`, this.cancelOrderByOrderId);
// DELETE /orders/by_client_id/{client_id}
this.router.delete(
`${this.path}/by_client_id/:client_id`,
this.cancelOrderByClientId
);
}
private getOpenOrders = async (
request: Request,
response: Response,
next: NextFunction
) => {
const errors = validationResult(request);
if (!errors.isEmpty()) {
return response.status(400).json({ errors: errors.array() as BadParamError[] });
}
const openOrders = await this.mangoSimpleClient.fetchAllBidsAndAsks(
true,
request.query.market ? String(request.query.market) : undefined
);
const orderDtos = openOrders.flat().map((orderInfo: OrderInfo) => {
if ("bestInitial" in orderInfo.order) {
const perpOrder = orderInfo.order as PerpOrder;
return {
createdAt: new Date(perpOrder.timestamp.toNumber() * 1000),
filledSize: undefined,
future: orderInfo.market.config.name,
id: perpOrder.orderId.toString(),
market: orderInfo.market.config.name,
price: perpOrder.price,
avgFillPrice: undefined,
remainingSize: undefined,
side: perpOrder.side,
size: perpOrder.size,
status: "open",
type: undefined, // todo should this always be limit?
reduceOnly: undefined,
ioc: undefined,
postOnly: undefined,
clientId:
perpOrder.clientId && perpOrder.clientId.toString() !== "0"
? perpOrder.clientId.toString()
: undefined,
} as OrderDto;
}
const spotOrder = orderInfo.order as Order;
return {
createdAt: undefined,
filledSize: undefined,
future: orderInfo.market.config.name,
id: spotOrder.orderId.toString(),
market: orderInfo.market.config.name,
price: spotOrder.price,
avgFillPrice: undefined,
remainingSize: undefined,
side: spotOrder.side,
size: spotOrder.size,
status: "open",
type: undefined,
reduceOnly: undefined,
ioc: undefined,
postOnly: undefined,
clientId:
spotOrder.clientId && spotOrder.clientId.toString() !== "0"
? spotOrder.clientId.toString()
: undefined,
} as OrderDto;
});
response.send({ success: true, result: orderDtos } as OrdersDto);
};
private placeOrder = async (
request: Request,
response: Response,
next: NextFunction
) => {
const placeOrderDto = request.body as PlaceOrderDto;
logger.info("placing order...")
// todo validation on placeOrderDto
// todo validation of marketname
try {
await this.mangoSimpleClient.placeOrder(
placeOrderDto.market,
placeOrderDto.type,
placeOrderDto.side,
placeOrderDto.size,
placeOrderDto.price,
placeOrderDto.ioc
? "ioc"
: placeOrderDto.postOnly
? "postOnly"
: "limit",
placeOrderDto.clientId
);
} catch (error) {
return response.status(400).send({
errors: [{ msg: error.message } as BadRequestError],
});
}
response.send({
success: true,
result: {
createdAt: new Date(),
filledSize: undefined,
future: placeOrderDto.market,
id: undefined,
market: placeOrderDto.market,
price: undefined,
remainingSize: undefined,
side: placeOrderDto.side,
size: placeOrderDto.size,
status: undefined,
type: placeOrderDto.type,
reduceOnly: undefined,
ioc: placeOrderDto.ioc,
postOnly: placeOrderDto.postOnly,
clientId: placeOrderDto.clientId
? placeOrderDto.clientId.toString()
: null,
},
} as PlaceOrderResponseDto);
};
private cancelAllOrders = async (
request: Request,
response: Response,
next: NextFunction
) => {
// todo log info
// todo: leads to 429 if too many orders exist, needs optimization
await this.mangoSimpleClient.cancelAllOrders();
response.send();
};
private cancelOrderByOrderId = async (
request: Request,
response: Response,
next: NextFunction
) => {
// todo log info
const orderId = request.params.order_id;
this.mangoSimpleClient
.getOrderByOrderId(orderId)
.then((orderInfos) => {
if (!orderInfos.length) {
return response
.status(400)
.json({ errors: [{ msg: "Order not found!" }] });
}
this.mangoSimpleClient
.cancelOrder(orderInfos[0])
.then(() => response.send())
.catch(() => {
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
});
})
.catch(() => {
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
});
};
private cancelOrderByClientId = async (
request: Request,
response: Response,
next: NextFunction
) => {
// todo log info
const clientId = request.params.client_id;
this.mangoSimpleClient
.getOrderByClientId(clientId)
.then((orderInfos) => {
if (!orderInfos.length) {
return response
.status(400)
.json({ errors: [{ msg: "Order not found!" }] });
}
this.mangoSimpleClient
.cancelOrder(orderInfos[0])
.then(() => response.send())
.catch(() => {
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
});
})
.catch(() => {
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
});
};
}
export default OrdersController;
/// Dtos
// e.g.
// {
// "success": true,
// "result": [
// {
// "createdAt": "2019-03-05T09:56:55.728933+00:00",
// "filledSize": 10,
// "future": "XRP-PERP",
// "id": 9596912,
// "market": "XRP-PERP",
// "price": 0.306525,
// "avgFillPrice": 0.306526,
// "remainingSize": 31421,
// "side": "sell",
// "size": 31431,
// "status": "open",
// "type": "limit",
// "reduceOnly": false,
// "ioc": false,
// "postOnly": false,
// "clientId": null
// }
// ]
// }
interface OrdersDto {
success: boolean;
result: OrderDto[];
}
interface OrderDto {
createdAt: Date;
filledSize: number;
future: string;
id: string;
market: string;
price: number;
avgFillPrice: number;
remainingSize: number;
side: string;
size: number;
status: string;
type: string;
reduceOnly: boolean;
ioc: boolean;
postOnly: boolean;
clientId: null;
}
// e.g.
// {
// "market": "XRP-PERP",
// "side": "sell",
// "price": 0.306525,
// "type": "limit",
// "size": 31431.0,
// "reduceOnly": false,
// "ioc": false,
// "postOnly": false,
// "clientId": null
// }
interface PlaceOrderDto {
market: string;
side: "sell" | "buy";
price: number;
type: "limit" | "market";
size: number;
reduceOnly: boolean;
ioc: boolean;
postOnly: boolean;
clientId: number; // todo: ftx uses string
}
interface PlaceOrderResponseDto {
success: true;
result: {
createdAt: Date;
filledSize: number;
future: string;
id: number;
market: string;
price: number;
remainingSize: number;
side: "buy" | "sell";
size: number;
status: "new" | "open" | "closed";
type: "limit" | "market";
reduceOnly: boolean;
ioc: boolean;
postOnly: boolean;
clientId: string;
};
}

View File

@ -0,0 +1,5 @@
import App from "./app";
const app = new App();
app.listen();

View File

@ -0,0 +1,49 @@
import { Config, GroupConfig } from "@blockworks-foundation/mango-client";
import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum";
import { CustomValidator } from "express-validator";
/// logging related
import pino from "pino";
/// mango related
export const i80f48ToPercent = (value: I80F48) =>
value.mul(I80F48.fromNumber(100));
const groupName = process.env.GROUP || "devnet.1";
const mangoGroupConfig: GroupConfig = Config.ids().groups.filter(
(group) => group.name === groupName
)[0];
const allMarketNames = mangoGroupConfig.spotMarkets
.map((spotMarketConfig) => spotMarketConfig.name)
.concat(
mangoGroupConfig.perpMarkets.map(
(perpMarketConfig) => perpMarketConfig.name
)
);
/// general
export function zipDict<K extends string | number | symbol, V>(
keys: K[],
values: V[]
): Partial<Record<K, V>> {
const result: Partial<Record<K, V>> = {};
keys.forEach((key, index) => {
result[key] = values[index];
});
return result;
}
export const logger = pino({
prettyPrint: { translateTime: true },
});
/// expressjs related
export const isValidMarket: CustomValidator = (marketName) => {
if (allMarketNames.indexOf(marketName) === -1) {
return Promise.reject(`Market ${marketName} not supported!`);
}
return Promise.resolve();
};

View File

@ -7,16 +7,17 @@ import {
nativeToUi,
QUOTE_INDEX,
} from "@blockworks-foundation/mango-client";
import { Market, OpenOrders } from "@project-serum/serum";
import Controller from "controller.interface";
import { NextFunction, Request, Response, Router } from "express";
import { sumBy } from "lodash";
import MangoSimpleClient from "mango.simple.client";
class WalletController implements Controller {
public path = "/wallet";
public path = "/api/wallet";
public router = Router();
constructor(public mangoMarkets: MangoSimpleClient) {
constructor(public mangoSimpleClient: MangoSimpleClient) {
this.initializeRoutes();
}
@ -30,18 +31,23 @@ class WalletController implements Controller {
next: NextFunction
) => {
// local copies of mango objects
const mangoGroupConfig = this.mangoMarkets.mangoGroupConfig;
const mangoGroup = this.mangoMarkets.mangoGroup;
const mangoGroupConfig = this.mangoSimpleClient.mangoGroupConfig;
const mangoGroup = this.mangoSimpleClient.mangoGroup;
// (re)load things which we want fresh
const mangoAccount = await this.mangoMarkets.mangoAccount.reload(
this.mangoMarkets.connection,
this.mangoMarkets.mangoGroup.dexProgramId
);
const mangoCache = await mangoGroup.loadCache(this.mangoMarkets.connection);
await mangoGroup.loadRootBanks(this.mangoMarkets.connection);
const [mangoAccount, mangoCache, rootBanks] = await Promise.all([
this.mangoSimpleClient.mangoAccount.reload(
this.mangoSimpleClient.connection,
this.mangoSimpleClient.mangoGroup.dexProgramId
),
this.mangoSimpleClient.mangoGroup.loadCache(
this.mangoSimpleClient.connection
),
mangoGroup.loadRootBanks(this.mangoSimpleClient.connection),
]);
////// copy pasta block from mango-ui-v3
/* tslint:disable */
const balances: Balances[][] = new Array();
for (const {
@ -53,7 +59,8 @@ class WalletController implements Controller {
response.send([]);
}
const openOrders: any = mangoAccount.spotOpenOrdersAccounts[marketIndex];
const openOrders: OpenOrders =
mangoAccount.spotOpenOrdersAccounts[marketIndex];
const quoteCurrencyIndex = QUOTE_INDEX;
let nativeBaseFree = 0;
@ -63,7 +70,7 @@ class WalletController implements Controller {
if (openOrders) {
nativeBaseFree = openOrders.baseTokenFree.toNumber();
nativeQuoteFree = openOrders.quoteTokenFree
.add(openOrders["referrerRebatesAccrued"])
.add((openOrders as any)["referrerRebatesAccrued"])
.toNumber();
nativeBaseLocked = openOrders.baseTokenTotal
.sub(openOrders.baseTokenFree)
@ -178,7 +185,7 @@ class WalletController implements Controller {
const token = getTokenBySymbol(mangoGroupConfig, quoteMeta.symbol);
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey);
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache));
/* tslint:enable */
////// end of copy pasta block from mango-ui-v3
// append balances for base symbols
@ -197,7 +204,7 @@ class WalletController implements Controller {
// append balance for quote symbol
balanceDtos.push({
coin: this.mangoMarkets.mangoGroupConfig.quoteSymbol,
coin: this.mangoSimpleClient.mangoGroupConfig.quoteSymbol,
free: quoteMeta.deposits.toNumber(),
spotBorrow: quoteMeta.borrows.toNumber(),
total: net.toNumber(),

View File

@ -8,9 +8,12 @@
"experimentalDecorators": true,
"moduleResolution": "node",
"noImplicitAny": true,
"skipLibCheck": true,
"outDir": "./dist",
"sourceMap": true,
"target": "ES2020"
"target": "ES2020",
"module": "commonjs",
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]

View File

@ -0,0 +1,9 @@
{
"extends": ["tslint:latest"],
"rules": {
"no-implicit-dependencies": false,
"no-submodule-imports": false,
"no-object-literal-type-assertion": false,
"no-string-literal": false
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +0,0 @@
{
"name": "mango-v3-service",
"version": "0.0.1",
"description": "REST API for trading against serum & mango markets",
"main": "./src/server.ts",
"scripts": {
"dev": "ts-node ./src/server.ts",
"lint": "tslint -p tsconfig.json -c tslint.json",
"prepare": "husky install",
"pretty-quick": "pretty-quick",
"test": "jest"
},
"author": "microwavedcola1",
"license": "MIT",
"dependencies": {
"@blockworks-foundation/mango-client": "^3.0.13",
"body-parser": "^1.19.0",
"envalid": "^7.2.1",
"express": "^4.17.1",
"lodash": "^4.17.21",
"loglevel": "^1.7.1"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^2.0.4",
"@types/express": "^4.17.13",
"husky": "^7.0.2",
"prettier": "^2.3.2",
"prettier-plugin-organize-imports": "^2.3.3",
"pretty-quick": "^3.1.1",
"ts-node": "^10.2.1",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.4.2"
}
}

13
py/README.md Normal file
View File

@ -0,0 +1,13 @@
# Pre-requisites
* you have [poetry](https://python-poetry.org/docs/#installation) installed
* you have the REST API service from `../mango-service-v3 running` locally
# How to test examples while developing
* `poetry install`
* `poetry shell`
* `python basic.py`
* ...
# Todos
* add pre-commit e.g. black, pylint, mypy, reorder-python-imports

60
py/basic.py Normal file
View File

@ -0,0 +1,60 @@
from mango_service_v3_py.api import Exchange
from mango_service_v3_py.dtos import PlacePerpOrder, PlaceOrder
if __name__ == "__main__":
exchange = Exchange()
print(exchange.get_open_positions())
print(exchange.get_balances())
print(exchange.get_markets())
print(exchange.get_market_by_market_name("BTC-PERP"))
print(exchange.get_orderboook("BTC-PERP"))
print(exchange.get_trades("BTC-PERP"))
print(exchange.get_candles("BTC-PERP", 60, 1625922900, 1631214960))
print(exchange.get_orders())
print(exchange.get_orders_by_market_name("BTC-PERP"))
exchange.place_order(
PlacePerpOrder(
market="BTC-PERP",
side="buy",
price=2000,
type="limit",
size=0.0001,
reduce_only=False,
ioc=False,
post_only=False,
client_id=123,
)
)
print(exchange.get_orders())
exchange.place_order(
PlaceOrder(
market="BTC/USDC",
side="buy",
price=2000,
type="limit",
size=0.0001,
reduce_only=False,
ioc=False,
post_only=False,
)
)
print(exchange.get_orders())
exchange.delete_order_by_order_id("3689367261485984031001846")
print(exchange.get_orders())
exchange.delete_order_by_client_id("3689367261485984031001846")
print(exchange.get_orders())
exchange.delete_all_orders()
print(exchange.get_orders())

View File

@ -0,0 +1 @@
__version__ = "0.1.0"

View File

@ -0,0 +1,103 @@
import inspect
import json
from typing import List, Union
import httpx
from httpx import TimeoutException
from pydantic import parse_obj_as
from mango_service_v3_py.dtos import (
Position,
Balance,
Market,
Orderbook,
Trade,
Candle,
Order,
PlaceOrder,
BadRequestError,
)
# todo add mypy
def timeout_error_msg_customizer(response):
try:
response.raise_for_status()
except TimeoutException as e:
raise Exception(f"timed out within {inspect.stack()[1][3]}") from e
class Exchange:
def __init__(self):
self.BASE_URL = "http://localhost:3000/api"
def get_open_positions(self) -> List[Position]:
response = httpx.get(f"{self.BASE_URL}/positions")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Position], json.loads(response.text)["result"])
def get_balances(self) -> List[Balance]:
response = httpx.get(f"{self.BASE_URL}/wallet/balances")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Balance], json.loads(response.text)["result"])
def get_markets(self) -> List[Market]:
response = httpx.get(f"{self.BASE_URL}/markets")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Market], json.loads(response.text)["result"])
def get_market_by_market_name(self, market_name: str) -> List[Market]:
response = httpx.get(f"{self.BASE_URL}/markets/{market_name}")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Market], json.loads(response.text)["result"])
def get_orderboook(self, market_name: str, depth: int = 30) -> Orderbook:
response = httpx.get(
f"{self.BASE_URL}/markets/{market_name}/orderbook?depth={depth}"
)
timeout_error_msg_customizer(response)
return parse_obj_as(Orderbook, json.loads(response.text)["result"])
def get_trades(self, market_name: str) -> List[Trade]:
response = httpx.get(f"{self.BASE_URL}/markets/{market_name}/trades")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Trade], json.loads(response.text)["result"])
def get_candles(
self, market_name: str, resolution: int, start_time: int, end_time: int
) -> List[Candle]:
response = httpx.get(
f"{self.BASE_URL}/markets/{market_name}/candles?resolution={resolution}&start_time={start_time}&end_time={end_time}"
)
timeout_error_msg_customizer(response)
return parse_obj_as(List[Candle], json.loads(response.text)["result"])
def get_orders(self,) -> List[Order]:
response = httpx.get(f"{self.BASE_URL}/orders")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Order], json.loads(response.text)["result"])
def get_orders_by_market_name(self, market_name: str) -> List[Order]:
response = httpx.get(f"{self.BASE_URL}/orders?market={market_name}")
timeout_error_msg_customizer(response)
return parse_obj_as(List[Order], json.loads(response.text)["result"])
def place_order(self, order: PlaceOrder) -> None:
response = httpx.post(f"{self.BASE_URL}/orders", json=order.dict())
timeout_error_msg_customizer(response)
# if response.status_code == httpx.codes.BAD_REQUEST:
# return parse_obj_as(
# List[BadRequestError], json.loads(response.text)["errors"]
# )
def delete_order_by_client_id(self, client_id):
response = httpx.delete(f"{self.BASE_URL}/orders/by_client_id/{client_id}")
timeout_error_msg_customizer(response)
def delete_order_by_order_id(self, order_id):
response = httpx.delete(f"{self.BASE_URL}/orders/{order_id}")
timeout_error_msg_customizer(response)
def delete_all_orders(self):
response = httpx.delete(f"{self.BASE_URL}/orders")
timeout_error_msg_customizer(response)

View File

@ -0,0 +1,137 @@
from datetime import datetime
from typing import List
from typing import Optional
from typing import Union
from pydantic import BaseModel
from typing_extensions import Literal
Side = Union[Literal["buy"], Literal["sell"]]
def to_camel_case(snake_str):
components = snake_str.split("_")
return components[0] + "".join(x.title() for x in components[1:])
class CamelCaseModel(BaseModel):
class Config:
alias_generator = to_camel_case
allow_population_by_field_name = True
class Balance(CamelCaseModel):
coin: str
free: float
spot_borrow: float
total: float
usd_value: float
available_without_borrow: float
class Market(CamelCaseModel):
name: str
base_currency: str
quote_currency: str
quote_volume24_h: Optional[float]
change1_h: Optional[float]
change24_h: Optional[float]
change_bod: Optional[float]
high_leverage_fee_exempt: Optional[bool]
min_provide_size: Optional[float]
type: str
underlying: Optional[str]
enabled: Optional[bool]
ask: Optional[int] # optional for devnet
bid: Optional[int] # optional for devnet
last: Optional[float] # optional for devnet
post_only: Optional[bool]
price: Optional[float] # optional for devnet
price_increment: float
size_increment: float
restricted: Optional[bool]
volume_usd24_h: Optional[float]
class Orderbook(CamelCaseModel):
asks: List[List[float]]
bids: List[List[float]]
class Trade(CamelCaseModel):
id: str
liquidation: Optional[bool]
price: float
side: str
size: float
time: datetime
class Candle(CamelCaseModel):
time: int
open: float
high: float
low: float
close: float
volume: float
class Position(CamelCaseModel):
cost: float
cumulative_buy_size: Optional[float]
cumulative_sell_size: Optional[float]
entry_price: float
estimated_liquidation_price: Optional[float]
future: str
initial_margin_requirement: Optional[float]
long_order_size: Optional[float]
maintenance_margin_requirement: Optional[float]
net_size: Optional[float]
open_size: Optional[float]
realized_pnl: Optional[float]
recent_average_open_price: Optional[float]
recent_break_even_price: Optional[float]
recent_pnl: Optional[float]
short_order_size: Optional[float]
side: str
size: float
unrealized_pnl: Optional[float]
collateral_used: Optional[float]
class Order(CamelCaseModel):
created_at: Optional[datetime] # optional for spot
filled_size: Optional[int]
future: str
id: int
market: str
price: float
avg_fill_price: Optional[float]
remaining_size: Optional[int]
side: Side
size: float
status: Optional[str]
type: Optional[str]
reduce_only: Optional[bool]
ioc: Optional[bool]
post_only: Optional[bool]
client_id: Optional[str]
class PlaceOrder(CamelCaseModel):
market: str
side: Side
price: int
type: str
size: float
reduce_only: bool
ioc: bool
post_only: bool
class PlacePerpOrder(PlaceOrder):
client_id: int
class BadRequestError(CamelCaseModel):
msg: str

267
py/market_maker.py Normal file
View File

@ -0,0 +1,267 @@
import datetime
import logging
import os
import sys
import time
from dataclasses import dataclass
from decimal import Decimal
from os.path import getmtime
from tenacity import retry, wait_fixed, stop_after_delay, stop_after_attempt
from mango_service_v3_py.api import Exchange
from mango_service_v3_py.dtos import Side, PlacePerpOrder
# based on https://github.com/BitMEX/sample-market-maker/blob/master/market_maker/market_maker.py
CYCLE_INTERVAL = 30
MARKET = "BTC-PERP"
SIZE = 0.0003
MAX_LONG_POSITION = 0.002
MAX_SHORT_POSITION = -0.002
MAX_ORDERS = 4
watched_files_mtimes = [(f, getmtime(f)) for f in ["market_maker.py"]]
logging.basicConfig(
format="%(asctime)s %(levelname)-2s %(message)s",
level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("simple_market_maker")
@dataclass
class SimpleOrder:
price: float
side: Side
size: float
def toNearest(num, tickDec):
return Decimal(round(num / tickDec, 0)) * tickDec
class MM:
def __init__(self):
self.exchange = Exchange()
self.market = None
self.start_position_buy = None
self.start_position_sell = None
self.positions = None
# todo unused
@retry(stop=(stop_after_delay(10) | stop_after_attempt(5)), wait=wait_fixed(5))
def retry_wrapper(self, exchange_method, *arg):
getattr(self.exchange, exchange_method)(arg)
def log_recent_trades(self) -> None:
trades = self.exchange.get_trades(MARKET)
recent_trades = [
trade
for trade in trades
if datetime.datetime.now((datetime.timezone.utc)) - trade.time
< datetime.timedelta(seconds=CYCLE_INTERVAL)
]
if recent_trades:
# todo: should log only my recent trades
logger.info("- recent trades")
for trade in recent_trades:
logger.info(
f" |_ side {trade.side:4}, size {trade.size:6}, price {trade.price:8}, value {trade.price * trade.size}, time: {trade.time.strftime('%H:%M:%S')}"
)
logger.info("")
def get_ticker(self):
self.market = self.exchange.get_market_by_market_name(MARKET)[0]
self.start_position_buy = self.market.bid - self.market.price_increment
self.start_position_sell = self.market.ask + self.market.price_increment
self.positions = [
position
for position in self.exchange.get_open_positions()
if position.future == MARKET
]
def get_price_offset(self, index):
start_position = (
self.start_position_buy if index < 0 else self.start_position_sell
)
index = index + 1 if index < 0 else index - 1
return toNearest(
Decimal(start_position) * Decimal(1 + self.market.price_increment) ** index,
Decimal(str(self.market.price_increment)),
)
def prepare_order(self, index) -> SimpleOrder:
size = Decimal(str(SIZE)) + ((abs(index) - 1) * Decimal(str(SIZE)))
price = self.get_price_offset(index)
return SimpleOrder(price=price, size=size, side="buy" if index < 0 else "sell")
def converge_orders(self, buy_orders, sell_orders):
to_create = []
to_cancel = []
buys_matched = 0
sells_matched = 0
existing_orders = self.exchange.get_orders()
existing_orders = sorted(existing_orders, key=lambda order_: order_.price)
buy_orders = sorted(buy_orders, key=lambda order_: order_.price)
sell_orders = sorted(sell_orders, key=lambda order_: order_.price)
for order in existing_orders:
try:
if order.side == "buy":
desired_order = buy_orders[buys_matched]
buys_matched += 1
else:
desired_order = sell_orders[sells_matched]
sells_matched += 1
if desired_order.size != Decimal(str(order.size)) or (
desired_order.price != Decimal(str(order.price))
and abs((desired_order.price / Decimal(str(order.price))) - 1)
> 0.01
):
to_cancel.append(order)
to_create.append(desired_order)
except IndexError:
to_cancel.append(order)
while buys_matched < len(buy_orders):
to_create.append(buy_orders[buys_matched])
buys_matched += 1
while sells_matched < len(sell_orders):
to_create.append(sell_orders[sells_matched])
sells_matched += 1
if len(to_cancel) > 0:
logger.info(f"- cancelling {len(to_cancel)} orders...")
for order in sorted(to_create, key=lambda order: order.price, reverse=True):
logger.info(
f" |_ side {order.side:4}, size {order.size}, price {order.price}, value {order.price * order.size}"
)
for order in to_cancel:
try:
self.exchange.delete_order_by_order_id(order.id)
except:
pass
logger.info("")
else:
logger.info("- no orders to cancel")
if len(to_create) > 0:
logger.info(f"- creating {len(to_create)} orders...")
for order in [
order
for order in sorted(
to_create, key=lambda order: order.price, reverse=True
)
if order.side == "sell"
]:
logger.info(
f" |_ price {order.price}, side {order.side:4}, size {order.size}, value {order.price * order.size}"
)
logger.info(
f" current bid -> {self.market.bid}, ask {self.market.ask} <- ask "
)
for order in [
order
for order in sorted(
to_create, key=lambda order: order.price, reverse=True
)
if order.side == "buy"
]:
logger.info(
f" |_ price {order.price}, side {order.side:4}, size {order.size}, value {order.price * order.size}"
)
for order in to_create:
self.exchange.place_order(
PlacePerpOrder(
market=MARKET,
side=order.side,
price=order.price,
type="limit",
size=order.size,
reduce_only=False,
ioc=False,
post_only=False,
client_id=123,
)
)
logger.info("")
else:
logger.info("- no orders to create, current open orders")
for order in sorted(
existing_orders, key=lambda order: order.price, reverse=True
):
logger.info(
f" |_ side {order.side:4}, size {order.size}, price {order.price}, value {order.price * order.size}"
)
def long_position_limit_exceeded(self):
if len(self.positions) == 0:
return False
return self.positions[0].net_size >= MAX_LONG_POSITION
def short_position_limit_exceeded(self):
if len(self.positions) == 0:
return False
return self.positions[0].net_size <= MAX_SHORT_POSITION
def place_orders(self):
buy_orders = []
sell_orders = []
if not self.long_position_limit_exceeded():
for i in reversed(range(1, 2 + 1)):
buy_orders.append(self.prepare_order(-i))
else:
logger.info(
f"- skipping adding to longs, current position {self.positions[0].net_size}"
)
if not self.short_position_limit_exceeded():
for i in reversed(range(1, MAX_ORDERS + 1)):
sell_orders.append(self.prepare_order(i))
else:
logger.info(
f"- skipping adding to shorts, current position {self.positions[0].net_size}"
)
return self.converge_orders(buy_orders, sell_orders)
def check_file_change(self):
for f, mtime in watched_files_mtimes:
if getmtime(f) > mtime:
self.restart()
def restart(self):
logger.info("------------------------------")
logger.info("restarting the market maker...")
os.execv(sys.executable, [sys.executable] + sys.argv)
if __name__ == "__main__":
mm = MM()
logger.info("deleting all orders...")
try:
mm.exchange.delete_all_orders()
except Exception as e:
logger.error(f"Exception: {e}")
while True:
logger.info("next cycle...")
try:
mm.check_file_change()
mm.log_recent_trades()
mm.get_ticker()
mm.place_orders()
time.sleep(CYCLE_INTERVAL)
logger.info("")
except Exception as e:
logger.error(f"Exception: {e}")
time.sleep(CYCLE_INTERVAL)
logger.info("")

595
py/poetry.lock generated Normal file
View File

@ -0,0 +1,595 @@
[[package]]
category = "main"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
name = "anyio"
optional = false
python-versions = ">=3.6.2"
version = "3.3.1"
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
[package.dependencies.typing-extensions]
python = "<3.8"
version = "*"
[package.extras]
doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["coverage (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16)"]
[[package]]
category = "dev"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
name = "appdirs"
optional = false
python-versions = "*"
version = "1.4.4"
[[package]]
category = "dev"
description = "Atomic file writes."
marker = "sys_platform == \"win32\""
name = "atomicwrites"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "21.2.0"
[package.extras]
dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
category = "dev"
description = "The uncompromising code formatter."
name = "black"
optional = false
python-versions = ">=3.6"
version = "19.10b0"
[package.dependencies]
appdirs = "*"
attrs = ">=18.1.0"
click = ">=6.5"
pathspec = ">=0.6,<1"
regex = "*"
toml = ">=0.9.4"
typed-ast = ">=1.4.0"
[package.extras]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2021.5.30"
[[package]]
category = "main"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
name = "charset-normalizer"
optional = false
python-versions = ">=3.5.0"
version = "2.0.4"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
category = "dev"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=3.6"
version = "8.0.1"
[package.dependencies]
colorama = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = "*"
[[package]]
category = "dev"
description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\" or platform_system == \"Windows\""
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.4"
[[package]]
category = "main"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
name = "h11"
optional = false
python-versions = ">=3.6"
version = "0.12.0"
[[package]]
category = "main"
description = "A minimal low-level HTTP client."
name = "httpcore"
optional = false
python-versions = ">=3.6"
version = "0.13.7"
[package.dependencies]
anyio = ">=3.0.0,<4.0.0"
h11 = ">=0.11,<0.13"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
[[package]]
category = "main"
description = "The next generation HTTP client."
name = "httpx"
optional = false
python-versions = ">=3.6"
version = "0.19.0"
[package.dependencies]
certifi = "*"
charset-normalizer = "*"
httpcore = ">=0.13.3,<0.14.0"
sniffio = "*"
[package.dependencies.rfc3986]
extras = ["idna2008"]
version = ">=1.3,<2"
[package.extras]
brotli = ["brotlicffi", "brotli"]
http2 = ["h2 (>=3,<5)"]
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=3.5"
version = "3.2"
[[package]]
category = "dev"
description = "Read metadata from Python packages"
marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false
python-versions = ">=3.6"
version = "4.8.1"
[package.dependencies]
zipp = ">=0.5"
[package.dependencies.typing-extensions]
python = "<3.8"
version = ">=3.6.4"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
perf = ["ipython"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.9.0"
[[package]]
category = "dev"
description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=3.6"
version = "21.0"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
category = "dev"
description = "Utility library for gitignore style pattern matching of file paths."
name = "pathspec"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "0.9.0"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
[package.dependencies]
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.10.0"
[[package]]
category = "main"
description = "Data validation and settings management using python 3.6 type hinting"
name = "pydantic"
optional = false
python-versions = ">=3.6.1"
version = "1.8.2"
[package.dependencies]
typing-extensions = ">=3.7.4.3"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
category = "dev"
description = "Python parsing module"
name = "pyparsing"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.4.3"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
checkqa-mypy = ["mypy (v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "Alternative regular expression module, to replace re."
name = "regex"
optional = false
python-versions = "*"
version = "2021.8.28"
[[package]]
category = "main"
description = "Validating URI References per RFC 3986"
name = "rfc3986"
optional = false
python-versions = "*"
version = "1.5.0"
[package.dependencies]
[package.dependencies.idna]
optional = true
version = "*"
[package.extras]
idna2008 = ["idna"]
[[package]]
category = "main"
description = "Sniff out which async library your code is running under"
name = "sniffio"
optional = false
python-versions = ">=3.5"
version = "1.2.0"
[[package]]
category = "dev"
description = "Retry code until it succeeds"
name = "tenacity"
optional = false
python-versions = ">=3.6"
version = "8.0.1"
[package.extras]
doc = ["reno", "sphinx", "tornado (>=4.5)"]
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
name = "toml"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.10.2"
[[package]]
category = "dev"
description = "a fork of Python 2 and 3 ast modules with type comment support"
name = "typed-ast"
optional = false
python-versions = "*"
version = "1.4.3"
[[package]]
category = "main"
description = "Backported and Experimental Type Hints for Python 3.5+"
name = "typing-extensions"
optional = false
python-versions = "*"
version = "3.10.0.2"
[[package]]
category = "dev"
description = "Measures the displayed width of unicode strings in a terminal"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.2.5"
[[package]]
category = "dev"
description = "Backport of pathlib-compatible object wrapper for zip files"
marker = "python_version < \"3.8\""
name = "zipp"
optional = false
python-versions = ">=3.6"
version = "3.5.0"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
[metadata]
content-hash = "76c0767f274f016c19e1196b85039711c356a9fc293989ec3c3b8e352c639dbf"
python-versions = "^3.7"
[metadata.files]
anyio = [
{file = "anyio-3.3.1-py3-none-any.whl", hash = "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd"},
{file = "anyio-3.3.1.tar.gz", hash = "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe"},
]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
]
black = [
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
]
certifi = [
{file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
{file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"},
{file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"},
]
click = [
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
]
httpcore = [
{file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"},
{file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"},
]
httpx = [
{file = "httpx-0.19.0-py3-none-any.whl", hash = "sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435"},
{file = "httpx-0.19.0.tar.gz", hash = "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0"},
]
idna = [
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
]
importlib-metadata = [
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
]
more-itertools = [
{file = "more-itertools-8.9.0.tar.gz", hash = "sha256:8c746e0d09871661520da4f1241ba6b908dc903839733c8203b552cffaf173bd"},
{file = "more_itertools-8.9.0-py3-none-any.whl", hash = "sha256:70401259e46e216056367a0a6034ee3d3f95e0bf59d3aa6a4eb77837171ed996"},
]
packaging = [
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
py = [
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
]
pydantic = [
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e"},
{file = "pydantic-1.8.2-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840"},
{file = "pydantic-1.8.2-cp36-cp36m-win_amd64.whl", hash = "sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b"},
{file = "pydantic-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1"},
{file = "pydantic-1.8.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23"},
{file = "pydantic-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287"},
{file = "pydantic-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e"},
{file = "pydantic-1.8.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820"},
{file = "pydantic-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3"},
{file = "pydantic-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f"},
{file = "pydantic-1.8.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b"},
{file = "pydantic-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3"},
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
]
regex = [
{file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"},
{file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"},
{file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"},
{file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"},
{file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"},
{file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"},
{file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"},
{file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"},
{file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"},
{file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"},
{file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"},
{file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"},
{file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"},
{file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"},
{file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"},
{file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"},
{file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"},
{file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"},
{file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"},
{file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"},
{file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"},
]
rfc3986 = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
sniffio = [
{file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
{file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
]
tenacity = [
{file = "tenacity-8.0.1-py3-none-any.whl", hash = "sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a"},
{file = "tenacity-8.0.1.tar.gz", hash = "sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typed-ast = [
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"},
{file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"},
{file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"},
{file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"},
{file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"},
{file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"},
{file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"},
{file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"},
{file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"},
{file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"},
{file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"},
{file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"},
{file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"},
{file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"},
{file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"},
{file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"},
{file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"},
{file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"},
{file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"},
{file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"},
{file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
zipp = [
{file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
{file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},
]

32
py/pyproject.toml Normal file
View File

@ -0,0 +1,32 @@
[tool.poetry]
name = "mango-service-v3-python-client"
version = "0.1.1"
description = ""
authors = ["microwavedcola1 <microwavedcola@gmail.com>"]
packages = [
{ include = "mango_service_v3_py" }
]
license = "MIT"
readme = "README.md"
homepage = ""
repository = ""
keywords = []
classifiers = [
]
include = [
"LICENSE"
]
[tool.poetry.dependencies]
python = "^3.7"
httpx = "^0.19.0"
pydantic = "^1.8.2"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
black = "^19.10b0"
tenacity = "^8.0.1"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

47
py/set_stink_bids.py Normal file
View File

@ -0,0 +1,47 @@
from mango_service_v3_py.api import Exchange
from mango_service_v3_py.dtos import PlacePerpOrder
MARKET = "BTC-PERP"
def fibonacci_of(n):
if n in {0, 1}:
return n
return fibonacci_of(n - 1) + fibonacci_of(n - 2)
if __name__ == "__main__":
exchange = Exchange()
exchange.delete_all_orders()
balances = exchange.get_balances()
total_usd_balance = sum([balance.usd_value for balance in balances])
market = exchange.get_market_by_market_name(MARKET)[0]
lowest = 25
fibs = [fib for fib in [fibonacci_of(n) for n in range(10)] if fib < lowest][1:]
fibs_sum = sum(fibs)
for i, fib in enumerate(fibs):
print((100 - fibs[-1] + fib) / 100)
price = market.last * (100 - fibs[-1] + fib) / 100
size = (total_usd_balance / market.price) * (fibs[len(fibs) - 1 - i] / fibs_sum)
if size < market.size_increment:
continue
print(f"setting order, price: {price}, size: {size}, value: {price * size}")
exchange.place_order(
PlacePerpOrder(
market=MARKET,
side="buy",
price=price,
type="limit",
size=size,
reduce_only=False,
ioc=False,
post_only=False,
client_id=123,
)
)
print(exchange.get_orders())

View File

@ -1,364 +0,0 @@
import Controller from "./controller.interface";
import MangoSimpleClient from "./mango.simple.client";
import {
BookSide,
BookSideLayout,
getAllMarkets,
MarketConfig,
PerpMarket,
PerpOrder,
} from "@blockworks-foundation/mango-client";
import { Market, Orderbook } from "@project-serum/serum";
import { Order } from "@project-serum/serum/lib/market";
import { AccountInfo } from "@solana/web3.js";
import { NextFunction, Request, Response, Router } from "express";
class MarketsController implements Controller {
public path = "/markets";
public router = Router();
constructor(public mangoMarkets: MangoSimpleClient) {
this.initializeRoutes();
}
private initializeRoutes() {
// GET /markets
this.router.get(this.path, this.getMarkets);
// todo GET /markets/{market_name}
// GET /markets/{market_name}/orderbook?depth={depth}
this.router.get(`${this.path}/:market_name/orderbook`, this.getOrderBook);
// GET /markets/{market_name}/trades
this.router.get(`${this.path}/:market_name/trades`, this.getTrades);
// GET /markets/{market_name}/candles?resolution={resolution}&start_time={start_time}&end_time={end_time}
this.router.get(`${this.path}/:market_name/candles`, this.getCandles);
}
private getMarkets = async (
request: Request,
response: Response,
next: NextFunction
) => {
let allMarketConfigs = getAllMarkets(this.mangoMarkets.mangoGroupConfig);
const marketDtos = [];
for (const marketConfig of allMarketConfigs) {
marketDtos.push({
name: marketConfig.name,
baseCurrency: marketConfig.baseSymbol,
quoteCurrency: "USDC",
quoteVolume24h: await getVolumeForMarket(marketConfig),
change1h: undefined,
change24h: undefined,
changeBod: undefined,
highLeverageFeeExempt: undefined,
minProvideSize: undefined,
type: marketConfig.name.includes("PERP") ? "futures" : "spot",
underlying: marketConfig.baseSymbol,
enabled: undefined,
ask: undefined,
bid: undefined,
last: undefined,
postOnly: undefined,
price: undefined,
priceIncrement: undefined,
sizeIncrement: undefined,
restricted: undefined,
volumeUsd24h: await getVolumeForMarket(marketConfig),
} as MarketDto);
}
response.send({ success: true, result: marketDtos } as MarketsDto);
};
private getOrderBook = async (
request: Request,
response: Response,
next: NextFunction
) => {
let marketName = request.params.market_name;
const depth = Number(request.query.depth) || 20;
const ordersInfo = await this.mangoMarkets.getAllBidsAndAsks(
false,
marketName
);
let bids = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side == "buy")
.sort((b1, b2) => b2.order.price - b1.order.price);
let asks = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side == "sell")
.sort((a1, a2) => a1.order.price - a2.order.price);
response.send({
success: true,
result: {
asks: asks
.slice(0, depth)
.map((ask) => [ask.order.price, ask.order.size]),
bids: bids
.slice(0, depth)
.map((bid) => [bid.order.price, bid.order.size]),
},
} as OrdersDto);
};
private getTrades = async (
request: Request,
response: Response,
next: NextFunction
) => {
const allMarketConfigs = getAllMarkets(this.mangoMarkets.mangoGroupConfig);
const marketName = request.params.market_name;
const marketPk = allMarketConfigs
.filter((marketConfig) => marketConfig.name === marketName)[0]
.publicKey.toBase58();
const tradesResponse = await fetch(
`https://serum-history.herokuapp.com/trades/address/${marketPk}`
);
const parsedTradesResponse = tradesResponse.json() as any;
const tradeDtos = parsedTradesResponse["data"].map((trade: any) => {
return {
id: trade["orderId"],
liquidation: undefined,
price: trade["price"],
side: trade["side"],
size: trade["size"],
time: new Date(trade["time"]),
} as TradeDto;
});
response.send({ success: true, result: tradeDtos } as TradesDto);
};
private getCandles = async (
request: Request,
response: Response,
next: NextFunction
) => {
const marketName = request.params.market_name;
const resolution = request.query.resolution;
const fromEpochS = request.query.start_time;
const toEpochS = request.query.end_time;
const historyResponse = await fetch(
`https://serum-history.herokuapp.com/tv/history` +
`?symbol=${marketName}&resolution=${resolution}` +
`&from=${fromEpochS}&to=${toEpochS}`
);
const { time, open, high, low, close, volume } =
(await historyResponse.json()) as any;
const ohlcvDtos: OhlcvDto[] = [];
for (let i = 0; i < time.length; i++) {
ohlcvDtos.push({
time: time[i],
open: open[i],
high: high[i],
low: low[i],
close: close[i],
volume: volume[i],
} as OhlcvDto);
}
response.send({ success: true, result: ohlcvDtos } as OhlcvsDto);
};
}
export default MarketsController;
/// helper functions
export async function getVolumeForMarket(marketConfig: MarketConfig) {
const perpVolume = await fetch(
`https://event-history-api.herokuapp.com/stats/perps/${marketConfig.publicKey.toString()}`
);
const parsedPerpVolume = await perpVolume.json();
return parsedPerpVolume?.data?.volume;
}
export function parseSpotOrders(
market: Market,
config: MarketConfig,
accountInfos: { [key: string]: AccountInfo<Buffer> }
): { bids: Order[]; asks: Order[] } {
const bidData = accountInfos[market["_decoded"].bids.toBase58()]?.data;
const askData = accountInfos[market["_decoded"].asks.toBase58()]?.data;
const bidOrderBook =
market && bidData ? Orderbook.decode(market, bidData) : ([] as Order[]);
const askOrderBook =
market && askData ? Orderbook.decode(market, askData) : ([] as Order[]);
return { bids: [...bidOrderBook], asks: [...askOrderBook] };
}
export function parsePerpOpenOrders(
market: PerpMarket,
config: MarketConfig,
accountInfos: { [key: string]: AccountInfo<Buffer> }
): { bids: PerpOrder[]; asks: PerpOrder[] } {
const bidData = accountInfos[market.bids.toBase58()]?.data;
const askData = accountInfos[market.asks.toBase58()]?.data;
const bidOrderBook =
market && bidData
? new BookSide(market.bids, market, BookSideLayout.decode(bidData))
: ([] as PerpOrder[]);
const askOrderBook =
market && askData
? new BookSide(market.asks, market, BookSideLayout.decode(askData))
: ([] as PerpOrder[]);
return { bids: [...bidOrderBook], asks: [...askOrderBook] };
}
/// Dtos
// e.g.
// {
// "success": true,
// "result": [
// {
// "name": "BTC-0628",
// "baseCurrency": null,
// "quoteCurrency": null,
// "quoteVolume24h": 28914.76,
// "change1h": 0.012,
// "change24h": 0.0299,
// "changeBod": 0.0156,
// "highLeverageFeeExempt": false,
// "minProvideSize": 0.001,
// "type": "future",
// "underlying": "BTC",
// "enabled": true,
// "ask": 3949.25,
// "bid": 3949,
// "last": 3949.00,
// "postOnly": false,
// "price": 10579.52,
// "priceIncrement": 0.25,
// "sizeIncrement": 0.0001,
// "restricted": false,
// "volumeUsd24h": 28914.76
// }
// ]
// }
interface MarketsDto {
success: boolean;
result: MarketDto[];
}
interface MarketDto {
name: string;
baseCurrency: string;
quoteCurrency: string;
quoteVolume24h: number;
change1h: number;
change24h: number;
changeBod: number;
highLeverageFeeExempt: boolean;
minProvideSize: number;
type: string;
underlying: string;
enabled: boolean;
ask: number;
bid: number;
last: number;
postOnly: boolean;
price: number;
priceIncrement: number;
sizeIncrement: number;
restricted: boolean;
volumeUsd24h: number;
}
// e.g.
// {
// "success": true,
// "result": {
// "asks": [
// [
// 4114.25,
// 6.263
// ]
// ],
// "bids": [
// [
// 4112.25,
// 49.29
// ]
// ]
// }
// }
interface OrdersDto {
success: boolean;
result: {
asks: number[][];
bids: number[][];
};
}
// e.g.
// {
// "success": true,
// "result": [
// {
// "id": 3855995,
// "liquidation": false,
// "price": 3857.75,
// "side": "buy",
// "size": 0.111,
// "time": "2019-03-20T18:16:23.397991+00:00"
// }
// ]
// }
interface TradesDto {
success: boolean;
result: TradeDto[];
}
interface TradeDto {
id: string;
liquidation: boolean;
price: number;
side: string;
size: number;
time: Date;
}
// e.g.
// {
// "success": true,
// "result": [
// {
// "close": 11055.25,
// "high": 11089.0,
// "low": 11043.5,
// "open": 11059.25,
// "startTime": "2019-06-24T17:15:00+00:00",
// "volume": 464193.95725
// }
// ]
// }
interface OhlcvsDto {
success: boolean;
result: OhlcvDto[];
}
interface OhlcvDto {
time: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
}

View File

@ -1,215 +0,0 @@
import Controller from "./controller.interface";
import MangoSimpleClient from "./mango.simple.client";
import { OrderInfo } from "./types";
import { PerpOrder } from "@blockworks-foundation/mango-client";
import { Order } from "@project-serum/serum/lib/market";
import { NextFunction, Request, Response, Router } from "express";
class OrdersController implements Controller {
public path = "/orders";
public router = Router();
constructor(public mangoSimpleClient: MangoSimpleClient) {
this.initializeRoutes();
}
private initializeRoutes() {
// GET /orders?market={market_name}
this.router.get(this.path, this.getOpenOrders);
// POST /orders
this.router.post(this.path, this.placeOrder);
// DELETE /orders
this.router.delete(this.path, this.cancelAllOrders);
// DELETE /orders/{order_id}
this.router.delete(`${this.path}/:order_id`, this.cancelOrderByOrderId);
// DELETE /orders/by_client_id/{client_order_id}
this.router.delete(
`${this.path}/by_client_id/:client_id`,
this.cancelOrderByClientId
);
}
private getOpenOrders = async (
request: Request,
response: Response,
next: NextFunction
) => {
const openOrders = await this.mangoSimpleClient.getAllBidsAndAsks(
true,
request.query.market ? String(request.query.market) : undefined
);
const orderDtos = openOrders.flat().map((orderInfo: OrderInfo) => {
if ("bestInitial" in orderInfo.order) {
const order_ = orderInfo.order as PerpOrder;
return {
createdAt: new Date(order_.timestamp.toNumber()),
filledSize: undefined,
future: orderInfo.market.config.name,
id: order_.orderId.toString(),
market: orderInfo.market.config.name,
price: order_.price,
avgFillPrice: undefined,
remainingSize: undefined,
side: order_.side,
size: order_.size,
status: "open",
type: undefined,
reduceOnly: undefined,
ioc: undefined,
postOnly: undefined,
clientId:
order_.clientId && order_.clientId.toString() !== "0"
? order_.clientId.toString()
: undefined,
} as OrderDto;
}
const order_ = orderInfo.order as Order;
return {
createdAt: undefined,
filledSize: undefined,
future: orderInfo.market.config.name,
id: order_.orderId.toString(),
market: orderInfo.market.config.name,
price: order_.price,
avgFillPrice: undefined,
remainingSize: undefined,
side: order_.side,
size: order_.size,
status: "open",
type: undefined,
reduceOnly: undefined,
ioc: undefined,
postOnly: undefined,
clientId:
order_.clientId && order_.clientId.toString() !== "0"
? order_.clientId.toString()
: undefined,
} as OrderDto;
});
response.send({ success: true, result: orderDtos } as OrdersDto);
};
private placeOrder = async (
request: Request,
response: Response,
next: NextFunction
) => {
const placeOrderDto = <PlaceOrderDto>request.body;
await this.mangoSimpleClient.placeOrder(
placeOrderDto.market,
placeOrderDto.type,
placeOrderDto.side,
placeOrderDto.size,
placeOrderDto.price,
placeOrderDto.ioc ? "ioc" : placeOrderDto.postOnly ? "postOnly" : "limit"
);
};
private cancelAllOrders = async (
request: Request,
response: Response,
next: NextFunction
) => {
await this.mangoSimpleClient.cancelAllOrders();
};
private cancelOrderByOrderId = async (
request: Request,
response: Response,
next: NextFunction
) => {
let order_id = request.params.order_id;
await this.mangoSimpleClient.cancelOrderByOrderId(order_id);
};
private cancelOrderByClientId = async (
request: Request,
response: Response,
next: NextFunction
) => {
let client_id = request.params.client_id;
await this.mangoSimpleClient.cancelOrderByClientId(client_id);
};
}
export default OrdersController;
/// Dtos
// e.g.
// {
// "success": true,
// "result": [
// {
// "createdAt": "2019-03-05T09:56:55.728933+00:00",
// "filledSize": 10,
// "future": "XRP-PERP",
// "id": 9596912,
// "market": "XRP-PERP",
// "price": 0.306525,
// "avgFillPrice": 0.306526,
// "remainingSize": 31421,
// "side": "sell",
// "size": 31431,
// "status": "open",
// "type": "limit",
// "reduceOnly": false,
// "ioc": false,
// "postOnly": false,
// "clientId": null
// }
// ]
// }
interface OrdersDto {
success: boolean;
result: OrderDto[];
}
interface OrderDto {
createdAt: Date;
filledSize: number;
future: string;
id: string;
market: string;
price: number;
avgFillPrice: number;
remainingSize: number;
side: string;
size: number;
status: string;
type: string;
reduceOnly: boolean;
ioc: boolean;
postOnly: boolean;
clientId: null;
}
// e.g.
// {
// "market": "XRP-PERP",
// "side": "sell",
// "price": 0.306525,
// "type": "limit",
// "size": 31431.0,
// "reduceOnly": false,
// "ioc": false,
// "postOnly": false,
// "clientId": null
// }
interface PlaceOrderDto {
market: string;
side: "sell" | "buy";
price: number;
type: "limit" | "market";
size: number;
reduceOnly: boolean;
ioc: boolean;
postOnly: boolean;
clientId: string;
}

View File

@ -1,10 +0,0 @@
import App from "./app";
import { cleanEnv, port } from "envalid";
cleanEnv(process.env, {
PORT: port({ default: 3000 }),
});
const app = new App();
app.listen();

View File

@ -1,15 +0,0 @@
import { I80F48 } from "@blockworks-foundation/mango-client/lib/src/fixednum";
export const i80f48ToPercent = (value: I80F48) =>
value.mul(I80F48.fromNumber(100));
export function zipDict<K extends string | number | symbol, V>(
keys: K[],
values: V[]
): Partial<Record<K, V>> {
const result: Partial<Record<K, V>> = {};
keys.forEach((key, index) => {
result[key] = values[index];
});
return result;
}

View File

@ -1,3 +0,0 @@
{
"extends": ["tslint:latest", "tslint-config-prettier"]
}