Skip to content

Commit

Permalink
feat: add indicators
Browse files Browse the repository at this point in the history
  • Loading branch information
bludnic committed Jan 21, 2024
1 parent d2b606f commit ed6e6bd
Show file tree
Hide file tree
Showing 35 changed files with 411 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ NEXT_PUBLIC_PROCESSOR_ENABLE_TRPC=
NEXT_PUBLIC_STATIC=
DATABASE_URL="postgresql://postgres:[email protected]:5432/postgres"
ADMIN_PASSWORD=opentrader

NEXT_PUBLIC_CANDLES_SERVICE_API_URL="http://localhost:5001"
NEXT_PUBLIC_CANDLES_SERVICE_API_KEY="opentrader"
########## SHARED END ######
4 changes: 2 additions & 2 deletions apps/frontend/src/ui/selects/SymbolSelect/LisboxComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const OuterElementType = forwardRef<HTMLDivElement>((props, ref) => {
});
OuterElementType.displayName = "OuterElementType";

/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any -- will be typed later */
/* eslint-disable @typescript-eslint/no-explicit-any -- will be typed later */
export const ListboxComponent = React.forwardRef<
HTMLDivElement,
{
Expand Down Expand Up @@ -105,4 +105,4 @@ export const ListboxComponent = React.forwardRef<
</Popper>
);
});
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any -- will be typed later */
/* eslint-enable @typescript-eslint/no-explicit-any -- will be typed later */
1 change: 0 additions & 1 deletion apps/frontend/src/utils/mui/createClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export function createClasses<C extends string, E extends ElementsParam<E>>(
const elementsKeys = Object.keys(elements) as (keyof E)[];

for (const elementKey of elementsKeys) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- it may be typed better, but it will require much more TS code that will affect the readability
result[elementKey] = buildElement(
componentName,
elementKey as string,
Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ services:
NEXT_PUBLIC_PROCESSOR_ENABLE_TRPC: "true"
NEXT_PUBLIC_STATIC: "true"
ADMIN_PASSWORD: opentrader
NEXT_PUBLIC_CANDLES_SERVICE_API_URL: http://localhost:5001
NEXT_PUBLIC_CANDLES_SERVICE_API_KEY: opentrader
PORT: 5000
depends_on:
- database
Expand All @@ -34,6 +36,8 @@ services:
NEXT_PUBLIC_PROCESSOR_URL: http://localhost:4000
NEXT_PUBLIC_PROCESSOR_ENABLE_TRPC: "true"
ADMIN_PASSWORD: opentrader
NEXT_PUBLIC_CANDLES_SERVICE_API_URL: http://localhost:5001
NEXT_PUBLIC_CANDLES_SERVICE_API_KEY: opentrader
depends_on:
- database
frontend:
Expand All @@ -53,6 +57,8 @@ services:
NEXT_PUBLIC_PROCESSOR_URL: http://localhost:4000
NEXT_PUBLIC_PROCESSOR_ENABLE_TRPC: "true"
ADMIN_PASSWORD: opentrader
NEXT_PUBLIC_CANDLES_SERVICE_API_URL: http://localhost:5001
NEXT_PUBLIC_CANDLES_SERVICE_API_KEY: opentrader
depends_on:
- database
database:
Expand Down
1 change: 0 additions & 1 deletion packages/backtesting-playground/src/grid-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ async function run() {
});

const {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- no types
data: { candlesticks },
} = await axios.get("http://localhost:5000/mapi/candlesticks", {
params: {
Expand Down
2 changes: 1 addition & 1 deletion packages/backtesting/src/exchange/memory-exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ExchangeCode } from "@opentrader/types";
import type { MarketSimulator } from "../market-simulator";

export class MemoryExchange implements IExchange {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, , @typescript-eslint/no-unsafe-assignment -- for simplicity
// eslint-disable-next-line @typescript-eslint/no-explicit-any, -- for simplicity
ccxt = {} as any;
/**
* @internal
Expand Down
4 changes: 3 additions & 1 deletion packages/bot-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"typescript": "^5.2.2"
},
"dependencies": {
"@opentrader/exchanges": "workspace:*"
"@opentrader/exchanges": "workspace:*",
"@opentrader/indicators": "workspace:*",
"@opentrader/tools": "workspace:*"
}
}
22 changes: 21 additions & 1 deletion packages/bot-processor/src/bot-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { IExchange } from "@opentrader/exchanges";
import { computeIndicators } from "@opentrader/indicators";
import type { IndicatorBarSize } from "@opentrader/types";
import { lastClosedCandleDate } from "@opentrader/tools";
import type { TBotContext } from "#bot-processor/types/bot/bot-context.type";
import { createContext } from "./utils/createContext";
import { SmartTradeService } from "./smart-trade.service";
Expand All @@ -7,6 +10,7 @@ import { isReplaceSmartTradeEffect } from "./effects/utils/isReplaceSmartTradeEf
import { isUseExchangeEffect } from "./effects/utils/isUseExchangeEffect";
import { isUseSmartTradeEffect } from "./effects/utils/isUseSmartTradeEffect";
import { isCancelSmartTradeEffect } from "./effects/utils/isCancelSmartTradeEffect";
import { isUseIndicatorsEffect } from "./effects/utils/isUseIndicatorsEffect";

export class BotManager<T extends IBotConfiguration> {
constructor(
Expand Down Expand Up @@ -35,13 +39,13 @@ export class BotManager<T extends IBotConfiguration> {
}

private async _process(context: TBotContext<T>): Promise<void> {
const processingDate = Date.now(); // @todo better to pass it through context
const generator = this.botTemplate(context);

let item = generator.next();

for (; !item.done; ) {
if (item.value instanceof Promise) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- will be typed later
const result = await item.value;

item = generator.next(result);
Expand Down Expand Up @@ -73,6 +77,22 @@ export class BotManager<T extends IBotConfiguration> {
item = generator.next();
} else if (isUseExchangeEffect(item.value)) {
item = generator.next(this.exchange);
} else if (isUseIndicatorsEffect(item.value)) {
const effect = item.value;
const barSize = effect.payload.barSize as IndicatorBarSize; // @todo fix eslint error
const { exchangeCode, baseCurrency, quoteCurrency } = this.botConfig;

console.log(`Compute indicators`, effect.payload);
const indicators = await computeIndicators({
exchangeCode,
symbol: `${baseCurrency}/${quoteCurrency}`,
barSize,
untilDate: lastClosedCandleDate(processingDate, barSize),
indicators: effect.payload.indicators,
});
console.log("Indicators computed", indicators);

item = generator.next(indicators);
} else {
console.log(item.value);
throw new Error("Unsupported effect");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export const USE_SMART_TRADE = "USE_SMART_TRADE";
export const REPLACE_SMART_TRADE = "REPLACE_SMART_TRADE";
export const CANCEL_SMART_TRADE = "CANCEL_SMART_TRADE";
export const USE_EXCHANGE = "USE_EXCHANGE";
export const USE_INDICATORS = "USE_INDICATORS";

export type EffectType =
| typeof USE_SMART_TRADE
| typeof REPLACE_SMART_TRADE
| typeof CANCEL_SMART_TRADE
| typeof USE_EXCHANGE;
| typeof USE_EXCHANGE
| typeof USE_INDICATORS;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { IndicatorBarSize, IndicatorName } from "@opentrader/types";
import type { BaseEffect } from "./base-effect";
import type { USE_INDICATORS } from "./effect-types";

export type UseIndicatorsEffect = BaseEffect<
typeof USE_INDICATORS,
{
indicators: IndicatorName[];
barSize: IndicatorBarSize;
}
>;
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 @@ -3,3 +3,4 @@ export * from "./useSmartTrade";
export * from "./replaceSmartTrade";
export * from "./cancelSmartTrade";
export * from "./useExchange";
export * from "./useIndicators";
14 changes: 14 additions & 0 deletions packages/bot-processor/src/effects/useIndicators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { IndicatorBarSize, IndicatorName } from "@opentrader/types";
import type { UseIndicatorsEffect } from "./common/types/use-indicators-effect";
import { USE_INDICATORS } from "./common/types/effect-types";
import { makeEffect } from "./utils/make-effect";

export function useIndicators<I extends IndicatorName>(
indicators: IndicatorName[],
barSize: IndicatorBarSize,
): UseIndicatorsEffect {
return makeEffect(USE_INDICATORS, {
indicators,
barSize,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { UseIndicatorsEffect } from "../common/types/use-indicators-effect";
import { USE_INDICATORS } from "../common/types/effect-types";

export function isUseIndicatorsEffect(
effect: unknown,
): effect is UseIndicatorsEffect {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -- this is required
return (effect && (effect as any).type) === USE_INDICATORS;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExchangeCode } from "@opentrader/types";
import type { ExchangeCode } from "@opentrader/types";

export type IBotConfiguration = {
id: number;
Expand Down
1 change: 1 addition & 0 deletions packages/bot-templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@opentrader/bot-processor": "workspace:*",
"@opentrader/exchanges": "workspace:*",
"@opentrader/indicators": "workspace:*",
"@opentrader/tools": "workspace:*"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@ import type {
import {
cancelSmartTrade,
useExchange,
useIndicators,
useSmartTrade,
} from "@opentrader/bot-processor";
import { computeGridLevelsFromCurrentAssetPrice } from "@opentrader/tools";
import type {
IGetMarketPriceResponse,
IGridBotLevel,
IGridLine,
XCandle,
} from "@opentrader/types";

export interface GridBotConfig extends IBotConfiguration {
gridLines: IGridLine[];
}

export function* arithmeticGridBot(ctx: TBotContext<GridBotConfig>) {
const candle1m: XCandle<"SMA10" | "SMA15"> = yield useIndicators(
["SMA10", "SMA15", "SMA30"],
"1m",
);
const candle5m: XCandle<"SMA10"> = yield useIndicators(["SMA10"], "5m");
const { config: bot, onStart, onStop } = ctx;

const exchange: IExchange = yield useExchange();
Expand Down
4 changes: 2 additions & 2 deletions packages/exchanges/src/exchanges/okx/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const getSymbol: Normalize["getSymbol"] = {

filters: {
price: {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- @todo need to assert */
/* eslint-disable @typescript-eslint/no-unsafe-member-access -- @todo need to assert */
tickSize: market.info.tickSz,
minPrice: null, // OKx doesn't provide this info
maxPrice: null, // OKx doesn't provide this info
Expand All @@ -127,7 +127,7 @@ const getSymbol: Normalize["getSymbol"] = {
stepSize: market.info.lotSz,
minQuantity: market.info.minSz,
maxQuantity: market.info.maxLmtSz,
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access -- @todo need to assert */
/* eslint-enable @typescript-eslint/no-unsafe-member-access -- @todo need to assert */
},
},
}),
Expand Down
6 changes: 6 additions & 0 deletions packages/indicators/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@opentrader/eslint-config/module.js"],
rules: {},
};
2 changes: 2 additions & 0 deletions packages/indicators/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
25 changes: 25 additions & 0 deletions packages/indicators/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@opentrader/indicators",
"version": "0.0.1",
"description": "",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"build": "tsc",
"lint": "eslint . --quiet",
"lint:fix": "eslint . --fix"
},
"author": "bludnic",
"devDependencies": {
"@opentrader/eslint-config": "workspace:*",
"@opentrader/tsconfig": "workspace:*",
"@opentrader/types": "workspace:*",
"@types/node": "^20.10.0",
"eslint": "8.54.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
"dependencies": {
"axios": "^1.6.1"
}
}
56 changes: 56 additions & 0 deletions packages/indicators/src/compute-indicators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {
ExchangeCode,
IndicatorBarSize,
IndicatorName,
IndicatorsResult,
XCandle,
} from "@opentrader/types";
import { fetchCandles } from "./fetch-candles";
import { indicatorsMap } from "./indicators";

