Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Undercut option to duplicate strategy flow #878

Merged
merged 36 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
99d039f
Add duplicate strategy modal
tiagofilipenunes Oct 15, 2023
dc36ef4
Merge main into issue-#702
tiagofilipenunes Oct 18, 2023
b59d1ba
Add cut svg
tiagofilipenunes Oct 19, 2023
c8e8ec4
merge main into 'issue-#702'
tiagofilipenunes Oct 20, 2023
2cb9b97
Add duplicate modal
tiagofilipenunes Oct 20, 2023
bc0c8bc
Change modal design, add undercut strategy logic
tiagofilipenunes Oct 20, 2023
74ef272
Update tooltip
tiagofilipenunes Oct 20, 2023
6523b07
Formatting changes
tiagofilipenunes Oct 20, 2023
1d8cf2b
Refactoring duplicate modal
tiagofilipenunes Oct 20, 2023
26e2516
Fix aria label
tiagofilipenunes Oct 20, 2023
d3106a9
Merge main into branch issue-#702
tiagofilipenunes Nov 9, 2023
842fe13
Refactor button
tiagofilipenunes Nov 10, 2023
586e301
Merge branch 'main' into issue-#702
tiagofilipenunes Jan 11, 2024
fecaa5b
Change undercut percentage to 0.1%
tiagofilipenunes Jan 11, 2024
6e30c37
Fix only allow undercut if strategy is not overlapping
tiagofilipenunes Jan 11, 2024
74d47f4
Update recurring strategy duplicate e2e test to select duplicate moda…
tiagofilipenunes Jan 11, 2024
04e6a9e
[CI] Update Screenshots
tiagofilipenunes Jan 11, 2024
2ed07cd
Update src/components/strategies/overview/strategyBlock/StrategyBlock…
tiagofilipenunes Jan 12, 2024
c57c2b6
[CI] Update Screenshots
tiagofilipenunes Jan 12, 2024
8adc2fd
Use deepclone to undercut strategy
tiagofilipenunes Jan 12, 2024
b6599e3
Change p to h2
tiagofilipenunes Jan 12, 2024
4bbea83
Remove type button
tiagofilipenunes Jan 12, 2024
32e791d
Use undercutDifference to set undercut option description
tiagofilipenunes Jan 12, 2024
1897b7a
Remove button aria-label
tiagofilipenunes Jan 12, 2024
5ed53ca
Refactor into getUndercutStrategy and remove structuredClone that fai…
tiagofilipenunes Jan 17, 2024
9a3e410
Move ModalDuplicateStrategy to modal folder and getUndercutStrategy t…
tiagofilipenunes Jan 17, 2024
cbeab79
Use deep copy instead of shallow copy for the undercut strategy
tiagofilipenunes Jan 17, 2024
265e937
Add unit tests
tiagofilipenunes Jan 17, 2024
2e75458
Add undercut limit e2e test
tiagofilipenunes Jan 17, 2024
480bca6
Merge main into branch 'issue-#702'
tiagofilipenunes Jan 17, 2024
764cd13
Update recurring e2e tests to add undercut with testCase input and ou…
tiagofilipenunes Jan 17, 2024
2857ff0
[CI] Update Screenshots
tiagofilipenunes Jan 17, 2024
9991924
Remove changes to duplicate e2e test
tiagofilipenunes Jan 17, 2024
e1a7e2b
Remove unnecessary comment from unit test
tiagofilipenunes Jan 17, 2024
3e6a688
Merge branch 'main' into issue-#702
tiagofilipenunes Jan 18, 2024
e4e5e4d
Change duplicate e2e test to go through the modal
tiagofilipenunes Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions e2e/pages/strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ const testCases: CreateStrategyTestCase[] = [
fiat: '$3,334.42',
},
},
undercut: {
totalFiat: '$3,344.42',
buy: {
min: '1,501.50 DAI',
max: '1,501.50 DAI',
budget: '10.00 DAI',
fiat: '$10.00',
},
sell: {
min: '1,698.30 DAI',
max: '1,698.30 DAI',
budget: '2.00 ETH',
fiat: '$3,334.42',
},
},
},
},
{
Expand Down
1 change: 1 addition & 0 deletions e2e/tests/strategy/recurring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { createRecurringStrategy } from './create';
export { deleteStrategyTest } from './delete';
export { depositStrategyTest } from './deposit';
export { duplicateStrategyTest } from './duplicate';
export { undercutStrategyTest } from './undercut';
export { editPriceStrategyTest } from './edit';
export { pauseStrategyTest } from './pause';
export { renewStrategyTest } from './renew';
Expand Down
59 changes: 59 additions & 0 deletions e2e/tests/strategy/recurring/undercut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect, test } from '@playwright/test';
import {
CreateStrategyTestCase,
MyStrategyDriver,
assertRecurringTestCase,
} from './../../../utils/strategy';
import { NotificationDriver } from './../../../utils/NotificationDriver';
import { ManageStrategyDriver } from './../../../utils/strategy/ManageStrategyDriver';
import { waitModalOpen } from '../../../utils/modal';

