mirror of
https://github.com/freqtrade/frequi.git
synced 2024-11-10 18:23:50 +00:00
Merge pull request #799 from freqtrade/chandle_dynamic
Improve chart visualization for long/short trades
This commit is contained in:
commit
e510c7213f
|
@ -5,10 +5,11 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
|
||||
import { Trade, PairHistory, PlotConfig } from '@/types';
|
||||
import randomColor from '@/shared/randomColor';
|
||||
import { roundTimeframe } from '@/shared/timemath';
|
||||
import heikinashi from '@/shared/heikinashi';
|
||||
import { getTradeEntries } from '@/shared/charts/tradeChartData';
|
||||
import ECharts from 'vue-echarts';
|
||||
|
||||
import { use } from 'echarts/core';
|
||||
|
@ -67,10 +68,6 @@ const buySignalColor = '#00ff26';
|
|||
const shortEntrySignalColor = '#00ff26';
|
||||
const sellSignalColor = '#faba25';
|
||||
const shortexitSignalColor = '#faba25';
|
||||
const tradeBuyColor = 'cyan';
|
||||
const tradeSellColor = 'pink';
|
||||
|
||||
import { defineComponent, ref, computed, onMounted, watch } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CandleChart',
|
||||
|
@ -101,10 +98,6 @@ export default defineComponent({
|
|||
return props.dataset ? props.dataset.timeframe : '';
|
||||
});
|
||||
|
||||
const timeframems = computed(() => {
|
||||
return props.dataset ? props.dataset.timeframe_ms : 0;
|
||||
});
|
||||
|
||||
const datasetColumns = computed(() => {
|
||||
return props.dataset ? props.dataset.columns : [];
|
||||
});
|
||||
|
@ -121,34 +114,6 @@ export default defineComponent({
|
|||
return `${strategy.value} - ${pair.value} - ${timeframe.value}`;
|
||||
});
|
||||
|
||||
/** Return trade entries for charting */
|
||||
const getTradeEntries = () => {
|
||||
const trades: (string | number)[][] = [];
|
||||
const tradesClose: (string | number)[][] = [];
|
||||
for (let i = 0, len = filteredTrades.value.length; i < len; i += 1) {
|
||||
const trade: Trade = filteredTrades.value[i];
|
||||
if (
|
||||
trade.open_timestamp >= props.dataset.data_start_ts &&
|
||||
trade.open_timestamp <= props.dataset.data_stop_ts
|
||||
) {
|
||||
trades.push([roundTimeframe(timeframems.value, trade.open_timestamp), trade.open_rate]);
|
||||
}
|
||||
if (
|
||||
trade.close_timestamp !== undefined &&
|
||||
trade.close_timestamp < props.dataset.data_stop_ts &&
|
||||
trade.close_timestamp > props.dataset.data_start_ts
|
||||
) {
|
||||
if (trade.close_date !== undefined && trade.close_rate !== undefined) {
|
||||
tradesClose.push([
|
||||
roundTimeframe(timeframems.value, trade.close_timestamp),
|
||||
trade.close_rate,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { trades, tradesClose };
|
||||
};
|
||||
|
||||
const updateChart = (initial = false) => {
|
||||
if (!hasData.value) {
|
||||
return;
|
||||
|
@ -210,12 +175,16 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = props.heikinAshi
|
||||
? heikinashi(datasetColumns.value, props.dataset.data)
|
||||
: props.dataset.data.slice();
|
||||
// Add new rows to end to allow slight "scroll past"
|
||||
const newArray = Array(dataset[dataset.length - 2].length);
|
||||
newArray[colDate] = dataset[dataset.length - 1][colDate] + props.dataset.timeframe_ms * 3;
|
||||
dataset.push(newArray);
|
||||
const options: EChartsOption = {
|
||||
dataset: {
|
||||
source: props.heikinAshi
|
||||
? heikinashi(datasetColumns.value, props.dataset.data)
|
||||
: props.dataset.data,
|
||||
source: dataset,
|
||||
},
|
||||
grid: [
|
||||
{
|
||||
|
@ -266,7 +235,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
{
|
||||
name: 'Long',
|
||||
name: 'Entry',
|
||||
type: 'scatter',
|
||||
symbol: 'triangle',
|
||||
symbolSize: 10,
|
||||
|
@ -284,12 +253,12 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
if (colSellData >= 0) {
|
||||
if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
|
||||
chartOptions.value.legend.data.push('Long exit');
|
||||
}
|
||||
// if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
|
||||
// chartOptions.value.legend.data.push('Long exit');
|
||||
// }
|
||||
if (Array.isArray(options.series)) {
|
||||
options.series.push({
|
||||
name: 'Long exit',
|
||||
name: 'Exit',
|
||||
type: 'scatter',
|
||||
symbol: 'diamond',
|
||||
symbolSize: 8,
|
||||
|
@ -306,28 +275,25 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
if (hasShorts) {
|
||||
// Add short support
|
||||
if (!Array.isArray(chartOptions.value?.legend) && chartOptions.value?.legend?.data) {
|
||||
if (colShortEntryData >= 0) {
|
||||
chartOptions.value.legend.data.push('Short');
|
||||
}
|
||||
if (colShortExitData >= 0) {
|
||||
chartOptions.value.legend.data.push('Short exit');
|
||||
}
|
||||
}
|
||||
if (Array.isArray(options.series)) {
|
||||
if (Array.isArray(options.series)) {
|
||||
if (hasShorts) {
|
||||
if (colShortEntryData >= 0) {
|
||||
options.series.push({
|
||||
name: 'Short',
|
||||
// Short entry
|
||||
name: 'Entry',
|
||||
type: 'scatter',
|
||||
symbol: 'pin',
|
||||
symbol: 'triangle',
|
||||
symbolRotate: 180,
|
||||
symbolSize: 10,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: shortEntrySignalColor,
|
||||
},
|
||||
tooltip: {
|
||||
// Hide tooltip - it's already there for longs.
|
||||
show: false,
|
||||
},
|
||||
encode: {
|
||||
x: colDate,
|
||||
y: colShortEntryData,
|
||||
|
@ -336,15 +302,20 @@ export default defineComponent({
|
|||
}
|
||||
if (colShortExitData >= 0) {
|
||||
options.series.push({
|
||||
name: 'Short exit',
|
||||
// Short exit
|
||||
name: 'Exit',
|
||||
type: 'scatter',
|
||||
symbol: 'pin',
|
||||
symbolSize: 8,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
// Hide tooltip - it's already there for longs.
|
||||
color: shortexitSignalColor,
|
||||
},
|
||||
tooltip: {
|
||||
show: false,
|
||||
},
|
||||
encode: {
|
||||
x: colDate,
|
||||
y: colShortExitData,
|
||||
|
@ -487,41 +458,42 @@ export default defineComponent({
|
|||
chartOptions.value.grid[chartOptions.value.grid.length - 1].bottom = '50px';
|
||||
delete chartOptions.value.grid[chartOptions.value.grid.length - 1].top;
|
||||
}
|
||||
const { trades, tradesClose } = getTradeEntries();
|
||||
const { tradeData } = getTradeEntries(props.dataset, filteredTrades.value);
|
||||
|
||||
const name = 'Trades';
|
||||
const nameClose = 'Trades Close';
|
||||
const nameTrades = 'Trades';
|
||||
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
|
||||
chartOptions.value.legend.data.push(name);
|
||||
chartOptions.value.legend.data.push(nameTrades);
|
||||
}
|
||||
const sp: ScatterSeriesOption = {
|
||||
name,
|
||||
const tradesSeries: ScatterSeriesOption = {
|
||||
name: nameTrades,
|
||||
type: 'scatter',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: tradeBuyColor,
|
||||
encode: {
|
||||
x: 0,
|
||||
y: 1,
|
||||
label: 5,
|
||||
tooltip: 6,
|
||||
},
|
||||
data: trades,
|
||||
};
|
||||
if (Array.isArray(chartOptions.value?.series)) {
|
||||
chartOptions.value.series.push(sp);
|
||||
}
|
||||
if (!Array.isArray(chartOptions.value.legend) && chartOptions.value.legend?.data) {
|
||||
chartOptions.value.legend.data.push(nameClose);
|
||||
}
|
||||
const closeSeries: ScatterSeriesOption = {
|
||||
name: nameClose,
|
||||
type: 'scatter',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
itemStyle: {
|
||||
color: tradeSellColor,
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
backgroundColor: props.theme !== 'dark' ? '#fff' : '#000',
|
||||
padding: 2,
|
||||
color: props.theme === 'dark' ? '#fff' : '#000',
|
||||
},
|
||||
data: tradesClose,
|
||||
labelLayout: { rotate: 75, align: 'left', dx: 10 },
|
||||
itemStyle: {
|
||||
// color: tradeSellColor,
|
||||
color: (v) => v.data[4],
|
||||
},
|
||||
symbol: (v) => v[2],
|
||||
symbolRotate: (v) => v[3],
|
||||
symbolSize: 13,
|
||||
data: tradeData,
|
||||
};
|
||||
if (chartOptions.value.series && Array.isArray(chartOptions.value.series)) {
|
||||
chartOptions.value.series.push(closeSeries);
|
||||
chartOptions.value.series.push(tradesSeries);
|
||||
}
|
||||
|
||||
console.log('chartOptions', chartOptions.value);
|
||||
|
@ -542,12 +514,13 @@ export default defineComponent({
|
|||
animation: false,
|
||||
legend: {
|
||||
// Initial legend, further entries are pushed to the below list
|
||||
data: ['Candles', 'Volume', 'Long'],
|
||||
data: ['Candles', 'Volume', 'Entry', 'Exit'],
|
||||
right: '1%',
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis',
|
||||
renderMode: 'richText',
|
||||
backgroundColor: 'rgba(80,80,80,0.7)',
|
||||
borderWidth: 0,
|
||||
textStyle: {
|
||||
|
@ -715,7 +688,6 @@ export default defineComponent({
|
|||
strategy,
|
||||
pair,
|
||||
timeframe,
|
||||
timeframems,
|
||||
datasetColumns,
|
||||
hasData,
|
||||
filteredTrades,
|
||||
|
|
|
@ -42,20 +42,37 @@
|
|||
<p v-if="botStore.activeBot.profit.first_trade_timestamp">
|
||||
First trade opened:
|
||||
|
||||
<strong
|
||||
><DateTimeTZ :date="botStore.activeBot.profit.first_trade_timestamp" show-timezone
|
||||
/></strong>
|
||||
<strong>
|
||||
<DateTimeTZ :date="botStore.activeBot.profit.first_trade_timestamp" show-timezone />
|
||||
</strong>
|
||||
<br />
|
||||
Last trade opened:
|
||||
<strong
|
||||
><DateTimeTZ :date="botStore.activeBot.profit.latest_trade_timestamp" show-timezone
|
||||
/></strong>
|
||||
<strong>
|
||||
<DateTimeTZ :date="botStore.activeBot.profit.latest_trade_timestamp" show-timezone />
|
||||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span v-if="botStore.activeBot.profit.profit_factor">
|
||||
Profit factor:
|
||||
{{ botStore.activeBot.profit.profit_factor.toFixed(2) }}
|
||||
</span>
|
||||
<br />
|
||||
<span v-if="botStore.activeBot.profit.trading_volume">
|
||||
Trading volume:
|
||||
{{
|
||||
formatPriceCurrency(
|
||||
botStore.activeBot.profit.trading_volume,
|
||||
botStore.activeBot.botState.stake_currency,
|
||||
botStore.activeBot.botState.stake_currency_decimals ?? 3,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { formatPercent } from '@/shared/formatters';
|
||||
import { formatPercent, formatPriceCurrency } from '@/shared/formatters';
|
||||
import DateTimeTZ from '@/components/general/DateTimeTZ.vue';
|
||||
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
|
@ -68,6 +85,7 @@ export default defineComponent({
|
|||
const botStore = useBotStore();
|
||||
return {
|
||||
formatPercent,
|
||||
formatPriceCurrency,
|
||||
botStore,
|
||||
};
|
||||
},
|
||||
|
|
70
src/shared/charts/tradeChartData.ts
Normal file
70
src/shared/charts/tradeChartData.ts
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { formatPercent } from '@/shared/formatters';
|
||||
import { roundTimeframe } from '@/shared/timemath';
|
||||
import { PairHistory, Trade } from '@/types';
|
||||
|
||||
function buildToolTip(trade: Trade, side: string): string {
|
||||
return `${trade.is_short ? 'Short' : 'Long'} ${side} ${formatPercent(
|
||||
trade.profit_ratio,
|
||||
)} \nEnter-tag: ${trade.enter_tag ?? ''} \nExit-Tag: ${trade.exit_reason ?? ''}`;
|
||||
}
|
||||
// const ENTRY_SYMB = 'circle';
|
||||
// const EXIT_SYMB = 'rect';
|
||||
|
||||
const ENTRY_SYMB =
|
||||
'path://m 52.444161,104.1909 8.386653,25.34314 8.386651,25.34313 -16.731501,0.0422 -16.731501,0.0422 8.344848,-25.38539 z m 0.08656,-48.368126 8.386652,25.343139 8.386652,25.343137 -16.731501,0.0422 -16.731502,0.0422 8.344848,-25.385389 z';
|
||||
const EXIT_SYMB =
|
||||
'path://m 102.20764,19.885384 h 24.1454 v 41.928829 h -24.1454 z m 12.17344,36.423813 8.38665,25.343139 8.38666,25.343134 -16.7315,0.0422 -16.731507,0.0422 8.344847,-25.385386 z';
|
||||
|
||||
/** Return trade entries for charting */
|
||||
export function getTradeEntries(dataset: PairHistory, filteredTrades: Trade[]) {
|
||||
const tradeData: (number | string)[][] = [];
|
||||
// Return schema:
|
||||
// 0: Timeframe
|
||||
// 1: rate
|
||||
// 2: symbol
|
||||
// 3: symbol rotate
|
||||
// 4: color
|
||||
// 5: label
|
||||
// 6: tooltip
|
||||
|
||||
for (let i = 0, len = filteredTrades.length; i < len; i += 1) {
|
||||
const trade: Trade = filteredTrades[i];
|
||||
if (
|
||||
trade.open_timestamp >= dataset.data_start_ts &&
|
||||
trade.open_timestamp <= dataset.data_stop_ts
|
||||
) {
|
||||
// Trade entry
|
||||
tradeData.push([
|
||||
roundTimeframe(dataset.timeframe_ms ?? 0, trade.open_timestamp),
|
||||
trade.open_rate,
|
||||
ENTRY_SYMB,
|
||||
trade.is_short ? 180 : 0,
|
||||
// (trade.profit_abs ?? 0) > 0 ? '#31e04b' : '#fc0505',
|
||||
trade.is_short ? '#b21dbf' : '#0099ff',
|
||||
'',
|
||||
// trade.profit_abs,
|
||||
buildToolTip(trade, 'entry'),
|
||||
]);
|
||||
}
|
||||
if (
|
||||
trade.close_timestamp !== undefined &&
|
||||
trade.close_timestamp <= dataset.data_stop_ts &&
|
||||
trade.close_timestamp > dataset.data_start_ts
|
||||
) {
|
||||
if (trade.close_date !== undefined && trade.close_rate !== undefined) {
|
||||
// Trade exit
|
||||
tradeData.push([
|
||||
roundTimeframe(dataset.timeframe_ms ?? 0, trade.close_timestamp),
|
||||
trade.close_rate,
|
||||
EXIT_SYMB,
|
||||
trade.is_short ? 180 : 0,
|
||||
trade.is_short ? '#b21dbf' : '#0099ff',
|
||||
// (trade.profit_abs ?? 0) > 0 ? '#31e04b' : '#fc0505',
|
||||
formatPercent(trade.profit_ratio, 2),
|
||||
buildToolTip(trade, 'exit'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { tradeData };
|
||||
}
|
|
@ -36,4 +36,8 @@ export interface ProfitInterface {
|
|||
best_pair_profit_ratio: number;
|
||||
winning_trades: number;
|
||||
losing_trades: number;
|
||||
profit_factor?: number;
|
||||
max_drawdown?: number;
|
||||
max_drawdown_abs?: number;
|
||||
trading_volume?: number;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import { TradingMode } from './types';
|
||||
|
||||
export interface Order {
|
||||
export interface BTOrder {
|
||||
amount: number;
|
||||
safe_price: number;
|
||||
ft_order_side: string;
|
||||
order_filled_timestamp?: number;
|
||||
ft_is_entry: boolean;
|
||||
}
|
||||
|
||||
export interface Order extends BTOrder {
|
||||
pair: string;
|
||||
order_id: string;
|
||||
status: string;
|
||||
remaining: number;
|
||||
amount: number;
|
||||
safe_price: number;
|
||||
cost: number;
|
||||
filled: number;
|
||||
ft_order_side: string;
|
||||
order_type: string;
|
||||
is_open: boolean;
|
||||
order_timestamp?: number;
|
||||
order_filled_timestamp?: number;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
|
@ -120,6 +124,7 @@ export interface ClosedTrade extends Trade {
|
|||
|
||||
export interface TradeResponse {
|
||||
trades: ClosedTrade[];
|
||||
offset: number;
|
||||
/** Trades count for this response */
|
||||
trades_count: number;
|
||||
/** Total trade count */
|
||||
|
|
Loading…
Reference in New Issue
Block a user