feat(markets): new price monitoring bounds panel (#5996)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
parent
72df08851c
commit
88f99031d1
@ -6,6 +6,7 @@ import {
|
|||||||
LiquiditySLAParametersInfoPanel,
|
LiquiditySLAParametersInfoPanel,
|
||||||
MarginScalingFactorsPanel,
|
MarginScalingFactorsPanel,
|
||||||
PriceMonitoringBoundsInfoPanel,
|
PriceMonitoringBoundsInfoPanel,
|
||||||
|
PriceMonitoringSettingsInfoPanel,
|
||||||
SuccessionLineInfoPanel,
|
SuccessionLineInfoPanel,
|
||||||
getDataSourceSpecForSettlementData,
|
getDataSourceSpecForSettlementData,
|
||||||
getDataSourceSpecForTradingTermination,
|
getDataSourceSpecForTradingTermination,
|
||||||
@ -21,7 +22,6 @@ import {
|
|||||||
RiskModelInfoPanel,
|
RiskModelInfoPanel,
|
||||||
SettlementAssetInfoPanel,
|
SettlementAssetInfoPanel,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketInfoTable } from '@vegaprotocol/markets';
|
|
||||||
import type { DataSourceFragment } from '@vegaprotocol/markets';
|
import type { DataSourceFragment } from '@vegaprotocol/markets';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
@ -74,27 +74,14 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
|||||||
<MarginScalingFactorsPanel market={market} />
|
<MarginScalingFactorsPanel market={market} />
|
||||||
<h2 className={headerClassName}>{t('Risk factors')}</h2>
|
<h2 className={headerClassName}>{t('Risk factors')}</h2>
|
||||||
<RiskFactorsInfoPanel market={market} />
|
<RiskFactorsInfoPanel market={market} />
|
||||||
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => (
|
<h2 className={headerClassName}>{t('Price monitoring bounds')}</h2>
|
||||||
<>
|
<div className="mt-3">
|
||||||
<h2 className={headerClassName}>
|
<PriceMonitoringBoundsInfoPanel market={market} />
|
||||||
{t('Price monitoring bounds %s', [(i + 1).toString()])}
|
</div>
|
||||||
</h2>
|
<h2 className={headerClassName}>{t('Price monitoring settings')}</h2>
|
||||||
<PriceMonitoringBoundsInfoPanel
|
<div className="mt-3">
|
||||||
market={market}
|
<PriceMonitoringSettingsInfoPanel market={market} />
|
||||||
triggerIndex={i + 1}
|
</div>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
{(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
|
||||||
(trigger, i) => (
|
|
||||||
<>
|
|
||||||
<h2 className={headerClassName}>
|
|
||||||
{t('Price monitoring settings %s', [(i + 1).toString()])}
|
|
||||||
</h2>
|
|
||||||
<MarketInfoTable data={trigger} key={i} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<h2 className={headerClassName}>{t('Liquidation strategy')}</h2>
|
<h2 className={headerClassName}>{t('Liquidation strategy')}</h2>
|
||||||
<LiquidationStrategyInfoPanel market={market} />
|
<LiquidationStrategyInfoPanel market={market} />
|
||||||
<h2 className={headerClassName}>{t('Liquidity monitoring')}</h2>
|
<h2 className={headerClassName}>{t('Liquidity monitoring')}</h2>
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
getSigners,
|
getSigners,
|
||||||
MarginScalingFactorsPanel,
|
MarginScalingFactorsPanel,
|
||||||
marketInfoProvider,
|
marketInfoProvider,
|
||||||
|
PriceMonitoringSettingsInfoPanel,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -245,39 +246,23 @@ export const ProposalMarketData = ({ proposalId }: { proposalId: string }) => {
|
|||||||
parentMarket={parentMarketData}
|
parentMarket={parentMarketData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showParentPriceMonitoringBounds &&
|
{showParentPriceMonitoringBounds && (
|
||||||
(
|
// shows bounds for parent market
|
||||||
parentMarketData?.priceMonitoringSettings?.parameters
|
|
||||||
?.triggers || []
|
|
||||||
).map((_, triggerIndex) => (
|
|
||||||
<>
|
|
||||||
<h2 className={marketDataHeaderStyles}>
|
|
||||||
{t(`Parent price monitoring bounds ${triggerIndex + 1}`)}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="text-vega-dark-300 line-through">
|
|
||||||
<PriceMonitoringBoundsInfoPanel
|
|
||||||
market={parentMarketData}
|
|
||||||
triggerIndex={triggerIndex}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{(
|
|
||||||
marketData.priceMonitoringSettings?.parameters?.triggers || []
|
|
||||||
).map((_, triggerIndex) => (
|
|
||||||
<>
|
<>
|
||||||
<h2 className={marketDataHeaderStyles}>
|
<h2 className={marketDataHeaderStyles}>
|
||||||
{t(`Price monitoring bounds ${triggerIndex + 1}`)}
|
{t('Parent price monitoring bounds')}
|
||||||
</h2>
|
</h2>
|
||||||
|
<div className="text-vega-dark-300 line-through">
|
||||||
<PriceMonitoringBoundsInfoPanel
|
<PriceMonitoringBoundsInfoPanel market={parentMarketData} />
|
||||||
market={marketData}
|
</div>
|
||||||
triggerIndex={triggerIndex}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
|
|
||||||
|
<h2 className={marketDataHeaderStyles}>
|
||||||
|
{t('Price monitoring settings')}
|
||||||
|
</h2>
|
||||||
|
<PriceMonitoringSettingsInfoPanel market={marketData} />
|
||||||
|
|
||||||
<h2 className={marketDataHeaderStyles}>
|
<h2 className={marketDataHeaderStyles}>
|
||||||
{t('Liquidity monitoring parameters')}
|
{t('Liquidity monitoring parameters')}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -6,6 +6,7 @@ from fixtures.market import setup_continuous_market
|
|||||||
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
|
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
|
||||||
|
|
||||||
market_title_test_id = "accordion-title"
|
market_title_test_id = "accordion-title"
|
||||||
|
market_accordion_content = "accordion-content"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
@ -225,18 +226,16 @@ def test_market_info_risk_factors(page: Page):
|
|||||||
def test_market_info_price_monitoring_bounds(page: Page):
|
def test_market_info_price_monitoring_bounds(page: Page):
|
||||||
# 6002-MDET-211
|
# 6002-MDET-211
|
||||||
page.get_by_test_id(market_title_test_id).get_by_text(
|
page.get_by_test_id(market_title_test_id).get_by_text(
|
||||||
"Price monitoring bounds 1"
|
"Price monitoring bounds"
|
||||||
).click()
|
).click()
|
||||||
expect(page.locator("p.col-span-1").nth(0)).to_contain_text(
|
expect(page.get_by_test_id(market_accordion_content).locator(
|
||||||
"99.9999% probability price bounds"
|
".w-full").nth(1)).to_contain_text("99.9999%")
|
||||||
)
|
expect(page.get_by_test_id(market_accordion_content).locator(".w-full").nth(2)
|
||||||
expect(page.locator("p.col-span-1").nth(1)
|
).to_contain_text("within 1d")
|
||||||
).to_contain_text("Within 86,400 seconds")
|
expect(page.get_by_test_id(market_accordion_content).locator(".text-left")
|
||||||
fields = [
|
).to_contain_text("83.11038 BTC")
|
||||||
["Highest Price", "138.66685 BTC"],
|
expect(page.get_by_test_id(market_accordion_content).locator(".text-right")
|
||||||
["Lowest Price", "83.11038 BTC"],
|
).to_contain_text("138.66685 BTC")
|
||||||
]
|
|
||||||
validate_info_section(page, fields)
|
|
||||||
|
|
||||||
|
|
||||||
def test_market_info_liquidity_monitoring_parameters(page: Page):
|
def test_market_info_liquidity_monitoring_parameters(page: Page):
|
||||||
|
@ -87,6 +87,7 @@
|
|||||||
"oracleInMarkets_other": "Oracle in {{count}} markets",
|
"oracleInMarkets_other": "Oracle in {{count}} markets",
|
||||||
"oracleInMarkets": "Oracle in {{count}} markets",
|
"oracleInMarkets": "Oracle in {{count}} markets",
|
||||||
"Price monitoring bounds {{index}}": "Price monitoring bounds {{index}}",
|
"Price monitoring bounds {{index}}": "Price monitoring bounds {{index}}",
|
||||||
|
"Price monitoring bounds": "Price monitoring bounds",
|
||||||
"Price oracle {{index}}": "Price oracle {{index}}",
|
"Price oracle {{index}}": "Price oracle {{index}}",
|
||||||
"Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.": "Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.",
|
"Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.": "Probability level for price projection, e.g. value of 0.95 will result in a price range such that over the specified projection horizon, the prices observed in the market should be in that range 95% of the time.",
|
||||||
"Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short": "Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short",
|
"Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short": "Probability level used in <0>Expected Shortfall</0> calculation when obtaining Risk Factor Long and Risk Factor Short",
|
||||||
@ -97,7 +98,7 @@
|
|||||||
"Proposal": "Proposal",
|
"Proposal": "Proposal",
|
||||||
"Propose a change to market": "Propose a change to market",
|
"Propose a change to market": "Propose a change to market",
|
||||||
"Read more": "Read more",
|
"Read more": "Read more",
|
||||||
"Results in {{auctionExtensionSecs}} seconds auction if breached": "Results in {{auctionExtensionSecs}} seconds auction if breached",
|
"Results in {{duration}} auction if breached": "Results in {{duration}} auction if breached",
|
||||||
"Risk factors": "Risk factors",
|
"Risk factors": "Risk factors",
|
||||||
"Risk model": "Risk model",
|
"Risk model": "Risk model",
|
||||||
"Settlement": "Settlement",
|
"Settlement": "Settlement",
|
||||||
@ -166,6 +167,12 @@
|
|||||||
"View settlement schedule specification": "View settlement schedule specification",
|
"View settlement schedule specification": "View settlement schedule specification",
|
||||||
"View specification": "View specification",
|
"View specification": "View specification",
|
||||||
"View termination specification": "View termination specification",
|
"View termination specification": "View termination specification",
|
||||||
"Within {{horizonSecs}} seconds": "Within {{horizonSecs}} seconds",
|
"Weighted": "Weighted",
|
||||||
"Weighted": "Weighted"
|
"within {{duration}}": "within {{duration}}",
|
||||||
|
"Minimum price that isn't currently breaching the specified price monitoring trigger": "Minimum price that isn't currently breaching the specified price monitoring trigger",
|
||||||
|
"Maximum price that isn't currently breaching the specified price monitoring trigger": "Maximum price that isn't currently breaching the specified price monitoring trigger",
|
||||||
|
"{{probability}} of prices must be within the bounds for {{duration}}": "{{probability}} of prices must be within the bounds for {{duration}}",
|
||||||
|
"No price monitoring bounds detected.": "No price monitoring bounds detected.",
|
||||||
|
"triggers_one": "Trigger",
|
||||||
|
"triggers_other": "Chain of {{count}} triggers"
|
||||||
}
|
}
|
||||||
|
19
libs/i18n/src/locales/en/react-helpers.json
Normal file
19
libs/i18n/src/locales/en/react-helpers.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"duration_days_one": "{{count}} day",
|
||||||
|
"duration_days_other": "{{count}} days",
|
||||||
|
"duration_hours_one": "{{count}} hour",
|
||||||
|
"duration_hours_other": "{{count}} hours",
|
||||||
|
"duration_minutes_one": "{{count}} minute",
|
||||||
|
"duration_minutes_other": "{{count}} minutes",
|
||||||
|
"duration_seconds_one": "{{count}} second",
|
||||||
|
"duration_seconds_other": "{{count}} seconds",
|
||||||
|
|
||||||
|
"duration_days_compact_one": "{{count}}d",
|
||||||
|
"duration_days_compact_other": "{{count}}d",
|
||||||
|
"duration_hours_compact_one": "{{count}}h",
|
||||||
|
"duration_hours_compact_other": "{{count}}h",
|
||||||
|
"duration_minutes_compact_one": "{{count}}m",
|
||||||
|
"duration_minutes_compact_other": "{{count}}m",
|
||||||
|
"duration_seconds_compact_one": "{{count}}s",
|
||||||
|
"duration_seconds_compact_other": "{{count}}s"
|
||||||
|
}
|
@ -221,27 +221,11 @@ export const MarketInfoAccordion = ({
|
|||||||
title={t('Risk factors')}
|
title={t('Risk factors')}
|
||||||
content={<RiskFactorsInfoPanel market={market} />}
|
content={<RiskFactorsInfoPanel market={market} />}
|
||||||
/>
|
/>
|
||||||
{(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
<AccordionItem
|
||||||
(_, triggerIndex) => {
|
itemId="trigger"
|
||||||
const id = `trigger-${triggerIndex}`;
|
title={t('Price monitoring bounds')}
|
||||||
return (
|
content={<PriceMonitoringBoundsInfoPanel market={market} />}
|
||||||
<AccordionItem
|
/>
|
||||||
key={id}
|
|
||||||
itemId={id}
|
|
||||||
title={t('Price monitoring bounds {{index}}', {
|
|
||||||
index: triggerIndex + 1,
|
|
||||||
})}
|
|
||||||
content={
|
|
||||||
<PriceMonitoringBoundsInfoPanel
|
|
||||||
market={market}
|
|
||||||
triggerIndex={triggerIndex}
|
|
||||||
key={id}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
itemId="liquidation-strategy"
|
itemId="liquidation-strategy"
|
||||||
title={t('Liquidation strategy')}
|
title={t('Liquidation strategy')}
|
||||||
|
@ -24,10 +24,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
determinePriceStep,
|
determinePriceStep,
|
||||||
formatNumber,
|
|
||||||
formatNumberPercentage,
|
formatNumberPercentage,
|
||||||
getDateTimeFormat,
|
getDateTimeFormat,
|
||||||
getMarketExpiryDateFormatted,
|
getMarketExpiryDateFormatted,
|
||||||
|
toBigNum,
|
||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import type { Get } from 'type-fest';
|
import type { Get } from 'type-fest';
|
||||||
import { MarketInfoTable } from './info-key-value-table';
|
import { MarketInfoTable } from './info-key-value-table';
|
||||||
@ -88,6 +88,12 @@ import { formatDuration } from 'date-fns';
|
|||||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
import { isPerpetual } from '../../product';
|
import { isPerpetual } from '../../product';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import min from 'lodash/min';
|
||||||
|
import sum from 'lodash/sum';
|
||||||
|
import { useDuration } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
type MarketInfoProps = {
|
type MarketInfoProps = {
|
||||||
market: MarketInfo;
|
market: MarketInfo;
|
||||||
@ -779,13 +785,30 @@ export const RiskFactorsInfoPanel = ({
|
|||||||
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
|
return <MarketInfoTable data={data} parentData={parentData} unformatted />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PriceMonitoringBoundsInfoPanel = ({
|
type TriggerInfo = {
|
||||||
market,
|
maxValidPrice: BigNumber;
|
||||||
triggerIndex,
|
minValidPrice: BigNumber;
|
||||||
}: MarketInfoProps & {
|
referencePrice: BigNumber;
|
||||||
triggerIndex: number;
|
horizonSecs: number;
|
||||||
}) => {
|
probability: number;
|
||||||
|
auctionExtensionSecs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Calculates a trigger info group signature. */
|
||||||
|
const triggerInfoGroupIteratee = (t: TriggerInfo) =>
|
||||||
|
`${t.horizonSecs}|${
|
||||||
|
t.probability
|
||||||
|
}|${t.minValidPrice.toString()}|${t.maxValidPrice.toString()}`;
|
||||||
|
|
||||||
|
type ExtendedTriggerInfo = Omit<TriggerInfo, 'auctionExtensionSecs'> & {
|
||||||
|
minAuctionExtensionSecs: number;
|
||||||
|
maxAuctionExtensionSecs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PriceMonitoringBoundsInfoPanel = ({ market }: MarketInfoProps) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
|
const duration = useDuration();
|
||||||
|
const compactDuration = useDuration('compact');
|
||||||
const { data } = useDataProvider({
|
const { data } = useDataProvider({
|
||||||
dataProvider: marketDataProvider,
|
dataProvider: marketDataProvider,
|
||||||
variables: { marketId: market.id },
|
variables: { marketId: market.id },
|
||||||
@ -793,45 +816,159 @@ export const PriceMonitoringBoundsInfoPanel = ({
|
|||||||
|
|
||||||
const quoteUnit = getQuoteName(market);
|
const quoteUnit = getQuoteName(market);
|
||||||
|
|
||||||
const bounds = data?.priceMonitoringBounds?.[triggerIndex];
|
let triggers = Object.entries(
|
||||||
const trigger = bounds?.trigger;
|
groupBy(
|
||||||
|
data?.priceMonitoringBounds?.map((b) => ({
|
||||||
|
...omit(b.trigger, '__typename'),
|
||||||
|
maxValidPrice: toBigNum(b.maxValidPrice, market.decimalPlaces),
|
||||||
|
minValidPrice: toBigNum(b.minValidPrice, market.decimalPlaces),
|
||||||
|
referencePrice: toBigNum(b.referencePrice, market.decimalPlaces),
|
||||||
|
})),
|
||||||
|
triggerInfoGroupIteratee
|
||||||
|
)
|
||||||
|
).map(([_, ts]) => {
|
||||||
|
const info = omit(ts[0], 'auctionExtensionSecs');
|
||||||
|
const auctions = ts.map((t) => t.auctionExtensionSecs);
|
||||||
|
const minAuctionExtensionSecs = min(auctions) as number;
|
||||||
|
const maxAuctionExtensionSecs = sum(auctions);
|
||||||
|
const extendedInfo: ExtendedTriggerInfo = {
|
||||||
|
...info,
|
||||||
|
minAuctionExtensionSecs,
|
||||||
|
maxAuctionExtensionSecs,
|
||||||
|
};
|
||||||
|
return extendedInfo;
|
||||||
|
});
|
||||||
|
|
||||||
if (!trigger) {
|
triggers = orderBy(
|
||||||
return null;
|
triggers,
|
||||||
|
[
|
||||||
|
(bd) => bd.probability,
|
||||||
|
(bd) => bd.horizonSecs,
|
||||||
|
(bd) => bd.minAuctionExtensionSecs,
|
||||||
|
(bd) => bd.maxAuctionExtensionSecs,
|
||||||
|
],
|
||||||
|
['desc', 'asc', 'asc', 'asc']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!triggers || triggers.length === 0) {
|
||||||
|
return <div>{t('No price monitoring bounds detected.')}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const price = (price: string, direction: 'min' | 'max') => (
|
||||||
<>
|
<div
|
||||||
<div className="mb-2 grid grid-cols-2 text-xs">
|
className={classNames(
|
||||||
<p className="col-span-1">
|
'rounded px-1 py-[1px] bg-vega-clight-500 dark:bg-vega-cdark-500 relative',
|
||||||
{t('{{probability}} probability price bounds', {
|
'after:absolute after:content-[" "] after:z-10',
|
||||||
probability: formatNumberPercentage(
|
'after:block after:w-3 after:h-3 after:bg-vega-clight-500 dark:after:bg-vega-cdark-500 after:rotate-45 after:-translate-y-1/2',
|
||||||
new BigNumber(trigger.probability).times(100)
|
{
|
||||||
),
|
'after:top-1/2 after:right-[-6px]': direction === 'min',
|
||||||
})}
|
'after:top-1/2 after:left-[-6px]': direction === 'max',
|
||||||
</p>
|
}
|
||||||
<p className="col-span-1 text-right">
|
)}
|
||||||
{t('Within {{horizonSecs}} seconds', {
|
>
|
||||||
horizonSecs: formatNumber(trigger.horizonSecs),
|
<div
|
||||||
|
className={classNames('text-[10px]', {
|
||||||
|
'text-left': direction === 'min',
|
||||||
|
'text-right': direction === 'max',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{price} <span>{quoteUnit}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return triggers.map((trigger, index) => {
|
||||||
|
const probability = formatNumberPercentage(
|
||||||
|
new BigNumber(trigger.probability).times(100)
|
||||||
|
);
|
||||||
|
const within = compactDuration(trigger.horizonSecs * 1000);
|
||||||
|
return (
|
||||||
|
<div key={index} className="mb-2 border-b border-b-vega-clight-500">
|
||||||
|
<div className="font-mono text-xs flex">
|
||||||
|
{/** MIN PRICE */}
|
||||||
|
<Tooltip
|
||||||
|
description={t(
|
||||||
|
"Minimum price that isn't currently breaching the specified price monitoring trigger"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{price(trigger.minValidPrice.toString(10), 'min')}
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/** TRIGGERS WHEN */}
|
||||||
|
<Tooltip
|
||||||
|
description={t(
|
||||||
|
'{{probability}} of prices must be within the bounds for {{duration}}',
|
||||||
|
{
|
||||||
|
probability: probability,
|
||||||
|
duration: within,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div aria-hidden className="w-full text-center text-[10px]">
|
||||||
|
<div className="border-b-[2px] border-dashed border-vega-clight-500 dark:border-vega-cdark-500 w-full h-1/2 translate-y-[1px]">
|
||||||
|
{probability}
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
{t('within {{duration}}', {
|
||||||
|
duration: within,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/** MAX PRICE */}
|
||||||
|
<Tooltip
|
||||||
|
description={t(
|
||||||
|
"Maximum price that isn't currently breaching the specified price monitoring trigger"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{price(trigger.maxValidPrice.toString(10), 'max')}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<p className="my-2 text-xs leading-none">
|
||||||
|
{t('Results in {{duration}} auction if breached', {
|
||||||
|
duration:
|
||||||
|
trigger.minAuctionExtensionSecs !==
|
||||||
|
trigger.maxAuctionExtensionSecs
|
||||||
|
? `${duration(
|
||||||
|
trigger.minAuctionExtensionSecs * 1000
|
||||||
|
)} ~ ${duration(trigger.maxAuctionExtensionSecs * 1000)}`
|
||||||
|
: duration(trigger.minAuctionExtensionSecs * 1000),
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{bounds && (
|
);
|
||||||
<MarketInfoTable
|
});
|
||||||
data={{
|
};
|
||||||
highestPrice: bounds.maxValidPrice,
|
|
||||||
lowestPrice: bounds.minValidPrice,
|
export const PriceMonitoringSettingsInfoPanel = ({
|
||||||
}}
|
market,
|
||||||
decimalPlaces={market.decimalPlaces}
|
className,
|
||||||
assetSymbol={quoteUnit}
|
}: MarketInfoProps & { className?: classNames.Argument }) => {
|
||||||
/>
|
const t = useT();
|
||||||
)}
|
|
||||||
<p className="mt-2 text-xs">
|
const triggers = groupBy(
|
||||||
{t('Results in {{auctionExtensionSecs}} seconds auction if breached', {
|
market.priceMonitoringSettings?.parameters?.triggers?.map((t) =>
|
||||||
auctionExtensionSecs: trigger.auctionExtensionSecs.toString(),
|
omit(t, '__typename')
|
||||||
})}
|
) || [],
|
||||||
</p>
|
(t) => `${t.horizonSecs}|${t.probability}|${t.auctionExtensionSecs}`
|
||||||
</>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="grid grid-cols-3 md:grid-cols-6 gap-3">
|
||||||
|
{Object.entries(triggers).map(([_, trigger], i) => (
|
||||||
|
<span className="border p-3 rounded">
|
||||||
|
<dt className="text-sm font-bold">
|
||||||
|
{t('triggers', {
|
||||||
|
count: trigger.length,
|
||||||
|
})}
|
||||||
|
</dt>
|
||||||
|
<dt>
|
||||||
|
<MarketInfoTable data={trigger[0]} />
|
||||||
|
</dt>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,3 +14,4 @@ export * from './use-yesterday';
|
|||||||
export * from './use-previous';
|
export * from './use-previous';
|
||||||
export { useScript } from './use-script';
|
export { useScript } from './use-script';
|
||||||
export { useUserAgent } from './use-user-agent';
|
export { useUserAgent } from './use-user-agent';
|
||||||
|
export * from './use-duration';
|
||||||
|
53
libs/react-helpers/src/hooks/use-duration.ts
Normal file
53
libs/react-helpers/src/hooks/use-duration.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { convertToDuration } from '@vegaprotocol/utils';
|
||||||
|
import { useT } from '../use-t';
|
||||||
|
|
||||||
|
enum DurationKeys {
|
||||||
|
Days = 'duration_days',
|
||||||
|
Hours = 'duration_hours',
|
||||||
|
Minutes = 'duration_minutes',
|
||||||
|
Seconds = 'duration_seconds',
|
||||||
|
DaysCompact = 'duration_days_compact',
|
||||||
|
HoursCompact = 'duration_hours_compact',
|
||||||
|
MinutesCompact = 'duration_minutes_compact',
|
||||||
|
SecondsCompact = 'duration_seconds_compact',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDuration = (mode: 'normal' | 'compact' = 'normal') => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
let DAYS = DurationKeys.Days;
|
||||||
|
let HOURS = DurationKeys.Hours;
|
||||||
|
let MINUTES = DurationKeys.Minutes;
|
||||||
|
let SECONDS = DurationKeys.Seconds;
|
||||||
|
|
||||||
|
if (mode === 'compact') {
|
||||||
|
DAYS = DurationKeys.DaysCompact;
|
||||||
|
HOURS = DurationKeys.HoursCompact;
|
||||||
|
MINUTES = DurationKeys.MinutesCompact;
|
||||||
|
SECONDS = DurationKeys.SecondsCompact;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (durationInMilliseconds: number) => {
|
||||||
|
const duration = convertToDuration(durationInMilliseconds);
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
|
||||||
|
if (duration.days > 0) {
|
||||||
|
segments.push(t(DAYS, { count: duration.days }));
|
||||||
|
}
|
||||||
|
if (duration.hours > 0) {
|
||||||
|
segments.push(t(HOURS, { count: duration.hours }));
|
||||||
|
}
|
||||||
|
if (duration.minutes > 0) {
|
||||||
|
segments.push(t(MINUTES, { count: duration.minutes }));
|
||||||
|
}
|
||||||
|
if (duration.seconds > 0) {
|
||||||
|
segments.push(t(SECONDS, { count: duration.seconds }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length > 0) {
|
||||||
|
return segments.join(' ');
|
||||||
|
}
|
||||||
|
return t(SECONDS, { count: 0 });
|
||||||
|
};
|
||||||
|
};
|
3
libs/react-helpers/src/use-t.ts
Normal file
3
libs/react-helpers/src/use-t.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
export const ns = 'react-helpers';
|
||||||
|
export const useT = () => useTranslation(ns).t;
|
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
convertToCountdown,
|
|
||||||
convertToCountdownString,
|
convertToCountdownString,
|
||||||
|
convertToDuration,
|
||||||
getSecondsFromInterval,
|
getSecondsFromInterval,
|
||||||
} from './time';
|
} from './time';
|
||||||
|
|
||||||
@ -60,7 +60,8 @@ describe('convertToCountdown', () => {
|
|||||||
[30, 3, 12, 3],
|
[30, 3, 12, 3],
|
||||||
],
|
],
|
||||||
])('converts %d ms to %s', (time, countdown) => {
|
])('converts %d ms to %s', (time, countdown) => {
|
||||||
expect(convertToCountdown(time)).toEqual(countdown);
|
const duration = convertToDuration(time);
|
||||||
|
expect(Object.values(duration)).toEqual(countdown);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,7 +47,14 @@ export function getSecondsFromInterval(str: string) {
|
|||||||
return seconds;
|
return seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertToCountdown = (time: number) => {
|
type Duration = {
|
||||||
|
days: number;
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToDuration = (time: number): Duration => {
|
||||||
const s = 1000;
|
const s = 1000;
|
||||||
const m = 1000 * 60;
|
const m = 1000 * 60;
|
||||||
const h = 1000 * 60 * 60;
|
const h = 1000 * 60 * 60;
|
||||||
@ -60,7 +67,7 @@ export const convertToCountdown = (time: number) => {
|
|||||||
const minutes = Math.floor((t - days * d - hours * h) / m);
|
const minutes = Math.floor((t - days * d - hours * h) / m);
|
||||||
const seconds = Math.floor((t - days * d - hours * h - minutes * m) / s);
|
const seconds = Math.floor((t - days * d - hours * h - minutes * m) / s);
|
||||||
|
|
||||||
return [days, hours, minutes, seconds];
|
return { days, hours, minutes, seconds };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,7 +77,7 @@ export const convertToCountdownString = (
|
|||||||
time: number,
|
time: number,
|
||||||
pattern = '0d00h00m00s'
|
pattern = '0d00h00m00s'
|
||||||
) => {
|
) => {
|
||||||
const values = convertToCountdown(time);
|
const values = Object.values(convertToDuration(time));
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const countdown = pattern
|
const countdown = pattern
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
"declaration": true,
|
"declaration": true,
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts", "../react-helpers/src/lib/use-duration.ts"],
|
||||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user