Merge branch 'dev'

This commit is contained in:
microwavedcola1 2021-09-26 14:37:01 +02:00
commit 905ea647f4
15 changed files with 496 additions and 225 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
mango-service-v3/node_modules
mango-service-v3/dist
mango-service-v3/run-dev-server.sh
mango-service-v3/run-dev-server-devnet.sh
mango-service-v3/run-build-and-publish-docker-image.sh
mango-service-v3/run-build-and-test-docker-locally.sh
mango-service-v3/run-update-api-docs.sh

View File

@ -26,7 +26,6 @@ See Todos
# Todos
## Small
losely sorted in order of importance/priority
- devenet integration tests
- retry framework in python
- error thrown in endpoints just crashes expressjs, need explicit handling
- rpc node related issues

View File

@ -11,4 +11,4 @@ RUN yarn build
ENV PORT=3000
CMD ["pm2-docker", "start", "dist/server.js", "-i", "1", "-n", "mango-service-v3", "--max-memory-restart", "200M"]
CMD ["pm2-docker", "start", "dist/server.js", "-i", "2", "-n", "mango-service-v3", "--max-memory-restart", "200M"]

View File

@ -5,8 +5,14 @@ import {
} from "@blockworks-foundation/mango-client";
import BN from "bn.js";
import Controller from "controller.interface";
import { RequestErrorCustom } from "dtos";
import { NextFunction, Request, Response, Router } from "express";
import MangoSimpleClient from "mango.simple.client";
import {
logger,
patchExternalMarketName,
patchInternalMarketName,
} from "./utils";
class AccountController implements Controller {
public path = "/api/positions";
@ -26,6 +32,19 @@ class AccountController implements Controller {
response: Response,
next: NextFunction
) => {
this.fetchPerpPositionsInternal()
.then((postionDtos) => {
response.send({ success: true, result: postionDtos } as PositionsDto);
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private async fetchPerpPositionsInternal() {
const groupConfig = this.mangoSimpleClient.mangoGroupConfig;
const mangoGroup = this.mangoSimpleClient.mangoGroup;
@ -114,7 +133,7 @@ class AccountController implements Controller {
cumulativeSellSize: undefined,
entryPrice,
estimatedLiquidationPrice: undefined,
future: marketConfig.name,
future: patchInternalMarketName(marketConfig.name),
initialMarginRequirement: undefined,
longOrderSize: undefined,
maintenanceMarginRequirement: undefined,
@ -133,9 +152,8 @@ class AccountController implements Controller {
} as PositionDto;
}
);
response.send({ success: true, result: postionDtos } as PositionsDto);
};
return postionDtos;
}
}
export default AccountController;
@ -182,7 +200,7 @@ interface PositionDto {
cumulativeSellSize: number;
entryPrice: number;
estimatedLiquidationPrice: number;
future: "ETH-PERP";
future: string;
initialMarginRequirement: number;
longOrderSize: number;
maintenanceMarginRequirement: number;

View File

@ -1,6 +1,7 @@
import bodyParser from "body-parser";
import Controller from "controller.interface";
import express from "express";
import { logger } from "./utils";
import AccountController from "./account.controller";
import CoinController from "./coin.controller";
import MangoSimpleClient from "./mango.simple.client";
@ -36,7 +37,9 @@ class App {
}
public listen() {
this.app.listen(process.env.PORT || 3000);
const port = process.env.PORT || 3000;
this.app.listen(port);
logger.info(`App listening on port ${port}`);
}
public getServer() {

View File

@ -5,6 +5,6 @@ export interface BadRequestError {
location: string;
}
export interface BadRequestErrorCustom {
export interface RequestErrorCustom {
msg: string;
}

View File

@ -51,7 +51,7 @@ class MangoSimpleClient {
}
static async create() {
const groupName = "mainnet.1";
const groupName = process.env.GROUP_NAME || "mainnet.1";
const clusterUrl =
process.env.CLUSTER_URL || "https://api.mainnet-beta.solana.com";

View File

@ -5,15 +5,21 @@ import {
PerpMarket,
} from "@blockworks-foundation/mango-client";
import { Market } from "@project-serum/serum";
import { PublicKey } from "@solana/web3.js";
import Big from "big.js";
import { BadRequestError } from "dtos";
import { BadRequestError, RequestErrorCustom } 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";
import {
isValidMarket,
logger,
patchExternalMarketName,
patchInternalMarketName,
} from "./utils";
class MarketsController implements Controller {
public path = "/api/markets";
@ -64,10 +70,19 @@ class MarketsController implements Controller {
response: Response,
next: NextFunction
) => {
response.send({
success: true,
result: await this.fetchMarketsInternal(),
} as MarketsDto);
this.fetchMarketsInternal()
.then((marketsDto) => {
response.send({
success: true,
result: marketsDto,
} as MarketsDto);
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private fetchMarket = async (
@ -82,11 +97,21 @@ class MarketsController implements Controller {
.json({ errors: errors.array() as BadRequestError[] });
}
const marketName = request.params.market_name;
response.send({
success: true,
result: await this.fetchMarketsInternal(marketName),
} as MarketsDto);
const marketName = patchExternalMarketName(request.params.market_name);
this.fetchMarketsInternal(marketName)
.then((marketsDto) => {
response.send({
success: true,
result: marketsDto,
} as MarketsDto);
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private async fetchMarketsInternal(
@ -191,7 +216,7 @@ class MarketsController implements Controller {
}
return {
name: marketConfig.name,
name: patchInternalMarketName(marketConfig.name),
baseCurrency: marketConfig.baseSymbol,
quoteCurrency: "USDC",
quoteVolume24h: volume,
@ -227,34 +252,50 @@ class MarketsController implements Controller {
.json({ errors: errors.array() as BadRequestError[] });
}
const marketName = request.params.market_name;
const marketName = patchExternalMarketName(request.params.market_name);
const depth = Number(request.query.depth) || 20;
this.getOrderBookInternal(marketName, depth)
.then(({ asks, bids }) => {
return response.send({
success: true,
result: {
asks: asks,
bids: bids,
},
});
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private async getOrderBookInternal(marketName: string, depth: number) {
const ordersInfo = await this.mangoSimpleClient.fetchAllBidsAndAsks(
false,
marketName
);
const bids = ordersInfo
const bids_ = ordersInfo
.flat()
.filter((orderInfo) => orderInfo.order.side === "buy")
.sort((b1, b2) => b2.order.price - b1.order.price);
const asks = ordersInfo
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);
};
const asks = asks_
.slice(0, depth)
.map((ask) => [ask.order.price, ask.order.size]);
const bids = bids_
.slice(0, depth)
.map((bid) => [bid.order.price, bid.order.size]);
return { asks, bids };
}
private getTrades = async (
request: Request,
@ -271,33 +312,45 @@ class MarketsController implements Controller {
const allMarketConfigs = getAllMarkets(
this.mangoSimpleClient.mangoGroupConfig
);
const marketName = request.params.market_name;
const marketName = patchExternalMarketName(request.params.market_name);
const marketPk = allMarketConfigs.filter(
(marketConfig) => marketConfig.name === marketName
)[0].publicKey;
this.getTradesInternal(marketPk)
.then((tradeDtos) => {
return response.send({
success: true,
result: tradeDtos,
});
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private async getTradesInternal(marketPk: 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;
});
return [];
}
response.send({ success: true, result: tradeDtos } as TradesDto);
};
return 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;
});
}
private getCandles = async (
request: Request,
@ -311,31 +364,35 @@ class MarketsController implements Controller {
.json({ errors: errors.array() as BadRequestError[] });
}
const marketName = request.params.market_name;
const marketName = patchExternalMarketName(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);
await getOhlcv(marketName, resolution, fromEpochS, toEpochS)
.then(({ t, o, h, l, c, v }) => {
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);
}
return response.send({
success: true,
result: ohlcvDtos,
});
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
}

View File

@ -1,12 +1,17 @@
import { PerpOrder } from "@blockworks-foundation/mango-client";
import { Order } from "@project-serum/serum/lib/market";
import { BadRequestError, BadRequestErrorCustom } from "dtos";
import { BadRequestError, RequestErrorCustom } from "dtos";
import { NextFunction, Request, Response, Router } from "express";
import { body, query, validationResult } from "express-validator";
import Controller from "./controller.interface";
import MangoSimpleClient from "./mango.simple.client";
import { OrderInfo } from "./types";
import { isValidMarket, logger } from "./utils";
import {
isValidMarket,
logger,
patchExternalMarketName,
patchInternalMarketName,
} from "./utils";
class OrdersController implements Controller {
public path = "/api/orders";
@ -67,61 +72,20 @@ class OrdersController implements Controller {
.json({ errors: errors.array() as BadRequestError[] });
}
const openOrders = await this.mangoSimpleClient.fetchAllBidsAndAsks(
true,
request.query.market ? String(request.query.market) : undefined
);
const marketName = request.query.market
? patchExternalMarketName(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: "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);
this.getOpenOrdersInternal(marketName)
.then((orderDtos) => {
return response.send({ success: true, result: orderDtos } as OrdersDto);
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private placeOrder = async (
@ -139,9 +103,9 @@ class OrdersController implements Controller {
const placeOrderDto = request.body as PlaceOrderDto;
logger.info(`placing order`);
try {
await this.mangoSimpleClient.placeOrder(
placeOrderDto.market,
this.mangoSimpleClient
.placeOrder(
patchExternalMarketName(placeOrderDto.market),
placeOrderDto.type,
placeOrderDto.side,
placeOrderDto.size,
@ -152,35 +116,16 @@ class OrdersController implements Controller {
? "postOnly"
: "limit",
placeOrderDto.clientId
);
} catch (error) {
return response.status(400).send({
errors: [{ msg: error.message } as BadRequestErrorCustom],
)
.then(() => {
return response.status(200).send();
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
}
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 (
@ -189,9 +134,17 @@ class OrdersController implements Controller {
next: NextFunction
) => {
logger.info(`cancelling all orders...`);
// todo: leads to 429 if too many orders exist, needs optimization
await this.mangoSimpleClient.cancelAllOrders();
response.send();
this.mangoSimpleClient
.cancelAllOrders()
.then(() => {
return response.status(200).send();
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private cancelOrderByOrderId = async (
@ -212,16 +165,16 @@ class OrdersController implements Controller {
this.mangoSimpleClient
.cancelOrder(orderInfos[0])
.then(() => response.send())
.catch(() => {
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
.status(500)
.json({ errors: [{ msg: error.message }] });
});
})
.catch(() => {
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).json({ errors: [{ msg: error.message }] });
});
};
@ -243,18 +196,76 @@ class OrdersController implements Controller {
this.mangoSimpleClient
.cancelOrder(orderInfos[0])
.then(() => response.send())
.catch(() => {
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
.status(500)
.json({ errors: [{ msg: error.message }] });
});
})
.catch(() => {
return response
.status(400)
.json({ errors: [{ msg: "Unexpected error occured!" }] });
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).json({ errors: [{ msg: error.message }] });
});
};
private async getOpenOrdersInternal(marketName: string) {
const openOrders = await this.mangoSimpleClient.fetchAllBidsAndAsks(
true,
marketName
);
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: patchInternalMarketName(orderInfo.market.config.name),
id: perpOrder.orderId.toString(),
market: patchInternalMarketName(orderInfo.market.config.name),
price: perpOrder.price,
avgFillPrice: undefined,
remainingSize: undefined,
side: perpOrder.side,
size: perpOrder.size,
status: "open",
type: "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: patchInternalMarketName(orderInfo.market.config.name),
id: spotOrder.orderId.toString(),
market: patchInternalMarketName(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;
});
return orderDtos;
}
}
export default OrdersController;

View File

@ -30,7 +30,7 @@ const mangoGroupConfig: GroupConfig = Config.ids().groups.filter(
)[0];
const allMarketNames = mangoGroupConfig.spotMarkets
.map((spotMarketConfig) => spotMarketConfig.name)
.map((spotMarketConfig) => spotMarketConfig.baseSymbol + "-SPOT")
.concat(
mangoGroupConfig.perpMarkets.map(
(perpMarketConfig) => perpMarketConfig.name
@ -41,6 +41,20 @@ const allCoins = mangoGroupConfig.tokens.map(
(tokenConfig) => tokenConfig.symbol
);
export function patchExternalMarketName(marketName: string) {
if (marketName.includes("-SPOT")) {
marketName = marketName.replace("-SPOT", "/USDC");
}
return marketName;
}
export function patchInternalMarketName(marketName: string) {
if (marketName.includes("/USDC")) {
marketName = marketName.replace("/USDC", "-SPOT");
}
return marketName;
}
/// general
export function zipDict<K extends string | number | symbol, V>(

View File

@ -7,13 +7,18 @@ import {
} from "@blockworks-foundation/mango-client";
import { OpenOrders } from "@project-serum/serum";
import Controller from "controller.interface";
import { BadRequestErrorCustom } from "dtos";
import { RequestErrorCustom } from "dtos";
import e, { NextFunction, Request, Response, Router } from "express";
import { body } from "express-validator";
import { sumBy } from "lodash";
import MangoSimpleClient from "mango.simple.client";
import { Balances } from "./types";
import { i80f48ToPercent, isValidCoin } from "./utils";
import {
i80f48ToPercent,
isValidCoin,
logger,
patchInternalMarketName,
} from "./utils";
class WalletController implements Controller {
public path = "/api/wallet";
@ -41,6 +46,22 @@ class WalletController implements Controller {
response: Response,
next: NextFunction
) => {
this.fetchBalancesInternal()
.then((balanceDtos) => {
return response.send({
success: true,
result: balanceDtos,
} as BalancesDto);
})
.catch((error) => {
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};
private async fetchBalancesInternal() {
// local copies of mango objects
const mangoGroupConfig = this.mangoSimpleClient.mangoGroupConfig;
const mangoGroup = this.mangoSimpleClient.mangoGroup;
@ -67,7 +88,7 @@ class WalletController implements Controller {
name,
} of mangoGroupConfig.spotMarkets) {
if (!mangoAccount || !mangoGroup) {
response.send([]);
return [];
}
const openOrders: OpenOrders =
@ -198,11 +219,10 @@ class WalletController implements Controller {
const value = net.mul(mangoGroup.getPrice(tokenIndex, mangoCache));
/* tslint:enable */
////// end of copy pasta block from mango-ui-v3
// append balances for base symbols
const balanceDtos = baseBalances.map((baseBalance) => {
return {
coin: baseBalance.key,
coin: patchInternalMarketName(baseBalance.key),
free: baseBalance.deposits.toNumber(),
spotBorrow: baseBalance.borrows.toNumber(),
total: baseBalance.net.toNumber(),
@ -215,16 +235,17 @@ class WalletController implements Controller {
// append balance for quote symbol
balanceDtos.push({
coin: this.mangoSimpleClient.mangoGroupConfig.quoteSymbol,
coin: patchInternalMarketName(
this.mangoSimpleClient.mangoGroupConfig.quoteSymbol
),
free: quoteMeta.deposits.toNumber(),
spotBorrow: quoteMeta.borrows.toNumber(),
total: net.toNumber(),
usdValue: value.toNumber(),
availableWithoutBorrow: net.sub(quoteMeta.borrows).toNumber(),
});
response.send({ success: true, result: balanceDtos } as BalancesDto);
};
return balanceDtos;
}
private withdraw = async (
request: Request,
@ -238,8 +259,9 @@ class WalletController implements Controller {
response.status(200);
})
.catch((error) => {
return response.status(400).send({
errors: [{ msg: error.message } as BadRequestErrorCustom],
logger.error(`message - ${error.message}, ${error.stack}`);
return response.status(500).send({
errors: [{ msg: error.message } as RequestErrorCustom],
});
});
};

View File

@ -1,16 +0,0 @@
import time
from mango_service_v3_py.api import MangoServiceV3Client
if __name__ == "__main__":
mango_service_v3_client = MangoServiceV3Client()
sleep = 0.5
while True:
try:
resp = mango_service_v3_client.get_balances()
print(resp)
sleep = sleep * 2
time.sleep(sleep)
except:
pass

View File

@ -33,26 +33,29 @@ def delayed(seconds):
class MangoServiceV3Client:
def __init__(self):
if "BASE_URL" in os.environ:
self.BASE_URL = f"{os.environ['BASE_URL']}/api"
def __init__(self, base_url, timeout):
self.timeout = timeout if timeout else 10.0
if base_url:
self.BASE_URL = base_url
else:
self.BASE_URL = "http://localhost:3000/api"
def get_open_positions(self) -> List[Position]:
response = httpx.get(f"{self.BASE_URL}/positions", timeout=10.0)
response = httpx.get(f"{self.BASE_URL}/positions", timeout=self.timeout)
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=10.0)
response = httpx.get(f"{self.BASE_URL}/wallet/balances", timeout=self.timeout)
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=10.0)
response = httpx.get(f"{self.BASE_URL}/markets", timeout=self.timeout)
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=10.0)
response = httpx.get(
f"{self.BASE_URL}/markets/{market_name}", timeout=self.timeout
)
return parse_obj_as(List[Market], json.loads(response.text)["result"])
def get_orderboook(self, market_name: str, depth: int = 30) -> Orderbook:
@ -63,7 +66,7 @@ class MangoServiceV3Client:
def get_trades(self, market_name: str) -> List[Trade]:
response = httpx.get(
f"{self.BASE_URL}/markets/{market_name}/trades", timeout=10.0
f"{self.BASE_URL}/markets/{market_name}/trades", timeout=self.timeout
)
return parse_obj_as(List[Trade], json.loads(response.text)["result"])
@ -76,34 +79,34 @@ class MangoServiceV3Client:
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=10.0)
response = httpx.get(f"{self.BASE_URL}/orders", timeout=self.timeout)
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=10.0
f"{self.BASE_URL}/orders?market={market_name}", timeout=self.timeout
)
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(by_alias=True), timeout=10.0
f"{self.BASE_URL}/orders",
json=order.dict(by_alias=True),
timeout=self.timeout,
)
# if response.status_code == httpx.codes.BAD_REQUEST:
# return parse_obj_as(
# List[BadRequestError], json.loads(response.text)["errors"]
# )
def cancel_order_by_client_id(self, client_id):
response = httpx.delete(
f"{self.BASE_URL}/orders/by_client_id/{client_id}", timeout=10.0
f"{self.BASE_URL}/orders/by_client_id/{client_id}", timeout=self.timeout
)
def cancel_order_by_order_id(self, order_id):
response = httpx.delete(f"{self.BASE_URL}/orders/{order_id}", timeout=10.0)
response = httpx.delete(
f"{self.BASE_URL}/orders/{order_id}", timeout=self.timeout
)
def cancel_all_orders(self):
response = httpx.delete(f"{self.BASE_URL}/orders", timeout=10.0)
response = httpx.delete(f"{self.BASE_URL}/orders", timeout=self.timeout)
@staticmethod
def to_nearest(num, tickDec):

90
py/test-devnet.py Normal file
View File

@ -0,0 +1,90 @@
import pytest
from mango_service_v3_py.api import MangoServiceV3Client
from mango_service_v3_py.dtos import PlaceOrder
@pytest.fixture
def mango_service_v3_client():
return MangoServiceV3Client("http://localhost:3001/api", timeout=60.0)
@pytest.fixture(autouse=True)
def run_around_tests(mango_service_v3_client):
# cleanup
mango_service_v3_client.cancel_all_orders()
orders = mango_service_v3_client.get_orders()
assert len(orders) == 0
yield
# teardown
assert True
def place_order(mango_service_v3_client, market):
mango_service_v3_client.place_order(
PlaceOrder(
market=market,
side="buy",
price=20000,
type="limit",
size=0.0001,
reduce_only=False,
ioc=False,
post_only=False,
client_id=123,
)
)
SPOT_AND_PERP_MARKETS = [("BTC-PERP"), ("BTC-SPOT")]
PERP_MARKET = [("BTC-PERP")]
@pytest.mark.parametrize("market", PERP_MARKET)
def test_get_positions(mango_service_v3_client, market):
positions = mango_service_v3_client.get_open_positions()
assert len(positions) > 0
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_get_balances(mango_service_v3_client, market):
balances = mango_service_v3_client.get_balances()
assert len(balances) > 0
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_place_order(mango_service_v3_client, market):
mango_service_v3_client.cancel_all_orders()
place_order(mango_service_v3_client, market)
orders = mango_service_v3_client.get_orders()
assert len(orders) == 1
order = orders[0]
print(order)
assert order.market == market
assert order.price == 20000
assert order.size == 0.0001
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_cancel_order_by_order_id(mango_service_v3_client, market):
place_order(mango_service_v3_client, market)
orders = mango_service_v3_client.get_orders()
order = orders[0]
mango_service_v3_client.cancel_order_by_order_id(order.id)
orders = mango_service_v3_client.get_orders()
assert len(orders) == 0
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_cancel_order_by_client_id(mango_service_v3_client, market):
place_order(mango_service_v3_client, market)
orders = mango_service_v3_client.get_orders()
order = orders[0]
mango_service_v3_client.cancel_order_by_client_id(order.client_id)
orders = mango_service_v3_client.get_orders()
assert len(orders) == 0

69
py/test-mainnet.py Normal file
View File

@ -0,0 +1,69 @@
import pytest
from mango_service_v3_py.api import MangoServiceV3Client
from mango_service_v3_py.dtos import PlaceOrder
# Note: some endpoints only return useful data for mainnet, this is because the REST API service rely on
# other off chain REST services which only serve data for mainnet
@pytest.fixture
def mango_service_v3_client():
return MangoServiceV3Client("http://localhost:3000/api", timeout=60.0)
@pytest.fixture(autouse=True)
def run_around_tests(mango_service_v3_client):
# cleanup
mango_service_v3_client.cancel_all_orders()
orders = mango_service_v3_client.get_orders()
assert len(orders) == 0
yield
# teardown
assert True
def place_order(mango_service_v3_client, market):
mango_service_v3_client.place_order(
PlaceOrder(
market=market,
side="buy",
price=20000,
type="limit",
size=0.0001,
reduce_only=False,
ioc=False,
post_only=False,
client_id=123,
)
)
SPOT_AND_PERP_MARKETS = [("BTC-PERP"), ("BTC-SPOT")]
PERP_MARKET = [("BTC-PERP")]
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_get_markets(mango_service_v3_client, market):
markets = mango_service_v3_client.get_markets()
assert len(markets) > 0
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_get_candles(mango_service_v3_client, market):
candles = mango_service_v3_client.get_candles(market, 60, 1625922900, 1631214960)
print(candles)
assert len(candles) > 0
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_get_orderbook(mango_service_v3_client, market):
ob = mango_service_v3_client.get_orderboook(market)
assert len(ob.asks) > 0
assert len(ob.bids) > 0
@pytest.mark.parametrize("market", SPOT_AND_PERP_MARKETS)
def test_get_trades(mango_service_v3_client, market):
trades = mango_service_v3_client.get_trades(market)
assert len(trades) > 0