export const undercutStrategyTest = (testCase: CreateStrategyTestCase) => {
assertRecurringTestCase(testCase);
const { base, quote } = testCase.input;
const output = testCase.output.undercut;
return test('Undercut', async ({ page }) => {
const manage = new ManageStrategyDriver(page);
const strategy = await manage.createStrategy(testCase.input);
await strategy.clickManageEntry('manage-strategy-duplicateStrategy');

const modal = await waitModalOpen(page);
await modal.getByTestId('undercut-strategy-btn').click();

await page.waitForURL('/strategies/create?strategy=*', {
timeout: 10_000,
});

await page.getByText('Create Strategy').click();
await page.waitForURL('/', { timeout: 10_000 });

const notif = new NotificationDriver(page, 'create-strategy');
await expect(notif.getTitle()).toHaveText('Success');
await expect(notif.getDescription()).toHaveText(
'New strategy was successfully created.'
);

const myStrategies = new MyStrategyDriver(page);
const strategies = await myStrategies.getAllStrategies();
await expect(strategies).toHaveCount(2);

const strategyUndercut = await myStrategies.getStrategy(2);
await expect(strategyUndercut.pair()).toHaveText(`${base}/${quote}`);
await expect(strategyUndercut.status()).toHaveText('Active');
await expect(strategyUndercut.totalBudget()).toHaveText(output.totalFiat);
await expect(strategyUndercut.buyBudget()).toHaveText(output.buy.budget);
await expect(strategyUndercut.buyBudgetFiat()).toHaveText(output.buy.fiat);
await expect(strategyUndercut.sellBudget()).toHaveText(output.sell.budget);
await expect(strategyUndercut.sellBudgetFiat()).toHaveText(
output.sell.fiat
);

const buyTooltip = await strategyUndercut.priceTooltip('buy');
await expect(buyTooltip.startPrice()).toHaveText(output.buy.min);
await buyTooltip.waitForDetached();

const sellTooltip = await strategyUndercut.priceTooltip('sell');
await expect(sellTooltip.startPrice()).toHaveText(output.sell.min);
await sellTooltip.waitForDetached();
});
};
15 changes: 15 additions & 0 deletions e2e/utils/strategy/CreateStrategyDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ export interface RecurringStrategyOutput {
fiat: string;
};
};
undercut: {
totalFiat: string;
buy: {
min: string;
max: string;
budget: string;
fiat: string;
};
sell: {
min: string;
max: string;
budget: string;
fiat: string;
};
};
}
export type RecurringStrategyTestCase = TestCase<
RecurringStrategyInput,
Expand Down
1 change: 1 addition & 0 deletions e2e/utils/strategy/MyStrategyDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class MyStrategyDriver {
minPrice: () => tooltip.getByTestId('min-price'),
maxPrice: () => tooltip.getByTestId('max-price'),
marginalPrice: () => tooltip.getByTestId('marginal-price'),
startPrice: () => tooltip.getByTestId('start-price'),
waitForDetached: async () => {
await this.page.mouse.move(0, 0);
await tooltip.waitFor({ state: 'detached' });
Expand Down
5 changes: 5 additions & 0 deletions src/assets/icons/cut.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export const StrategyBlockManage: FC<Props> = ({
name: 'Duplicate Strategy',
action: () => {
carbonEvents.strategyEdit.strategyDuplicateClick(strategyEvent);
duplicate(strategy);
if (!isOverlapping) {
openModal('duplicateStrategy', { strategy });
} else {
duplicate(strategy);
}
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ const OrderTooltip: FC<OrderTooltipProps> = ({ strategy, buy }) => {
<th className="p-8 text-start font-weight-400 text-white/60">
Price
</th>
<td className="p-8 text-end">
<td className="p-8 text-end" data-testid="start-price">
{startPrice} {quote.symbol}
</td>
</tr>
Expand Down
3 changes: 2 additions & 1 deletion src/components/strategies/overview/strategyBlock/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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<ModalDuplicateStrategyData> = ({
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 (
<ModalOrMobileSheet id={id} title="Duplicate Strategy">
<h2 className="text-secondary font-weight-400">Select your option.</h2>

{duplicateOptions.map(
({ icon: Icon, title, onClick, description, testId }) => (
<article
key={title}
className="grid grid-cols-[32px_1fr_auto] grid-rows-[auto_auto] gap-8 rounded bg-black/90 p-16"
>
<div className="row-span-2 flex h-32 w-32 items-center justify-center self-center rounded-full bg-green/25">
<Icon className="h-16 w-16 text-green" />
</div>
<h3 className="text-14 font-weight-500">{title}</h3>
<Button
variant="white"
onClick={onClick}
className="row-span-2 self-center"
data-testid={testId}
>
Select
</Button>
<p className="text-12 font-weight-400 text-white/60">
{description}
</p>
</article>
)
)}
</ModalOrMobileSheet>
);
};
Loading
Loading