Skip to content

Commit

Permalink
feat: implement DCA trades (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
bludnic committed Oct 6, 2024
1 parent 846334c commit 0489049
Show file tree
Hide file tree
Showing 22 changed files with 539 additions and 152 deletions.
40 changes: 37 additions & 3 deletions packages/bot-processor/src/effect-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
* Repository URL: https://github.com/bludnic/opentrader
*/
import { rsi } from "@opentrader/indicators";
import { OrderStatusEnum, OrderType } from "@opentrader/types";
import { OrderStatusEnum, OrderType, XEntityType, XOrderSide } from "@opentrader/types";
import { TradeService, SmartTradeService } from "./types/index.js";
import type { TBotContext } from "./types/index.js";
import type { BaseEffect, EffectType } from "./effects/types/index.js";
import type {
import { BaseEffect, EffectType, USE_DCA } from "./effects/types/index.js";
import {
buy,
useTrade,
useArbTrade,
Expand All @@ -34,6 +34,7 @@ import type {
useMarket,
useCandle,
useRSI,
useDca,
} from "./effects/index.js";
import {
BUY,
Expand Down Expand Up @@ -63,6 +64,7 @@ export const effectRunnerMap: Record<
[REPLACE_SMART_TRADE]: runReplaceSmartTradeEffect,
[USE_TRADE]: runUseTradeEffect,
[USE_ARB_TRADE]: runUseArbTradeEffect,
[USE_DCA]: runUseDcaEffect,
[BUY]: runBuyEffect,
[SELL]: runSellEffect,
[USE_EXCHANGE]: runUseExchangeEffect,
Expand Down Expand Up @@ -207,6 +209,38 @@ async function runUseArbTradeEffect(effect: ReturnType<typeof useArbTrade>, ctx:
return new SmartTradeService(effect.ref, smartTrade);
}

async function runUseDcaEffect(effect: ReturnType<typeof useDca>, ctx: TBotContext<any>) {
const { payload, ref } = effect;

const smartTrade = await ctx.control.getOrCreateSmartTrade(ref, {
type: "DCA",
quantity: payload.quantity,
buy: {
symbol: payload.symbol,
type: payload.price ? OrderType.Limit : OrderType.Market,
price: payload.price,
status: OrderStatusEnum.Idle,
},
sell: {
symbol: payload.symbol,
type: OrderType.Limit,
relativePrice: payload.tpPercent,
status: OrderStatusEnum.Idle,
},
// Safety orders
additionalOrders: payload.safetyOrders.map((order) => ({
relativePrice: order.relativePrice,
quantity: order.quantity,
symbol: payload.symbol,
type: OrderType.Limit,
side: XOrderSide.Buy,
entityType: XEntityType.SafetyOrder,
})),
});

return new SmartTradeService(effect.ref, smartTrade);
}

async function runBuyEffect(effect: ReturnType<typeof buy>, ctx: TBotContext<any>) {
const { payload, ref } = effect;

Expand Down
1 change: 1 addition & 0 deletions packages/bot-processor/src/effects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export * from "./useExchange.js";
export * from "./useIndicators.js";
export * from "./useTrade.js";
export * from "./useArbTrade.js";
export * from "./useDca.js";
export * from "./market.js";
export * from "./indicators.js";
2 changes: 2 additions & 0 deletions packages/bot-processor/src/effects/types/effect-types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const USE_SMART_TRADE = "USE_SMART_TRADE";
export const USE_TRADE = "USE_TRADE";
export const USE_ARB_TRADE = "USE_ARB_TRADE";
export const USE_DCA = "USE_DCA";
export const BUY = "BUY";
export const SELL = "SELL";
export const REPLACE_SMART_TRADE = "REPLACE_SMART_TRADE";
Expand All @@ -17,6 +18,7 @@ export type EffectType =
| typeof USE_SMART_TRADE
| typeof USE_TRADE
| typeof USE_ARB_TRADE
| typeof USE_DCA
| typeof BUY
| typeof SELL
| typeof REPLACE_SMART_TRADE
Expand Down
34 changes: 34 additions & 0 deletions packages/bot-processor/src/effects/useDca.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { USE_DCA } from "./types/index.js";
import { makeEffect } from "./utils/index.js";

type SafetyOrder = {
quantity: number;
relativePrice: number;
};

type UseDcaPayload = {
/**
* Entry order quantity
*/
quantity: number;
/**
* The Limit entry price of the order. If not provided, a market order will be placed.
*/
price?: number;
/**
* The Limit exit price of the order (e.g. 0.01 is 10%)
*/
tpPercent: number;
/**
* Safety orders to be placed when price goes down
*/
safetyOrders: SafetyOrder[];
/**
* The symbol to trade, e.g. BTC/USDT.
*/
symbol?: string;
};

export function useDca(params: UseDcaPayload, ref = "0") {
return makeEffect(USE_DCA, params, ref);
}
15 changes: 14 additions & 1 deletion packages/bot-processor/src/types/store/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OrderStatusEnum, OrderType, XSmartTradeType } from "@opentrader/types";
import { OrderStatusEnum, OrderType, XEntityType, XOrderSide, XSmartTradeType } from "@opentrader/types";

export type OrderPayload = {
/**
Expand All @@ -12,11 +12,24 @@ export type OrderPayload = {
type: OrderType;
status?: OrderStatusEnum; // default to Idle
price?: number; // if undefined, then it's a market order
/**
* Price deviation relative to entry price.
* If 0.1, the order will be placed as entryPrice + 10%
* If -0.1, the order will be placed as entryPrice - 10%
*/
relativePrice?: number;
};

export interface AdditionalOrderPayload extends OrderPayload {
quantity: number;
entityType: XEntityType;
side: XOrderSide;
}

export type CreateSmartTradePayload = {
type: XSmartTradeType;
buy: OrderPayload;
sell?: OrderPayload;
additionalOrders?: AdditionalOrderPayload[];
quantity: number;
};
31 changes: 31 additions & 0 deletions packages/bot-templates/src/templates/test/dca.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";
import { cancelSmartTrade, TBotContext, useDca } from "@opentrader/bot-processor";
import { logger } from "@opentrader/logger";

export function* testDca(ctx: TBotContext<any>) {
if (ctx.onStart) {
logger.info(`[Test DCA] Bot started`);
}
if (ctx.onStop) {
logger.info(`[Test DCA] Bot stopped`);

yield cancelSmartTrade();

return;
}

logger.info("[Test DCA] Executing strategy template");
yield useDca({
quantity: 0.001,
tpPercent: 0.03, // +3%
safetyOrders: [
{ relativePrice: -0.01, quantity: 0.002 },
{ relativePrice: -0.02, quantity: 0.004 },
{ relativePrice: -0.03, quantity: 0.006 },
],
});
}

testDca.displayName = "Test DCA";
testDca.hidden = true;
testDca.schema = z.object({});
1 change: 1 addition & 0 deletions packages/bot-templates/src/templates/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./rsi.js";
export * from "./state.js";
export * from "./trades.js";
export * from "./testMarketOrder.js";
export * from "./dca.js";
5 changes: 4 additions & 1 deletion packages/bot/src/streams/orders.stream.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { findStrategy } from "@opentrader/bot-templates/server";
import type { ExchangeCode, IWatchOrder, MarketId } from "@opentrader/types";
import { BotProcessing, getWatchers, shouldRunStrategy } from "@opentrader/processing";
import { BotProcessing, getWatchers, shouldRunStrategy, SmartTradeExecutor } from "@opentrader/processing";
import type { OrderWithSmartTrade, ExchangeAccountWithCredentials } from "@opentrader/db";
import { xprisma } from "@opentrader/db";
import { logger } from "@opentrader/logger";
Expand Down Expand Up @@ -104,6 +104,9 @@ export class OrdersStream {
subscribedMarkets,
});
}

const smartTradeExecutor = await SmartTradeExecutor.fromId(order.smartTrade.id);
await smartTradeExecutor.next();
}

private async onOrderCanceled(exchangeOrder: IWatchOrder, order: OrderWithSmartTrade) {
Expand Down
9 changes: 6 additions & 3 deletions packages/db/src/entities/order.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { XOrderStatus, XOrderType } from "@opentrader/types";
import type { Order as OrderModel } from "@prisma/client";

type GenericOrderProps = "type" | "status" | "price" | "filledPrice" | "filledAt" | "placedAt";
type GenericOrderProps = "type" | "status" | "price" | "relativePrice" | "filledPrice" | "filledAt" | "placedAt";

type OrderEntityBuilder<
OrderType extends XOrderType,
Expand All @@ -11,6 +11,7 @@ type OrderEntityBuilder<
type: OrderType;
status: OrderStatus;
price: OrderType extends "Limit" ? NonNullable<O["price"]> : null;
relativePrice: O["relativePrice"];
filledPrice: OrderStatus extends "Filled" ? NonNullable<O["filledPrice"]> : null;
filledAt: OrderStatus extends "Filled" ? NonNullable<O["filledAt"]> : null;
placedAt: OrderStatus extends "Placed" | "Filled" | "Canceled" ? NonNullable<O["placedAt"]> : null;
Expand Down Expand Up @@ -60,7 +61,8 @@ export function toOrderEntity<T extends OrderModel>(order: T): OrderEntity<T> {
type,
status,

price: assertNonNullable(price, "price"),
// Skip price checking for relative price orders
price: baseProps.relativePrice ? price || -1 : assertNonNullable(price, "price"),
filledPrice: null,
filledAt: null,
placedAt: null,
Expand Down Expand Up @@ -105,7 +107,8 @@ export function toOrderEntity<T extends OrderModel>(order: T): OrderEntity<T> {
type,
status,

price: assertNonNullable(price, "price"),
// Skip price checking for relative price orders
price: baseProps.relativePrice ? price || -1 : assertNonNullable(price, "price"),
filledPrice: null,
filledAt: null,
placedAt: null,
Expand Down
Loading

0 comments on commit 0489049

Please sign in to comment.