type ComputeIndicatorsParams<I> = {
exchangeCode: ExchangeCode;
symbol: string;
barSize: IndicatorBarSize;
untilDate: string;
indicators: I[];
};

export async function computeIndicators<I extends IndicatorName>({
exchangeCode,
symbol,
barSize,
untilDate,
indicators,
}: ComputeIndicatorsParams<I>): Promise<XCandle<I>> {
// Find the maximum number of periods across all indicators.
// This allows to fetch all needed candles in one request.
const maxPeriods = Math.max(
...indicators.map((i) => indicatorsMap[i].periods),
);

const candles = await fetchCandles({
exchangeCode,
symbol,
untilDate,
barSize,
limit: maxPeriods,
});

const result: IndicatorsResult<I> = {} as IndicatorsResult<I>;
for (const indicator of indicators) {
result[indicator] = indicatorsMap[indicator].compute(candles);
}

const lastCandle = candles[candles.length - 1];

return {
timestamp: new Date(lastCandle.timestamp).getTime(),
open: lastCandle.open,
high: lastCandle.high,
low: lastCandle.low,
close: lastCandle.close,
volume: lastCandle.volume,
indicators: result,
};
}
20 changes: 20 additions & 0 deletions packages/indicators/src/fetch-candles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import axios from "axios";
import type { OHLCVData, OHLCVRequest, OHLCVResponse } from "./types";

export async function fetchCandles(params: OHLCVRequest): Promise<OHLCVData[]> {
console.log("Fetch candles", params);

const { data } = await axios.get<OHLCVResponse>(
`${process.env.NEXT_PUBLIC_CANDLES_SERVICE_API_URL}/api/trpc/candles.list`,
{
headers: {
Authorization: process.env.NEXT_PUBLIC_CANDLES_SERVICE_API_KEY,
},
params: {
input: JSON.stringify(params),
},
},
);

return data.result.data;
}
2 changes: 2 additions & 0 deletions packages/indicators/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./types";
export * from "./compute-indicators";
19 changes: 19 additions & 0 deletions packages/indicators/src/indicators/compute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { OHLCVData } from "../types";
import { SMA } from "./sma";

export function computeSMA(candles: OHLCVData[], periods: 5 | 10 | 15 | 30) {
if (candles.length < periods) {
throw new Error(
`Not enough data points for the given period: ${periods}. Candles provided: ${candles.length}`,
);
}

const lastCandles = candles.slice(-periods);

const result = SMA(
lastCandles.map((x) => x.close),
periods,
);

return result[0];
}
Loading

0 comments on commit ed6e6bd

Please sign in to comment.