Skip to content

Commit

Permalink
use inverse candlestick data in chart (#1475)
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime authored Jul 18, 2024
1 parent a001013 commit b6c6a3c
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 29 deletions.
92 changes: 76 additions & 16 deletions apps/minifront/src/state/swap/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,20 @@ export const sendSimulateTradeRequest = ({
return simulationClient.simulateTrade(req);
};

export const sendCandlestickDataRequest = async (
/**
* Due to the way price data is recorded, symmetric comparisons do not return
* symmetric data. to get the complete picture, a client must combine both
* datasets.
* 1. query the intended comparison direction (start token -> end token)
* 2. query the inverse comparison direction (end token -> start token)
* 3. flip the inverse data (reciprocal values, high becomes low)
* 4. combine the data (use the highest high, lowest low, sum volumes)
*/
export const sendCandlestickDataRequests = async (
{ startMetadata, endMetadata }: Pick<PriceHistorySlice, 'startMetadata' | 'endMetadata'>,
limit: bigint,
signal?: AbortSignal,
): Promise<CandlestickData[] | undefined> => {
): Promise<CandlestickData[]> => {
const start = startMetadata?.penumbraAssetId;
const end = endMetadata?.penumbraAssetId;

Expand All @@ -65,22 +74,73 @@ export const sendCandlestickDataRequest = async (
throw new Error('Asset pair equivalent');
}

try {
const { data } = await dexClient.candlestickData(
{
pair: { start, end },
limit,
},
{ signal },
);
return data;
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
return;
} else {
const suppressAbort = (err: unknown) => {
if (!signal?.aborted) {
throw err;
}
}
};

const directReq = dexClient
.candlestickData({ pair: { start, end }, limit }, { signal })
.catch(suppressAbort);
const inverseReq = dexClient
.candlestickData({ pair: { start: end, end: start }, limit }, { signal })
.catch(suppressAbort);

const directCandles = (await directReq)?.data ?? [];
const inverseCandles = (await inverseReq)?.data ?? [];

// collect candles at each height
const collatedByHeight = Map.groupBy(
[
...directCandles,
...inverseCandles.map(
// flip inverse data to match orientation of direct data
inverseCandle => {
const correctedCandle = inverseCandle.clone();
// comparative values are reciprocal
correctedCandle.open = 1 / inverseCandle.open;
correctedCandle.close = 1 / inverseCandle.close;
// high and low swap places
correctedCandle.high = 1 / inverseCandle.low;
correctedCandle.low = 1 / inverseCandle.high;
return correctedCandle;
},
),
],
({ height }) => height,
);

// combine data at each height into a single candle
const combinedCandles = Array.from(collatedByHeight.entries()).map(
([height, candlesAtHeight]) => {
// TODO: open/close don't diverge much, and when they do it seems to be due
// to inadequate number precision. it might be better to just pick one, but
// it's not clear which one is 'correct'
const combinedCandleAtHeight = candlesAtHeight.reduce((acc, cur) => {
// sum volumes
acc.directVolume += cur.directVolume;
acc.swapVolume += cur.swapVolume;

// highest high, lowest low
acc.high = Math.max(acc.high, cur.high);
acc.low = Math.min(acc.low, cur.low);

// these accumulate to be averaged
acc.open += cur.open;
acc.close += cur.close;
return acc;
}, new CandlestickData({ height }));

// average accumulated open/close
combinedCandleAtHeight.open /= candlesAtHeight.length;
combinedCandleAtHeight.close /= candlesAtHeight.length;

return combinedCandleAtHeight;
},
);

return combinedCandles;
};

const byBalanceDescending = (a: BalancesResponse, b: BalancesResponse) => {
Expand Down
25 changes: 12 additions & 13 deletions apps/minifront/src/state/swap/price-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/
import { CandlestickData } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb.js';
import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response';
import { AllSlices, SliceCreator } from '..';
import { sendCandlestickDataRequest } from './helpers';
import { sendCandlestickDataRequests } from './helpers';
import { PRICE_RELEVANCE_THRESHOLDS } from '@penumbra-zone/types/assets';

interface Actions {
load: (ac?: AbortController) => AbortController['abort'];
Expand All @@ -26,20 +27,18 @@ export const createPriceHistorySlice = (): SliceCreator<PriceHistorySlice> => (s
const { assetIn, assetOut } = get().swap;
const startMetadata = getMetadataFromBalancesResponseOptional(assetIn);
const endMetadata = assetOut;
void sendCandlestickDataRequest(
void sendCandlestickDataRequests(
{ startMetadata, endMetadata },
// there's no UI to set limit yet, and most ranges don't always happen to
// include price records. 2500 at least scales well when there is data
2500n,
// there's no UI to set limit yet, and any given range won't always happen
// to include price records.
PRICE_RELEVANCE_THRESHOLDS.default * 2n,
ac.signal,
).then(data => {
if (data) {
set(({ swap }) => {
swap.priceHistory.startMetadata = startMetadata;
swap.priceHistory.endMetadata = endMetadata;
swap.priceHistory.candles = data;
});
}
).then(candles => {
set(({ swap }) => {
swap.priceHistory.startMetadata = startMetadata;
swap.priceHistory.endMetadata = endMetadata;
swap.priceHistory.candles = candles;
});
});

return () => ac.abort('Returned slice abort');
Expand Down

0 comments on commit b6c6a3c

Please sign in to comment.