From e16abd94c0e82c09cca36c0cca59615ef62ef710 Mon Sep 17 00:00:00 2001 From: Ken Kunz Date: Thu, 12 Sep 2024 16:24:11 -0500 Subject: [PATCH 1/8] refactor TradeDirection (enum -> simple union) --- src/lib/trade-executor/state/trade-info.ts | 26 +++++++++++-------- .../trade-executor/state/trading-pair-info.ts | 12 --------- .../PositionTable.svelte | 2 +- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/lib/trade-executor/state/trade-info.ts b/src/lib/trade-executor/state/trade-info.ts index 403e5033f..2834ecb50 100644 --- a/src/lib/trade-executor/state/trade-info.ts +++ b/src/lib/trade-executor/state/trade-info.ts @@ -8,10 +8,7 @@ import type { TradeExecution } from './trade'; import type { USDollarAmount } from './utility-types'; -export enum TradeDirection { - Enter, - Exit -} +export type TradeDirection = 'enter' | 'exit'; /** * Prototype object that can be applied to a TradeExecution object to enrich @@ -49,8 +46,15 @@ const tradeInfoPrototype = { }, // Determine trade direction (enter|exit) based on planned_quantity - get direction() { - return this.planned_quantity > 0 ? TradeDirection.Enter : TradeDirection.Exit; + get direction(): TradeDirection { + return this.planned_quantity > 0 ? 'enter' : 'exit'; + }, + + get directionLabel() { + if (this.isCreditTrade) { + return this.direction === 'enter' ? 'Supply' : 'Withdraw'; + } + return this.direction === 'enter' ? 'Buy' : 'Sell'; }, get pricingPair() { @@ -58,7 +62,7 @@ const tradeInfoPrototype = { }, get actionLabel() { - return this.pair.getActionLabel(this.direction); + return `${this.directionLabel} ${this.pair.actionSymbol}`; }, get isCreditTrade() { @@ -78,10 +82,10 @@ const tradeInfoPrototype = { }, get positionImpact() { - // NOTE: order of flags is significant since trades may have multiple flags - for (const flag of ['open', 'close', 'increase', 'reduce'] as const) { - if (this.flags?.includes(flag)) return flag; - } + // order of flags is significant since trades may have multiple flags + const impacts = ['open', 'close', 'increase', 'reduce'] as const; + // return first matching impact flag (if any) + return impacts.find((flag) => this.flags?.includes(flag)); } } satisfies ThisType>; diff --git a/src/lib/trade-executor/state/trading-pair-info.ts b/src/lib/trade-executor/state/trading-pair-info.ts index fee64580b..e1e6c3545 100644 --- a/src/lib/trade-executor/state/trading-pair-info.ts +++ b/src/lib/trade-executor/state/trading-pair-info.ts @@ -6,7 +6,6 @@ * */ import type { TradingPairIdentifier } from './identifier'; -import { TradeDirection } from './trade-info'; const kindShortLabels = { spot_market_hold: 'spot', @@ -57,17 +56,6 @@ const tradingPairInfoPrototype = { get kindShortLabel() { return kindShortLabels[this.kind]; - }, - - getDirectionLabel(direction: TradeDirection) { - if (direction === TradeDirection.Enter) { - return this.isCreditSupply ? 'Supply' : 'Buy'; - } - return this.isCreditSupply ? 'Withdraw' : 'Sell'; - }, - - getActionLabel(direction: TradeDirection) { - return `${this.getDirectionLabel(direction)} ${this.actionSymbol}`; } } satisfies ThisType>; diff --git a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/PositionTable.svelte b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/PositionTable.svelte index 1d3e47fd1..b53065115 100644 --- a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/PositionTable.svelte +++ b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/PositionTable.svelte @@ -72,7 +72,7 @@ table.column({ header: 'Frozen on', id: 'frozen_on', - accessor: (position) => position.lastTrade?.direction + accessor: (position) => position.lastTrade?.directionLabel }), table.column({ header: 'Value', From 2e22663438290047cc8c47a42adcb575773ff082 Mon Sep 17 00:00:00 2001 From: Ken Kunz Date: Thu, 12 Sep 2024 16:26:35 -0500 Subject: [PATCH 2/8] fix broken position remarks tooltip links --- .../[status=positionStatus]-positions/FlagCell.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/FlagCell.svelte b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/FlagCell.svelte index 932555268..19e7977a5 100644 --- a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/FlagCell.svelte +++ b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/FlagCell.svelte @@ -18,7 +18,7 @@

Position can still have a profitable close if a trailing or dynamic stop loss was used.

See more

@@ -47,7 +47,7 @@

See more

{/if} From 1f8f2f877371d7e9f827b51110f6898db65aa9cc Mon Sep 17 00:00:00 2001 From: Ken Kunz Date: Thu, 12 Sep 2024 17:10:29 -0500 Subject: [PATCH 3/8] improve failed trade/position indicators & alerts --- src/lib/explorer/TradingDescription.svelte | 24 ++++---- .../[position=integer]/+page.svelte | 56 ++++++++++--------- .../[position=integer]/TradeTable.svelte | 2 +- .../trade-[trade=integer]/+page.svelte | 2 +- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/lib/explorer/TradingDescription.svelte b/src/lib/explorer/TradingDescription.svelte index 6643878ff..e8aa84307 100644 --- a/src/lib/explorer/TradingDescription.svelte +++ b/src/lib/explorer/TradingDescription.svelte @@ -20,24 +20,26 @@ Used in DataTable context (vs. standard svelte component context). import { DataBadge } from '$lib/components'; export let label: string; - export let modifier: string | undefined; + export let modifier = ''; export let isTest = false; + export let failed = false;
{label} - {#if modifier} - - {modifier} - - {/if} - {#if isTest} - + + {modifier} + + + {#if isTest} Test - - {/if} + {/if} + {#if failed} + Failed + {/if} +
diff --git a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/TradeTable.svelte b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/TradeTable.svelte index 361ce6649..eb12460bd 100644 --- a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/TradeTable.svelte +++ b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/TradeTable.svelte @@ -28,7 +28,7 @@ table.column({ id: 'description', header: 'Trade', - accessor: (t) => ({ label: t.actionLabel, modifier: t.positionImpact, isTest: t.isTest }), + accessor: (t) => ({ label: t.actionLabel, modifier: t.positionImpact, isTest: t.isTest, failed: t.failed }), cell: ({ value }) => createRender(TradingDescription, value) }), table.column({ diff --git a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/+page.svelte b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/+page.svelte index 7c65e1fed..f3619ff07 100644 --- a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/+page.svelte +++ b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/[position=integer]/trade-[trade=integer]/+page.svelte @@ -52,7 +52,7 @@ {#if trade.failed} - +
  • Failure reason: {trade.failedTx?.revert_reason ?? 'unknown'}
  • {#if trade.failedTx?.tx_hash} From d663006d0e97a63b19e54262c82bdac82bee9a56 Mon Sep 17 00:00:00 2001 From: Ken Kunz Date: Fri, 13 Sep 2024 10:53:59 -0500 Subject: [PATCH 4/8] add "profitability data inconsistent" remark - displayed on strategy positions table for rows with inconsistent data - only displayed for users with admin role --- src/lib/trade-executor/state/position-info.ts | 38 +++++++++++++++++++ .../+page.svelte | 3 +- .../FlagCell.svelte | 32 ++++++++++++++++ .../PositionTable.svelte | 3 +- 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/lib/trade-executor/state/position-info.ts b/src/lib/trade-executor/state/position-info.ts index 16f3d7919..667b8604e 100644 --- a/src/lib/trade-executor/state/position-info.ts +++ b/src/lib/trade-executor/state/position-info.ts @@ -10,6 +10,7 @@ import type { State } from './state'; import type { PositionStatistics } from './statistics'; import type { TimeBucket } from '$lib/chart'; import { type PositionStatus, type TradingPosition, tradingPositionTooltips } from './position'; +import type { TradeDirection } from './trade-info'; /** * English tooltips for the datapoints @@ -215,6 +216,43 @@ const tradingPositionInfoPrototype = { return this.latestStats?.profitability; }, + valueForTradeDirection(direction: TradeDirection) { + return this.trades.reduce((acc, t) => { + if (t.direction === direction) acc += t.executedValue; + return acc; + }, 0); + }, + + get totalEnteredValue() { + return this.valueForTradeDirection('enter'); + }, + + get totalExitedValue() { + return this.valueForTradeDirection('exit'); + }, + + get profitabilityFromTradeTotals() { + if (!this.closed) return; + + const totalEntered = this.totalEnteredValue; + const totalExited = this.totalExitedValue; + const profitability = (totalExited - totalEntered) / totalEntered; + return Number.isFinite(profitability) ? profitability : 0; + }, + + get hasInconsistentProfitability() { + const fromStats = this.profitability; + const fromTradeTotals = this.profitabilityFromTradeTotals; + + if (!this.closed || fromStats === undefined || fromTradeTotals === undefined) { + return false; + } + + const difference = fromTradeTotals - fromStats; + const percentChange = difference / fromStats; + return Math.abs(percentChange) > 0.01 && Math.abs(difference) > 0.001; + }, + get candleTimeBucket(): TimeBucket { return this.durationSeconds > 7 * 24 * 3600 ? '1d' : '1h'; }, diff --git a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/+page.svelte b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/+page.svelte index 5ced65b79..9687c8661 100644 --- a/src/routes/strategies/[strategy]/[status=positionStatus]-positions/+page.svelte +++ b/src/routes/strategies/[strategy]/[status=positionStatus]-positions/+page.svelte @@ -7,7 +7,7 @@ import { capitalize } from '$lib/helpers/formatters'; export let data; - $: ({ positions, status, strategy } = data); + $: ({ admin, positions, status, strategy } = data); $: q = $page.url.searchParams; $: options = { @@ -39,6 +39,7 @@ {/if} import type { TradingPositionInfo } from 'trade-executor/state/position-info'; import type { TradeInfo } from 'trade-executor/state/trade-info'; + import { Alert } from '$lib/components'; import PositionFlag from './PositionFlag.svelte'; + import { formatNumber, formatPercent } from '$lib/helpers/formatters'; + export let admin = false; export let position: TradingPositionInfo; export let baseUrl: string; @@ -51,6 +54,35 @@
{/if} + + {#if admin && position.hasInconsistentProfitability} + +

+ This info is only displayed to admin users. +

+

+ This position has inconsistent profitability data. The value from position stats does not match the result of + comparing all entry and exit trades. +

+
    +
  • Profitability value from position stats: {formatPercent(position.profitability, 2)}
  • +
  • + Profitability value from entry/exit trades: + {formatPercent(position.profitabilityFromTradeTotals, 2)} +
      +
    • Sum of entry trades: ${formatNumber(position.totalEnteredValue)}
    • +
    • Sum of exit trades: ${formatNumber(position.totalExitedValue)}
    • +
    • + (${formatNumber(position.totalExitedValue)} - ${formatNumber(position.totalEnteredValue)}) / ${formatNumber( + position.totalEnteredValue + )} = + {formatPercent(position.profitabilityFromTradeTotals, 2)} +
    • +
    +
  • +
+
+ {/if}