Skip to content

Commit

Permalink
feat: add create DCA bot page
Browse files Browse the repository at this point in the history
  • Loading branch information
bludnic committed Oct 24, 2024
1 parent 8d80bb6 commit 01efd24
Show file tree
Hide file tree
Showing 25 changed files with 414 additions and 9 deletions.
14 changes: 8 additions & 6 deletions packages/bot-templates/src/templates/dca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function* dca(ctx: TBotContext<DCABotConfig>) {
quantity: settings.entry.quantity,
tpPercent: settings.tp.percent / 100,
safetyOrders: settings.safetyOrders.map((so) => ({
relativePrice: -so.deviation / 100,
relativePrice: -so.priceDeviation / 100,
quantity: so.quantity,
})),
});
Expand All @@ -33,7 +33,7 @@ export function* dca(ctx: TBotContext<DCABotConfig>) {
quantity: settings.entry.quantity,
tpPercent: settings.tp.percent / 100,
safetyOrders: settings.safetyOrders.map((so) => ({
relativePrice: -so.deviation / 100,
relativePrice: -so.priceDeviation / 100,
quantity: so.quantity,
})),
},
Expand All @@ -48,6 +48,7 @@ dca.schema = z.object({
quantity: z.number().positive().describe("Quantity of the Entry Order in base currency").default(0.001),
type: z.nativeEnum(XOrderType).describe("Entry with Limit or Market order").default(XOrderType.Market),
price: z.number().optional(),
conditions: z.any().optional(), // @todo schema validation
}),
tp: z.object({
percent: z.number().positive().describe("Take Profit from entry order price in %").default(3),
Expand All @@ -56,15 +57,16 @@ dca.schema = z.object({
.array(
z.object({
quantity: z.number().positive().positive("Quantity of the Safety Order in base currency"),
deviation: z.number().positive().positive("Price deviation from the Entry Order price in %"),
priceDeviation: z.number().positive().positive("Price deviation from the Entry Order price in %"),
}),
)
.default([
{ quantity: 0.002, deviation: 1 },
{ quantity: 0.003, deviation: 2 },
{ quantity: 0.004, deviation: 3 },
{ quantity: 0.002, priceDeviation: 1 },
{ quantity: 0.003, priceDeviation: 2 },
{ quantity: 0.004, priceDeviation: 3 },
]),
});

dca.runPolicy = {
onOrderFilled: true,
};
Expand Down
138 changes: 138 additions & 0 deletions packages/db/src/extension/models/dca-bot.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { DefaultArgs, GetFindResult, InternalArgs } from "@prisma/client/runtime/library";

import type { XBotType } from "@opentrader/types";
import { TBotState, ZDcaBotSettings } from "../../types/index.js";
import { TDcaBotSettings } from "../../types/dca-bot/index.js";

type NarrowBotType<ExtArgs extends InternalArgs, T> = Omit<
Awaited<GetFindResult<Prisma.$BotPayload<ExtArgs>, T, {}>>,
"settings" | "state"
> & {
settings: TDcaBotSettings;
state: TBotState;
};

const BOT_TYPE = "DcaBot" satisfies XBotType;

export const dcaBotModel = <ExtArgs extends InternalArgs = DefaultArgs>(prisma: PrismaClient) => ({
async findUnique<T extends Prisma.BotFindUniqueArgs<ExtArgs>>(
args: Prisma.SelectSubset<T, Prisma.BotFindUniqueArgs<ExtArgs>>,
) {
const bot = await prisma.bot.findUnique<T>({
...args,
where: {
...args.where,
type: BOT_TYPE,
},
});

if (!bot) return null;

if ("settings" in bot) {
(bot as any).settings = ZDcaBotSettings.parse(JSON.parse(bot.settings));
}
if ("state" in bot) {
(bot as any).state = JSON.parse(bot.state) as TBotState;
}

return bot as unknown as NarrowBotType<ExtArgs, T>;
},
async findUniqueOrThrow<T extends Prisma.BotFindUniqueOrThrowArgs>(
args: Prisma.SelectSubset<T, Prisma.BotFindUniqueOrThrowArgs>,
) {
const bot = await prisma.bot.findUniqueOrThrow<T>({
...args,
where: {
...args.where,
type: BOT_TYPE,
},
});

if ("settings" in bot) {
(bot as any).settings = ZDcaBotSettings.parse(JSON.parse(bot.settings));
}
if ("state" in bot) {
(bot as any).state = JSON.parse(bot.state) as TBotState;
}

return bot as unknown as NarrowBotType<ExtArgs, T>;
},
async findFirstOrThrow<T extends Prisma.BotFindFirstOrThrowArgs>(
args: Prisma.SelectSubset<T, Prisma.BotFindFirstOrThrowArgs>,
) {
if (args.where) {
args.where.type = BOT_TYPE;
}

const bot = await prisma.bot.findFirstOrThrow<T>(args);

if ("settings" in bot) {
(bot as any).settings = ZDcaBotSettings.parse(JSON.parse(bot.settings));
}
if ("state" in bot) {
(bot as any).state = JSON.parse(bot.state) as TBotState;
}

return bot as unknown as NarrowBotType<ExtArgs, T>;
},
async findMany<T extends Prisma.BotFindManyArgs>(args: Prisma.SelectSubset<T, Prisma.BotFindManyArgs>) {
if (args.where) {
args.where.type = BOT_TYPE;
}
const bots = await prisma.bot.findMany<T>(args);

return bots.map((bot) => {
if ("settings" in bot) {
(bot as any).settings = ZDcaBotSettings.parse(JSON.parse(bot.settings));
}
if ("state" in bot) {
(bot as any).state = JSON.parse(bot.state) as TBotState;
}

return bot as unknown as NarrowBotType<ExtArgs, T>;
});
},
async create<T extends Prisma.BotCreateArgs>(args: Prisma.SelectSubset<T, Prisma.BotCreateArgs>) {
ZDcaBotSettings.parse(JSON.parse(args.data.settings));

const bot = await prisma.bot.create<T>({
...args,
data: {
...args.data,
type: BOT_TYPE,
},
});

if ("settings" in bot) {
(bot as any).settings = ZDcaBotSettings.parse(JSON.parse(bot.settings));
}
if ("state" in bot) {
(bot as any).state = JSON.parse(bot.state) as TBotState;
}

return bot as unknown as NarrowBotType<ExtArgs, T>;
},
async update<T extends Prisma.BotUpdateArgs>(args: Prisma.SelectSubset<T, Prisma.BotUpdateArgs>) {
if (typeof args.data.settings === "string") {
ZDcaBotSettings.parse(JSON.parse(args.data.settings));
}

const bot = await prisma.bot.update<T>({
...args,
where: {
...args.where,
type: BOT_TYPE,
},
});

if ("settings" in bot) {
(bot as any).settings = ZDcaBotSettings.parse(JSON.parse(bot.settings));
}
if ("state" in bot) {
(bot as any).state = JSON.parse(bot.state) as TBotState;
}

return bot as unknown as NarrowBotType<ExtArgs, T>;
},
});
14 changes: 14 additions & 0 deletions packages/db/src/types/dca-bot/dca-bot.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from "zod";

import { zt } from "@opentrader/prisma";
import { ZBotState } from "../bot/bot-state.schema.js";

export const ZDcaBotSettings = z.any(); // @todo use `dca.schema` from @opentrader/bot-templates

export const ZDcaBot = zt.BotSchema.extend({
settings: ZDcaBotSettings,
state: ZBotState,
});

export type TDcaBot = z.infer<typeof ZDcaBot>;
export type TDcaBotSettings = z.infer<typeof ZDcaBotSettings>;
1 change: 1 addition & 0 deletions packages/db/src/types/dca-bot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./dca-bot.schema.js";
3 changes: 2 additions & 1 deletion packages/db/src/types/grid-bot/grid-bot.schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { zt } from "@opentrader/prisma";
import type { z } from "zod";

import { zt } from "@opentrader/prisma";
import { ZGridBotSettings } from "./grid-bot-settings.schema.js";
import { ZBotState } from "../bot/bot-state.schema.js";

Expand Down
1 change: 1 addition & 0 deletions packages/db/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./exchange-account/index.js";
export * from "./bot/index.js";
export * from "./grid-bot/index.js";
export * from "./dca-bot/index.js";
export * from "./bot-logs/index.js";
export * from "./order/index.js";
export * from "./smart-trade/index.js";
2 changes: 2 additions & 0 deletions packages/db/src/xprisma.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrismaClient } from "@prisma/client";
import { MarketData, StrategyAction, StrategyError, MarketEventType } from "@opentrader/types";
import { dcaBotModel } from "./extension/models/dca-bot.model.js";
import { gridBotModel } from "./extension/models/grid-bot.model.js";
import { orderModel } from "./extension/models/order.model.js";
import { smartTradeModel } from "./extension/models/smart-trade.model.js";
Expand All @@ -22,6 +23,7 @@ const xprismaClient = prismaClient.$extends({
model: {
bot: {
grid: gridBotModel(prismaClient),
dca: dcaBotModel(prismaClient),
custom: customBotModel(prismaClient),

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/trpc/src/routers/appRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
botRouter,
cronRouter,
exchangeAccountsRouter,
dcaBotRouter,
gridBotRouter,
smartTradeRouter,
symbolsRouter,
Expand All @@ -13,6 +14,7 @@ export const appRouter = trpc.router({
exchangeAccount: exchangeAccountsRouter,
symbol: symbolsRouter,
bot: botRouter,
dcaBot: dcaBotRouter,
gridBot: gridBotRouter,
smartTrade: smartTradeRouter,
cron: cronRouter,
Expand Down
63 changes: 63 additions & 0 deletions packages/trpc/src/routers/private/dca-bot/create-bot/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { TRPCError } from "@trpc/server";

import { XBotType } from "@opentrader/types";
import { xprisma } from "@opentrader/db";
import { dca } from "@opentrader/bot-templates";
import type { Context } from "../../../../utils/context.js";
import type { TCreateDcaBotInputSchema } from "./schema.js";

type Options = {
ctx: {
user: NonNullable<Context["user"]>;
};
input: TCreateDcaBotInputSchema;
};

export async function createDcaBot({ ctx, input }: Options) {
const { exchangeAccountId, data } = input;

const exchangeAccount = await xprisma.exchangeAccount.findUnique({
where: {
id: exchangeAccountId,
owner: {
id: ctx.user.id,
},
},
});

if (!exchangeAccount) {
throw new TRPCError({
message: "Exchange Account doesn't exists",
code: "NOT_FOUND",
});
}

const parsed = dca.schema.safeParse(data.settings);
if (!parsed.success) {
throw new TRPCError({
message: `Invalid strategy params: ${parsed.error.message}`,
code: "PARSE_ERROR",
});
}

const bot = await xprisma.bot.dca.create({
data: {
...data,
settings: JSON.stringify(data.settings),
type: "DcaBot" satisfies XBotType,
template: "dca",
exchangeAccount: {
connect: {
id: exchangeAccount.id,
},
},
owner: {
connect: {
id: ctx.user.id,
},
},
},
});

return bot;
}
14 changes: 14 additions & 0 deletions packages/trpc/src/routers/private/dca-bot/create-bot/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from "zod";

import { ZDcaBot } from '@opentrader/db';

export const ZCreateDcaBotInputSchema = z.object({
exchangeAccountId: z.number(),
data: ZDcaBot.pick({
name: true,
symbol: true,
settings: true,
}),
});

export type TCreateDcaBotInputSchema = z.infer<typeof ZCreateDcaBotInputSchema>;
23 changes: 23 additions & 0 deletions packages/trpc/src/routers/private/dca-bot/get-bot/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { xprisma } from "@opentrader/db";
import type { Context } from "../../../../utils/context.js";
import type { TGetDcaBotInputSchema } from "./schema.js";

type Options = {
ctx: {
user: NonNullable<Context["user"]>;
};
input: TGetDcaBotInputSchema;
};

export async function getDcaBot({ ctx, input: id }: Options) {
const bot = await xprisma.bot.dca.findUniqueOrThrow({
where: {
id,
owner: {
id: ctx.user.id,
},
},
});

return bot;
}
5 changes: 5 additions & 0 deletions packages/trpc/src/routers/private/dca-bot/get-bot/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from "zod";

export const ZGetDcaBotInputSchema = z.number();

export type TGetDcaBotInputSchema = z.infer<typeof ZGetDcaBotInputSchema>;
23 changes: 23 additions & 0 deletions packages/trpc/src/routers/private/dca-bot/get-bots/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { xprisma } from "@opentrader/db";
import type { Context } from "../../../../utils/context.js";

type Options = {
ctx: {
user: NonNullable<Context["user"]>;
};
};

export async function getDcaBots({ ctx }: Options) {
const bots = await xprisma.bot.dca.findMany({
where: {
owner: {
id: ctx.user.id,
},
},
orderBy: {
createdAt: "desc",
},
});

return bots;
}
Loading

0 comments on commit 01efd24

Please sign in to comment.