From 3ecd480d6cb3866f730e99e37b2f360dfb372072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pekka=20Maanp=C3=A4=C3=A4?= Date: Sat, 25 Nov 2023 22:13:40 +0200 Subject: [PATCH] Get rid of Lodash dependency and use just Remeda and custom utils instead --- package-lock.json | 24 ---- package.json | 2 - src/backtest/backtest-order-execution.ts | 2 +- src/backtest/backtest-result.ts | 12 +- src/backtest/backtest.ts | 7 +- src/backtest/candle-update-provider-async.ts | 5 +- src/indicators/donchian-channel.ts | 4 +- src/indicators/indicator.ts | 5 +- src/stakers/common-staker.ts | 3 +- src/strategies/higher-order/auto-optimizer.ts | 2 +- src/time/to-start-of.ts | 4 +- src/util/util.ts | 104 +++++++++++++++--- test/backtest-add-candles.test.ts | 2 +- test/backtest-smoke.test.ts | 15 ++- 14 files changed, 116 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35da163..06e0f31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,12 @@ "version": "1.0.0", "dependencies": { "cli-progress": "^3.8.2", - "lodash": "^4.17.20", "luxon": "^3.2.1", "remeda": "^1.3.0", "technicalindicators": "^3.1.0" }, "devDependencies": { "@types/jest": "^29.2.5", - "@types/lodash": "^4.14.167", "@types/luxon": "^3.2.0", "@types/node": "^18.11.18", "husky": "^8.0.3", @@ -1064,12 +1062,6 @@ "pretty-format": "^29.0.0" } }, - "node_modules/@types/lodash": { - "version": "4.14.192", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", - "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==", - "dev": true - }, "node_modules/@types/luxon": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz", @@ -3082,11 +3074,6 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5045,12 +5032,6 @@ "pretty-format": "^29.0.0" } }, - "@types/lodash": { - "version": "4.14.192", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz", - "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==", - "dev": true - }, "@types/luxon": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz", @@ -6515,11 +6496,6 @@ "p-locate": "^4.1.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index 41d53b0..1f69ee7 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,12 @@ }, "dependencies": { "cli-progress": "^3.8.2", - "lodash": "^4.17.20", "luxon": "^3.2.1", "remeda": "^1.3.0", "technicalindicators": "^3.1.0" }, "devDependencies": { "@types/jest": "^29.2.5", - "@types/lodash": "^4.14.167", "@types/luxon": "^3.2.0", "@types/node": "^18.11.18", "husky": "^8.0.3", diff --git a/src/backtest/backtest-order-execution.ts b/src/backtest/backtest-order-execution.ts index b3fd74f..bb88540 100644 --- a/src/backtest/backtest-order-execution.ts +++ b/src/backtest/backtest-order-execution.ts @@ -1,4 +1,3 @@ -import { minBy } from "lodash"; import { concat, drop, @@ -7,6 +6,7 @@ import { isDefined, map, maxBy, + minBy, pipe, } from "remeda"; import { diff --git a/src/backtest/backtest-result.ts b/src/backtest/backtest-result.ts index fbfa502..1819b84 100644 --- a/src/backtest/backtest-result.ts +++ b/src/backtest/backtest-result.ts @@ -1,8 +1,8 @@ -import { flatMap, max, min, sortBy, sumBy } from "lodash"; -import { map, pipe, values } from "remeda"; +import { flatMap, map, pipe, sortBy, sumBy, values } from "remeda"; import { Candle, Range, Trade } from "../core/types"; import { Timeframe, toTimestamp } from "../time"; import { OverrideProps } from "../util/type-util"; +import { max, min } from "../util/util"; import { BacktestAsyncArgs, BacktestState } from "./backtest"; import { revertLastTransaction } from "./backtest-order-execution"; import { updateAsset } from "./update-asset"; @@ -151,9 +151,11 @@ function revertUnclosedTrades(state: BacktestState) { } function getTradesInOrder(state: BacktestState) { - return sortBy( - flatMap(state.assets, (a) => a.trades), - (t) => t.entry.time + return pipe( + state.assets, + values, + flatMap((asset) => asset.trades), + sortBy((trade) => trade.entry.time) ); } diff --git a/src/backtest/backtest.ts b/src/backtest/backtest.ts index d4f4d44..cabeb6e 100644 --- a/src/backtest/backtest.ts +++ b/src/backtest/backtest.ts @@ -1,4 +1,3 @@ -import { tap } from "lodash/fp"; import { first, last, pipe } from "remeda"; import { CandleDataProvider, Persister } from ".."; import { @@ -10,7 +9,7 @@ import { } from "../core/types"; import { Moment, Timeframe, toTimestamp } from "../time"; import { Dictionary } from "../util/type-util"; -import { repeatUntil, repeatUntilAsync, then } from "../util/util"; +import { repeatUntil, repeatUntilAsync, tap, then } from "../util/util"; import { BacktestPersistenceState, initBacktestPersistence, @@ -28,12 +27,12 @@ import { createAsyncCandleProvider, } from "./candle-update-provider-async"; import { - createSyncCandleProvider, SyncCandleUpdateProvider, + createSyncCandleProvider, } from "./candle-update-provider-sync"; import { createCandleUpdates } from "./create-candle-updates"; import { produceNextState } from "./produce-next-state"; -import { createProgressBar, ProgressHandler } from "./progress-handler"; +import { ProgressHandler, createProgressBar } from "./progress-handler"; /** * Args used by both synchronous and asynchronous backtest. diff --git a/src/backtest/candle-update-provider-async.ts b/src/backtest/candle-update-provider-async.ts index 4d19ad6..ee9069e 100644 --- a/src/backtest/candle-update-provider-async.ts +++ b/src/backtest/candle-update-provider-async.ts @@ -1,4 +1,3 @@ -import { dropRightWhile, dropWhile } from "lodash/fp"; import { pipe, reverse } from "remeda"; import { Range } from "../core/types"; import { @@ -7,7 +6,7 @@ import { } from "../data/candle-data-provider"; import { Moment, Timeframe, timeframeToPeriod, toTimestamp } from "../time"; import { Nullable } from "../util/type-util"; -import { then } from "../util/util"; +import { dropLastWhile, dropWhile, then } from "../util/util"; import { BacktestAsyncArgs } from "./backtest"; import { CandleUpdate, createCandleUpdates } from "./create-candle-updates"; @@ -67,7 +66,7 @@ function createDefaultAsyncCandleProvider(args: { (c: CandleUpdate) => c.time <= (previousCandleTime || -Infinity) ) ), - then(dropRightWhile((c) => c.time > fullRange.to)), + then(dropLastWhile((c) => c.time > fullRange.to)), then(reverse()) // for perf, removing from end ); diff --git a/src/indicators/donchian-channel.ts b/src/indicators/donchian-channel.ts index d0c666c..b81c857 100644 --- a/src/indicators/donchian-channel.ts +++ b/src/indicators/donchian-channel.ts @@ -1,7 +1,7 @@ -import { maxBy, minBy } from "lodash"; +import { maxBy, minBy } from "remeda"; import { Candle } from "../core/types"; import { avg } from "../util/util"; -import { createIndicatorWithPeriod, IndicatorChannel } from "./indicator-util"; +import { IndicatorChannel, createIndicatorWithPeriod } from "./indicator-util"; /** * Returns the value of a Donchian channel indicator. diff --git a/src/indicators/indicator.ts b/src/indicators/indicator.ts index 7033872..9c751fd 100644 --- a/src/indicators/indicator.ts +++ b/src/indicators/indicator.ts @@ -1,7 +1,6 @@ -import { takeRightWhile } from "lodash"; import { AssetState, Candle } from "../core/types"; import { Dictionary } from "../util/type-util"; -import { last } from "../util/util"; +import { last, takeLastWhile } from "../util/util"; /* Note: While most of this project works with pure functions, the indicators @@ -66,7 +65,7 @@ function createIndicator(initializer: () => (c: Candle) => RESULT) { return { update: ({ series, bufferSize }: IndicatorInputState): void => { - takeRightWhile(series, (c) => c.time > lastTimestamp).forEach((c) => { + takeLastWhile(series, (c) => c.time > lastTimestamp).forEach((c) => { const nextValue = nextValueGenerator(c); nextValue && result.push(nextValue); while (bufferSize !== undefined && result.length > bufferSize) { diff --git a/src/stakers/common-staker.ts b/src/stakers/common-staker.ts index f7ecbef..9a8cbd4 100644 --- a/src/stakers/common-staker.ts +++ b/src/stakers/common-staker.ts @@ -1,5 +1,4 @@ -import { sumBy } from "lodash/fp"; -import { keys, map, mapValues, pipe, values } from "remeda"; +import { keys, map, mapValues, pipe, sumBy, values } from "remeda"; import { AssetMap, AssetState, FullTradeState } from "../core/types"; import { SizelessOrder, Staker, StrategyUpdate } from "../high-level-api/types"; import { Dictionary, OverrideProps } from "../util/type-util"; diff --git a/src/strategies/higher-order/auto-optimizer.ts b/src/strategies/higher-order/auto-optimizer.ts index d4c3250..1fa0068 100644 --- a/src/strategies/higher-order/auto-optimizer.ts +++ b/src/strategies/higher-order/auto-optimizer.ts @@ -1,4 +1,4 @@ -import { maxBy } from "lodash"; +import { maxBy } from "remeda"; import { allInStaker, AssetState, diff --git a/src/time/to-start-of.ts b/src/time/to-start-of.ts index c2bf2cd..181d73d 100644 --- a/src/time/to-start-of.ts +++ b/src/time/to-start-of.ts @@ -1,4 +1,4 @@ -import { dropRightWhile } from "lodash"; +import { dropLastWhile } from "../util/util"; import { Moment, toDateTime, toTimestamp } from "./moment"; const TIME_PROPS = ["year", "month", "day", "hour", "minute"] as const; @@ -16,7 +16,7 @@ export function toStartOf( ): number { const dateTime = toDateTime(time); - const propsToCopy = dropRightWhile(TIME_PROPS, (p) => p !== unit); + const propsToCopy = dropLastWhile(TIME_PROPS, (p) => p !== unit); const timeObject: Record<(typeof TIME_PROPS)[number], number> = TIME_PROPS.reduce( diff --git a/src/util/util.ts b/src/util/util.ts index e2e4b28..50cb5da 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,10 +1,14 @@ -import _ from "lodash"; import { - fromPairs as remedaFromPairs, identity, isArray, isNumber, + maxBy, + minBy, + purry, + fromPairs as remedaFromPairs, + pickBy as remedaPickBy, sort, + sumBy, } from "remeda"; import { Candle, CandleSeries, Order } from "../core/types"; import { SizelessOrder } from "../high-level-api/types"; @@ -31,9 +35,9 @@ export function last(array: Array) { export function avg(values: number[]): number { return sum(values) / values.length; } -export function sum(values: number[]): number { - return values.reduce((sum, value) => sum + value, 0); -} +export const sum = sumBy(identity); +export const min = minBy(identity); +export const max = maxBy(identity); export function getAverageCandleSize( series: CandleSeries, @@ -134,22 +138,12 @@ export const fromPairs = (pairs: [string, T][]) => remedaFromPairs(pairs); /** * Filters object entries by the given predicate. * - * Lodash's pickBy curried and with better typing. + * Remeda's pickBy with looser typing. */ export const pickBy = (predicate: (value: T, key: string) => boolean) => (obj: Dictionary): Dictionary => - _.pickBy(obj, predicate); - -/** - * Maps object values by the given function. - * - * Lodash's mapValues curried and with better typing. - */ -export const mapValues = - (mapper: (value: T, key: string) => R) => - (obj: Dictionary): Dictionary => - _.mapValues(obj, mapper); + remedaPickBy(obj, predicate); /** * Enables using Promise.then in pipe without arrow functions. @@ -199,3 +193,79 @@ export const repeatUntilAsync = } return value; }; + +export function tap(value: T, fn: (value: T) => void): T; +export function tap(fn: (value: T) => void): (value: T) => T; +export function tap() { + return purry(_tap, arguments); +} +function _tap(value: T, fn: (value: T) => void): T { + fn(value); + return value; +} + +export function takeLastWhile( + array: ReadonlyArray, + fn: (item: T) => boolean +): Array; +export function takeLastWhile( + fn: (item: T) => boolean +): (array: ReadonlyArray) => Array; +export function takeLastWhile() { + return purry(_takeLastWhile, arguments); +} +export function _takeLastWhile( + array: ReadonlyArray, + fn: (item: T) => boolean +): ReadonlyArray { + for (let i = array.length - 1; i >= 0; i--) { + if (!fn(array[i])) { + return array.slice(i + 1); + } + } + return array; +} + +export function dropWhile( + array: ReadonlyArray, + fn: (item: T) => boolean +): Array; +export function dropWhile( + fn: (item: T) => boolean +): (array: ReadonlyArray) => Array; +export function dropWhile() { + return purry(_dropWhile, arguments); +} +export function _dropWhile( + array: ReadonlyArray, + fn: (item: T) => boolean +): ReadonlyArray { + for (let i = 0; i < array.length; i++) { + if (!fn(array[i])) { + return array.slice(i); + } + } + return []; +} + +export function dropLastWhile( + array: ReadonlyArray, + fn: (item: T) => boolean +): Array; +export function dropLastWhile( + fn: (item: T) => boolean +): (array: ReadonlyArray) => Array; +export function dropLastWhile() { + return purry(_dropLastWhile, arguments); +} +export function _dropLastWhile( + array: ReadonlyArray, + fn: (item: T) => boolean +): ReadonlyArray { + for (let i = array.length - 1; i >= 0; i--) { + if (!fn(array[i])) { + return array.slice(0, i + 1); + } + } + return []; +} diff --git a/test/backtest-add-candles.test.ts b/test/backtest-add-candles.test.ts index 403df59..f1ce83c 100644 --- a/test/backtest-add-candles.test.ts +++ b/test/backtest-add-candles.test.ts @@ -1,4 +1,4 @@ -import { last } from "lodash/fp"; +import { last } from "remeda"; import { backtestSync } from "../src"; import { Candle, FullTradingStrategy, SeriesMap } from "../src/core/types"; diff --git a/test/backtest-smoke.test.ts b/test/backtest-smoke.test.ts index f1c18ac..0dd9e9d 100644 --- a/test/backtest-smoke.test.ts +++ b/test/backtest-smoke.test.ts @@ -1,26 +1,25 @@ -import { dropWhile, takeWhile } from "lodash/fp"; -import { omit, pipe } from "remeda"; +import { omit, pipe, takeWhile } from "remeda"; import { - allInStaker, AssetState, - backtest, BacktestAsyncArgs, BacktestResult, BacktestStatistics, - backtestSync, BacktestSyncResult, BacktestSyncStatistics, CandleDataProvider, CandleSeries, - getPersistedBacktestResult, Persister, - toTimestamp, TradingStrategy, + allInStaker, + backtest, + backtestSync, + getPersistedBacktestResult, + toTimestamp, withStaker, } from "../src"; import { CommonBacktestArgs } from "../src/backtest/backtest"; import { Dictionary } from "../src/util/type-util"; -import { last } from "../src/util/util"; +import { dropWhile, last } from "../src/util/util"; import { testData } from "./test-data/testData"; const series: CandleSeries = testData.getBtcHourly();