= ({ strategy, buy }) => {
Price
|
-
+ |
{startPrice} {quote.symbol}
|
diff --git a/src/components/strategies/overview/strategyBlock/utils.ts b/src/components/strategies/overview/strategyBlock/utils.ts
index 3c8536120..661f4209e 100644
--- a/src/components/strategies/overview/strategyBlock/utils.ts
+++ b/src/components/strategies/overview/strategyBlock/utils.ts
@@ -32,7 +32,8 @@ export const getTooltipTextByStatus = (
};
const tooltipTextByStrategyEditOptionsId = {
- duplicateStrategy: 'Create a new strategy with the same details',
+ duplicateStrategy:
+ 'Create a new strategy with the same details or undercut it',
deleteStrategy:
'Delete the strategy and withdraw all associated funds to your wallet',
pauseStrategy: 'Deactivate the strategy by nulling the prices',
diff --git a/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.test.tsx b/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.test.tsx
new file mode 100644
index 000000000..656d73f39
--- /dev/null
+++ b/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.test.tsx
@@ -0,0 +1,140 @@
+import { describe, test, expect } from 'vitest';
+import { SafeDecimal } from 'libs/safedecimal';
+import { deepCopy, getUndercutStrategy } from './utils';
+
+type StrategyStatus = 'active' | 'noBudget' | 'paused' | 'inactive';
+
+const baseStrategy = {
+ id: '',
+ idDisplay: '',
+ base: {
+ address: '',
+ decimals: 18,
+ symbol: 'ETH',
+ },
+ quote: {
+ address: '',
+ decimals: 6,
+ symbol: 'USDC',
+ },
+ order0: {
+ balance: '',
+ startRate: '',
+ endRate: '',
+ marginalRate: '',
+ },
+ order1: {
+ balance: '',
+ startRate: '',
+ endRate: '',
+ marginalRate: '',
+ },
+ status: 'active' as StrategyStatus,
+ encoded: {
+ id: '',
+ token0: '',
+ token1: '',
+ order0: {
+ y: '',
+ z: '',
+ A: '',
+ B: '',
+ },
+ order1: {
+ y: '',
+ z: '',
+ A: '',
+ B: '',
+ },
+ },
+ roi: new SafeDecimal('0'),
+};
+
+let undercutStrategy = deepCopy(baseStrategy);
+
+describe('Test undercut strategy', () => {
+ test('getUndercutStrategy with 0.1% rate', () => {
+ const undercutDifference = 0.001;
+
+ baseStrategy.order0.startRate = '0';
+ baseStrategy.order0.endRate = '1700';
+ baseStrategy.order1.startRate = '1800';
+ baseStrategy.order1.endRate = '1900';
+ undercutStrategy.order0.startRate = '0';
+ undercutStrategy.order0.endRate = '1701.7';
+ undercutStrategy.order1.startRate = '1798.2';
+ undercutStrategy.order1.endRate = '1898.1';
+
+ expect(getUndercutStrategy(baseStrategy, undercutDifference)).toStrictEqual(
+ undercutStrategy
+ );
+ });
+
+ test('getUndercutStrategy with 0.1% rate and a strategy with one limit order', () => {
+ const undercutDifference = 0.001;
+
+ baseStrategy.order0.startRate = '0';
+ baseStrategy.order0.endRate = '0';
+ baseStrategy.order1.startRate = '1900';
+ baseStrategy.order1.endRate = '1900';
+ undercutStrategy.order0.startRate = '0';
+ undercutStrategy.order0.endRate = '0';
+ undercutStrategy.order1.startRate = '1898.1';
+ undercutStrategy.order1.endRate = '1898.1';
+
+ expect(getUndercutStrategy(baseStrategy, undercutDifference)).toStrictEqual(
+ undercutStrategy
+ );
+ });
+
+ test('getUndercutStrategy with 1% rate', () => {
+ const undercutDifference = 0.01;
+
+ baseStrategy.order0.startRate = '1600';
+ baseStrategy.order0.endRate = '1700';
+ baseStrategy.order1.startRate = '1800';
+ baseStrategy.order1.endRate = '1900';
+ undercutStrategy.order0.startRate = '1616';
+ undercutStrategy.order0.endRate = '1717';
+ undercutStrategy.order1.startRate = '1782';
+ undercutStrategy.order1.endRate = '1881';
+
+ expect(getUndercutStrategy(baseStrategy, undercutDifference)).toStrictEqual(
+ undercutStrategy
+ );
+ });
+
+ test('getUndercutStrategy with negative rate', () => {
+ const undercutDifference = -1;
+
+ baseStrategy.order0.startRate = '1600';
+ baseStrategy.order0.endRate = '1700';
+ baseStrategy.order1.startRate = '1800';
+ baseStrategy.order1.endRate = '1900';
+ undercutStrategy.order0.startRate = '1616';
+ undercutStrategy.order0.endRate = '1717';
+ undercutStrategy.order1.startRate = '1782';
+ undercutStrategy.order1.endRate = '1881';
+
+ expect(() =>
+ getUndercutStrategy(baseStrategy, undercutDifference)
+ ).toThrow();
+ });
+
+ test('getUndercutStrategy with rate higher than 100%', () => {
+ const undercutDifference = 1.01;
+
+ baseStrategy.order0.startRate = '1600';
+ baseStrategy.order0.endRate = '1700';
+ baseStrategy.order1.startRate = '1800';
+ baseStrategy.order1.endRate = '1900';
+ undercutStrategy.order0.startRate = '1616';
+ undercutStrategy.order0.endRate = '1717';
+ undercutStrategy.order1.startRate = '1782';
+ undercutStrategy.order1.endRate = '1881';
+
+ expect(() =>
+ getUndercutStrategy(baseStrategy, undercutDifference)
+ ).toThrow();
+ });
+});
diff --git a/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx b/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx
new file mode 100644
index 000000000..f8a37b9f6
--- /dev/null
+++ b/src/libs/modals/modals/ModalDuplicateStrategy/ModalDuplicateStrategy.tsx
@@ -0,0 +1,85 @@
+import { ReactComponent as IconCut } from 'assets/icons/cut.svg';
+import { ReactComponent as IconCopy } from 'assets/icons/copy.svg';
+import { Button } from 'components/common/button';
+import { useDuplicateStrategy } from 'components/strategies/create/useDuplicateStrategy';
+import { useModal } from 'hooks/useModal';
+import { ModalFC } from 'libs/modals/modals.types';
+import { ModalOrMobileSheet } from 'libs/modals/ModalOrMobileSheet';
+import { Strategy } from 'libs/queries';
+import { getUndercutStrategy } from './utils';
+
+export type ModalDuplicateStrategyData = {
+ strategy: Strategy;
+};
+
+export const ModalDuplicateStrategy: ModalFC = ({
+ id,
+ data: { strategy },
+}) => {
+ const { closeModal } = useModal();
+ const { duplicate } = useDuplicateStrategy();
+ const undercutDifference = 0.001;
+
+ const undercutStrategy = () => {
+ const undercutStrategy = getUndercutStrategy(strategy, undercutDifference);
+
+ duplicate(undercutStrategy);
+ closeModal(id);
+ };
+
+ const duplicateStrategy = () => {
+ duplicate(strategy);
+ closeModal(id);
+ };
+
+ const duplicateOptions = [
+ {
+ icon: IconCopy,
+ title: 'Copy as Is',
+ onClick: duplicateStrategy,
+ description:
+ 'Duplicate the strategy with the existing values (price, budget)',
+ testId: 'duplicate-strategy-btn',
+ },
+ {
+ icon: IconCut,
+ title: 'Undercut the Strategy',
+ onClick: undercutStrategy,
+ description: `Set prices at ${
+ undercutDifference * 100
+ }% tighter spread and try to get filled ahead`,
+ testId: 'undercut-strategy-btn',
+ },
+ ];
+
+ return (
+
+ Select your option.
+
+ {duplicateOptions.map(
+ ({ icon: Icon, title, onClick, description, testId }) => (
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+ )
+ )}
+
+ );
+};
diff --git a/src/libs/modals/modals/ModalDuplicateStrategy/utils.ts b/src/libs/modals/modals/ModalDuplicateStrategy/utils.ts
new file mode 100644
index 000000000..c3316490d
--- /dev/null
+++ b/src/libs/modals/modals/ModalDuplicateStrategy/utils.ts
@@ -0,0 +1,54 @@
+import type { Strategy } from 'libs/queries';
+import { SafeDecimal } from 'libs/safedecimal';
+
+const replaceDecimal = (key: string, value: any) => {
+ if (value instanceof SafeDecimal)
+ return { __type__: 'SafeDecimal', value: value.toString() };
+ return value;
+};
+const reviveDecimal = (key: string, value: any) => {
+ if (
+ typeof value === 'object' &&
+ value !== null &&
+ value['__type__'] === 'SafeDecimal'
+ )
+ return new SafeDecimal(value.value);
+ return value;
+};
+
+export const deepCopy = (obj: any): any =>
+ JSON.parse(JSON.stringify(obj, replaceDecimal), reviveDecimal);
+
+export const getUndercutStrategy = (
+ strategy: Strategy,
+ undercutDifference: number
+): Strategy => {
+ const multiplyByRate = (rate: string, factor: number) =>
+ new SafeDecimal(rate).times(factor).toString();
+
+ if (undercutDifference < 0 || undercutDifference > 1)
+ throw new Error(
+ 'undercutDifference must be less than or equal to 1, and higher than or equal to 0'
+ );
+
+ const undercutStrategy = deepCopy(strategy);
+
+ undercutStrategy.order0.startRate = multiplyByRate(
+ strategy.order0.startRate,
+ 1 + undercutDifference
+ );
+ undercutStrategy.order0.endRate = multiplyByRate(
+ strategy.order0.endRate,
+ 1 + undercutDifference
+ );
+ undercutStrategy.order1.startRate = multiplyByRate(
+ strategy.order1.startRate,
+ 1 - undercutDifference
+ );
+ undercutStrategy.order1.endRate = multiplyByRate(
+ strategy.order1.endRate,
+ 1 - undercutDifference
+ );
+
+ return undercutStrategy;
+};
diff --git a/src/libs/modals/modals/index.ts b/src/libs/modals/modals/index.ts
index 18114bff0..a6c9c51eb 100644
--- a/src/libs/modals/modals/index.ts
+++ b/src/libs/modals/modals/index.ts
@@ -40,6 +40,10 @@ import {
ModalManageNotifications,
ModalManageNotificationsData,
} from './ModalManageNotifications';
+import {
+ ModalDuplicateStrategy,
+ ModalDuplicateStrategyData,
+} from './ModalDuplicateStrategy/ModalDuplicateStrategy';
import {
ModalConfirmWithdraw,
ModalConfirmWithdrawData,
@@ -63,6 +67,7 @@ export interface ModalSchema {
restrictedCountry: undefined;
genericInfo: ModalGenericInfoData;
manageNotifications: ModalManageNotificationsData;
+ duplicateStrategy: ModalDuplicateStrategyData;
confirmPauseStrategy: ModalConfirmPauseData;
confirmWithdrawStrategy: ModalConfirmWithdrawData;
confirmDeleteStrategy: ModalConfirmDeleteData;
@@ -84,6 +89,7 @@ export const MODAL_COMPONENTS: TModals = {
restrictedCountry: (props) => ModalRestrictedCountry(props),
genericInfo: (props) => ModalGenericInfo(props),
manageNotifications: (props) => ModalManageNotifications(props),
+ duplicateStrategy: (props) => ModalDuplicateStrategy(props),
confirmPauseStrategy: (props) => ModalConfirmPause(props),
confirmWithdrawStrategy: (props) => ModalConfirmWithdraw(props),
confirmDeleteStrategy: (props) => ModalConfirmDelete(props),