Skip to content

Commit

Permalink
Merge pull request #813 from tradingstrategy-ai/812-position-issue
Browse files Browse the repository at this point in the history
Add position remark for inconsistent profit data
  • Loading branch information
kenkunz authored Sep 16, 2024
2 parents fcd7c8c + 9a86e4a commit c139d15
Show file tree
Hide file tree
Showing 16 changed files with 395 additions and 216 deletions.
24 changes: 13 additions & 11 deletions src/lib/explorer/TradingDescription.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
</script>

<div class="description-cell">
<span class="label">
{label}
</span>
{#if modifier}
<span class="modifier">
{modifier}
</span>
{/if}
{#if isTest}
<span class="test-badge">
<span class="modifier">
{modifier}
</span>
<span class="badges">
{#if isTest}
<DataBadge status="warning">Test</DataBadge>
</span>
{/if}
{/if}
{#if failed}
<DataBadge status="error">Failed</DataBadge>
{/if}
</span>
</div>

<style lang="postcss">
Expand All @@ -51,7 +53,7 @@ Used in DataTable context (vs. standard svelte component context).
color: var(--c-text-extra-light);
}
.test-badge {
.badges {
font: var(--f-ui-xs-medium);
letter-spacing: var(--ls-ui-xs);
Expand Down
124 changes: 52 additions & 72 deletions src/lib/trade-executor/state/position-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,65 +9,13 @@ import type { Percent, PrimaryKeyString, USDollarAmount, USDollarPrice } from '.
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 PositionStatus, type TradingPosition } from './position';
import { type TradeDirection, TradeDirections } from './trade-info';

/**
* English tooltips for the datapoints
*/
export const tradingPositionInfoTooltips = {
...tradingPositionTooltips,
durationSeconds:
'How long this position was or has been open. The duration is calcualated from the open decision time to the closing trade execution time.',
stillOpen: 'Is the position currently open.',
candleTimeBucket: 'Which candles we use to visualise the history of this position.',
openPrice: 'The execution price of the opening trade.',
closePrice: 'The closing price of the position.',
currentPrice: 'The latest recorded price of the position.',
interestRateAtOpen: 'The opening interest rate of the position.',
interestRateAtClose: 'Closing interest rate is not currently available.',
currentInterestRate: 'The latest recorded interest rate of the position.',
realisedProfitability:
'The realised profitability of the position. BETA WARNING: Currently calculation may not be correct for multitrade positions.',
unrealisedProfitability:
'The current estimated profitability of the position if closed now. BETA WARNING: Currently calculation may not be correct for multitrade positions.',
portfolioWeightAtOpen: 'The position size in the terms of % total portfolio when the position was opened.',
valueAtOpen: 'The position value when the position was opened.',
valueAtClose: 'The position value when the position was closed.',
currentValue: 'The last recorded value of the position.',
quantityAtOpen: 'The position size in tokens when the position was opened.',
quantityAtClose: 'The position size in tokens when the position was closed.',
currentQuantity: 'The latest recorded position size in tokens.',
estimatedMaximumRisk: 'How much % of the portfolio is at the risk if this position is completely lost.',
stopLossPercentOpen:
'Stop loss % for this position, relative to the opening price. Stop loss may be dynamic and trailing stop loss may increase over time. BETA WARNING: Currently calculated relative to the open price, not the current price.',
stopLossPercentOpenMissing: 'Stop loss not used at the position open or the value was not recorded',
stopLossPrice:
'Stop loss price for this position. Position is attempted closed as soon as possible if the market mid-price crosses this level.',
stopLossTriggered:
'Stop loss was triggered for this position. Stop loss can still close at profit if a trailing stop loss or other form of dynamic stop loss was used.',
marketMidPriceAtOpen: 'What was the market mid-price when this position was opened.',
portfolioRiskPercent:
'Maximum portfolio % value at a risk when the position was opened. This risk assumes any stop losses can be executed without significant price impact or slippage.',
portfolioRiskPercentMissing: 'Stop loss data not recorded or stop loss was not used and cannot calculate this value.',
volume: 'How much trading volume trades of this position have generated',
tradingFees: 'How much trading fees were total. This includes protocol fees and liquidity provider fees',
tradingFeesMissing: 'Trading fee data was not recorded for this position',
tradingFeesPercent:
'How much trading fees were % of trading volume. This includes protocol fees and liquidity provider fees'
};

type TradingPositionWithStats = TradingPosition & {
stats: PositionStatistics[];
};
export const createTradingPositionInfo = <T extends TradingPosition>(base: T, stats: PositionStatistics[] = []) => ({
...base,

/**
* Prototype object that can be applied to a TradingPosition object to enrich
* it with additional properties. Yields an object with all the properties
* (and types) of the original plus the inherited prototype properties/types
* (which is non-trivial with TypeScript classes)
*/
const tradingPositionInfoPrototype = {
tooltip: tradingPositionInfoTooltips,
stats,

get durationSeconds() {
const endDate = this.closed_at ?? Date.now();
Expand Down Expand Up @@ -143,7 +91,7 @@ const tradingPositionInfoPrototype = {
*/
get valueAtClose() {
if (this.closed) {
return this.lastTrade.executedValue;
return this.lastTrade!.executedValue;
}
},

Expand Down Expand Up @@ -215,6 +163,45 @@ const tradingPositionInfoPrototype = {
return this.latestStats?.profitability;
},

// return sum of all trade values for a given direction (enter = 1 or exit = -1)
valueForTradeDirection(direction: TradeDirection) {
return this.trades.reduce((acc, t) => {
if (t.direction === direction) acc += t.executedValue;
return acc;
}, 0);
},

get totalEnteredValue() {
return this.valueForTradeDirection(TradeDirections.Enter);
},

get totalExitedValue() {
return this.valueForTradeDirection(TradeDirections.Exit);
},

get profitabilityFromTradeTotals() {
if (!this.closed) return;

const totalEntered = this.totalEnteredValue;
const totalExited = this.totalExitedValue;
let profitability = (totalExited - totalEntered) / totalEntered;
if (this.isShortPosition) profitability *= -1;
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';
},
Expand All @@ -223,6 +210,10 @@ const tradingPositionInfoPrototype = {
return this.pair.isCreditSupply;
},

get isShortPosition() {
return this.pair.isShort;
},

get interestRateAtOpen() {
return this.loan?.collateral.interest_rate_at_open;
},
Expand Down Expand Up @@ -288,20 +279,9 @@ const tradingPositionInfoPrototype = {
get isTest() {
return this.trades.some((t) => t.isTest);
}
} satisfies ThisType<TradingPositionWithStats & Record<string, any>>;

export type TradingPositionInfo = TradingPositionWithStats & typeof tradingPositionInfoPrototype;
});

/**
* Factory function to create a TradingPositionInfo object
*/
export function createTradingPositionInfo(
position: TradingPosition,
stats: PositionStatistics[] = []
): TradingPositionInfo {
const positionInfo = Object.create(tradingPositionInfoPrototype);
return Object.assign(positionInfo, position, { stats });
}
export type TradingPositionInfo = ReturnType<typeof createTradingPositionInfo<TradingPosition>>;

/**
* Get a single TradingPositionInfo object from state for a given status and id
Expand All @@ -316,7 +296,7 @@ export function getTradingPositionInfo(state: State, status: PositionStatus, id:
/**
* Get all TradingPositionInfo objects from state for a given status
*/
export function getTradingPositionInfoArray(state: State, status: PositionStatus): TradingPositionInfo[] {
export function getTradingPositionInfoArray(state: State, status: PositionStatus) {
const positions = state.portfolio[`${status}_positions`];
return Object.values(positions).map((position) => {
const stats = state.stats.positions[position.position_id];
Expand Down
50 changes: 50 additions & 0 deletions src/lib/trade-executor/state/position-tooltips.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* English tooltips for trading position properties and getters
*/
export const positionTooltips = {
// Properties - see ./position.ts
opened_at: 'The strategy cycle decision time when the strategy decided to open this trade.',
closed_at:
'The block timestamp when the closing trade of this position executed. This can be outside normal strategy decision making cycles when stop loss or take profit signals are triggered.',
trailing_stop_loss_pct: 'If trailing stop loss was turned on, what was its value relative to the position value.',

// Getters - see ./position-info.ts
durationSeconds:
'How long this position was or has been open. The duration is calcualated from the open decision time to the closing trade execution time.',
stillOpen: 'Is the position currently open.',
candleTimeBucket: 'Which candles we use to visualise the history of this position.',
openPrice: 'The execution price of the opening trade.',
closePrice: 'The closing price of the position.',
currentPrice: 'The latest recorded price of the position.',
interestRateAtOpen: 'The opening interest rate of the position.',
interestRateAtClose: 'Closing interest rate is not currently available.',
currentInterestRate: 'The latest recorded interest rate of the position.',
realisedProfitability:
'The realised profitability of the position. BETA WARNING: Currently calculation may not be correct for multitrade positions.',
unrealisedProfitability:
'The current estimated profitability of the position if closed now. BETA WARNING: Currently calculation may not be correct for multitrade positions.',
portfolioWeightAtOpen: 'The position size in the terms of % total portfolio when the position was opened.',
valueAtOpen: 'The position value when the position was opened.',
valueAtClose: 'The position value when the position was closed.',
currentValue: 'The last recorded value of the position.',
quantityAtOpen: 'The position size in tokens when the position was opened.',
quantityAtClose: 'The position size in tokens when the position was closed.',
currentQuantity: 'The latest recorded position size in tokens.',
estimatedMaximumRisk: 'How much % of the portfolio is at the risk if this position is completely lost.',
stopLossPercentOpen:
'Stop loss % for this position, relative to the opening price. Stop loss may be dynamic and trailing stop loss may increase over time. BETA WARNING: Currently calculated relative to the open price, not the current price.',
stopLossPercentOpenMissing: 'Stop loss not used at the position open or the value was not recorded',
stopLossPrice:
'Stop loss price for this position. Position is attempted closed as soon as possible if the market mid-price crosses this level.',
stopLossTriggered:
'Stop loss was triggered for this position. Stop loss can still close at profit if a trailing stop loss or other form of dynamic stop loss was used.',
marketMidPriceAtOpen: 'What was the market mid-price when this position was opened.',
portfolioRiskPercent:
'Maximum portfolio % value at a risk when the position was opened. This risk assumes any stop losses can be executed without significant price impact or slippage.',
portfolioRiskPercentMissing: 'Stop loss data not recorded or stop loss was not used and cannot calculate this value.',
volume: 'How much trading volume trades of this position have generated',
tradingFees: 'How much trading fees were total. This includes protocol fees and liquidity provider fees',
tradingFeesMissing: 'Trading fee data was not recorded for this position',
tradingFeesPercent:
'How much trading fees were % of trading volume. This includes protocol fees and liquidity provider fees'
} as const;
10 changes: 0 additions & 10 deletions src/lib/trade-executor/state/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,3 @@ export const tradingPositionSchema = z.object({
liquidation_price: usDollarAmount.nullish()
});
export type TradingPosition = z.infer<typeof tradingPositionSchema>;

/**
* English tooltips for the datapoints
*/
export const tradingPositionTooltips = {
opened_at: 'The strategy cycle decision time when the strategy decided to open this trade.',
closed_at:
'The block timestamp when the closing trade of this position executed. This can be outside normal strategy decision making cycles when stop loss or take profit signals are triggered.',
trailing_stop_loss_pct: 'If trailing stop loss was turned on, what was its value relative to the position value.'
};
116 changes: 116 additions & 0 deletions src/lib/trade-executor/state/trade-info.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { fixture } from './utility-type-fixtures';
import { tradeExecutionSchema } from './trade';
import { type TradeInfo, TradeDirections, createTradeInfo } from './trade-info';

const tradeExecution = fixture.fromSchema(tradeExecutionSchema);

describe('a spot long position trade', () => {
let tradeInfo: TradeInfo;

beforeEach(() => {
tradeInfo = createTradeInfo(tradeExecution);
tradeInfo.pair.kind = 'spot_market_hold';
});

describe('with positive planned quantity', () => {
beforeEach(() => {
tradeInfo.planned_quantity = 1;
});

test('should have direction "Enter"', () => {
expect(tradeInfo.direction).toBe(TradeDirections.Enter);
});

test('should have directionLabel "Buy"', () => {
expect(tradeInfo.directionLabel).toBe('Buy');
});
});

describe('with negative planned quantity', () => {
beforeEach(() => {
tradeInfo.planned_quantity = -1;
});

test('should have direction "Exit"', () => {
expect(tradeInfo.direction).toBe(TradeDirections.Exit);
});

test('should have directionLabel "Sell"', () => {
expect(tradeInfo.directionLabel).toBe('Sell');
});
});
});

describe('a lending protocol short position trade', () => {
let tradeInfo: TradeInfo;

beforeEach(() => {
tradeInfo = createTradeInfo(tradeExecution);
tradeInfo.pair.kind = 'lending_protocol_short';
});

describe('with positive planned quantity', () => {
beforeEach(() => {
tradeInfo.planned_quantity = 1;
});

test('should have direction "Exit"', () => {
expect(tradeInfo.direction).toBe(TradeDirections.Exit);
});

test('should have directionLabel "Buy"', () => {
expect(tradeInfo.directionLabel).toBe('Buy');
});
});

describe('with negative planned quantity', () => {
beforeEach(() => {
tradeInfo.planned_quantity = -1;
});

test('should have direction "Enter"', () => {
expect(tradeInfo.direction).toBe(TradeDirections.Enter);
});

test('should have directionLabel "Sell"', () => {
expect(tradeInfo.directionLabel).toBe('Sell');
});
});
});

describe('a credit position trade', () => {
let tradeInfo: TradeInfo;

beforeEach(() => {
tradeInfo = createTradeInfo(tradeExecution);
tradeInfo.pair.kind = 'credit_supply';
});

describe('with positive planned quantity', () => {
beforeEach(() => {
tradeInfo.planned_quantity = 1;
});

test('should have direction "Enter"', () => {
expect(tradeInfo.direction).toBe(TradeDirections.Enter);
});

test('should have directionLabel "Supply"', () => {
expect(tradeInfo.directionLabel).toBe('Supply');
});
});

describe('with negative planned quantity', () => {
beforeEach(() => {
tradeInfo.planned_quantity = -1;
});

test('should have direction "Exit"', () => {
expect(tradeInfo.direction).toBe(TradeDirections.Exit);
});

test('should have directionLabel "Withdraw"', () => {
expect(tradeInfo.directionLabel).toBe('Withdraw');
});
});
});
Loading

0 comments on commit c139d15

Please sign in to comment.