diff --git a/CMakeLists.txt b/CMakeLists.txt index 193ac4e08f..ef9b68da33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,6 +156,7 @@ target_sources(atomic_qt_shared_deps INTERFACE ${CMAKE_SOURCE_DIR}/src/atomic.dex.update.service.cpp ${CMAKE_SOURCE_DIR}/src/atomic.dex.qt.orders.model.cpp ${CMAKE_SOURCE_DIR}/src/atomic.dex.qt.orders.proxy.model.cpp + ${CMAKE_SOURCE_DIR}/src/atomic.dex.qt.candlestick.charts.model.cpp ${CMAKE_SOURCE_DIR}/src/atomic.dex.qt.addressbook.model.cpp ${CMAKE_SOURCE_DIR}/src/atomic.dex.qt.addressbook.proxy.filter.model.cpp ${CMAKE_SOURCE_DIR}/src/atomic.dex.qt.contact.model.cpp diff --git a/atomic_qt_design/qml/Constants/General.qml b/atomic_qt_design/qml/Constants/General.qml index 082cd2ce0f..ac770af3b5 100644 --- a/atomic_qt_design/qml/Constants/General.qml +++ b/atomic_qt_design/qml/Constants/General.qml @@ -43,7 +43,7 @@ QtObject { readonly property double time_toast_important_error: 10000 readonly property double time_toast_basic_info: 3000 - readonly property var chart_times: (["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "3d", "1w"]) + readonly property var chart_times: (["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "3d"/*, "1w"*/]) readonly property var time_seconds: ({ "1m": 60, "3m": 180, "5m": 300, "15m": 900, "30m": 1800, "1h": 3600, "2h": 7200, "4h": 14400, "6h": 21600, "12h": 43200, "1d": 86400, "3d": 259200, "1w": 604800 }) property var all_coins @@ -148,11 +148,15 @@ QtObject { } function fullNamesOfCoins(coins) { - return coins.map(c => fullCoinName(c.name, c.ticker)) + return coins.map(c => { + return { value: c.ticker, text: fullCoinName(c.name, c.ticker) } + }) } function getTickers(coins) { - return coins.map(c => c.ticker) + return coins.map(c => { + return { value: c.ticker, text: c.ticker } + }) } @@ -161,7 +165,9 @@ QtObject { } function getTickersAndBalances(coins) { - return coins.map(c => c.ticker + " (" + c.balance + ")") + return coins.map(c => { + return { value: c.ticker, text: c.ticker + " (" + c.balance + ")" } + }) } function getMinTradeAmount() { diff --git a/atomic_qt_design/qml/Exchange/Orders/Orders.qml b/atomic_qt_design/qml/Exchange/Orders/Orders.qml index 6bed0b0c49..ba2524970c 100644 --- a/atomic_qt_design/qml/Exchange/Orders/Orders.qml +++ b/atomic_qt_design/qml/Exchange/Orders/Orders.qml @@ -37,12 +37,8 @@ Item { API.get().refresh_orders_and_swaps() } - function baseCoins() { - return API.get().enabled_coins - } - function changeTicker(ticker) { - combo_base.currentIndex = baseCoins().map(c => c.ticker).indexOf(ticker) + combo_base.currentIndex = combo_base.model.map(c => c.value).indexOf(ticker) } // Orders page quick refresher, used right after a fresh successful trade @@ -113,9 +109,11 @@ Item { Layout.bottomMargin: 10 Layout.rightMargin: 15 - model: General.fullNamesOfCoins(baseCoins()) + textRole: "text" + + model: General.fullNamesOfCoins(API.get().enabled_coins) onCurrentTextChanged: { - base = baseCoins()[currentIndex].ticker + base = model[currentIndex].value } } diff --git a/atomic_qt_design/qml/Exchange/Trade/CandleStickChart.qml b/atomic_qt_design/qml/Exchange/Trade/CandleStickChart.qml index 7d7f04ebc9..6f1edfc71f 100644 --- a/atomic_qt_design/qml/Exchange/Trade/CandleStickChart.qml +++ b/atomic_qt_design/qml/Exchange/Trade/CandleStickChart.qml @@ -7,509 +7,631 @@ import "../../Components" import "../../Constants" // List -ChartView { - id: chart +Item { + id: root readonly property double y_margin: 0.02 - readonly property bool has_data: series.count > 0 - margins.top: 0 - margins.left: 0 - margins.bottom: 0 - margins.right: 0 + readonly property bool pair_supported: cs_mapper.model.is_current_pair_supported + + function getChartSeconds() { + const idx = combo_time.currentIndex + const timescale = General.chart_times[idx] + return General.time_seconds[timescale] + } Component.onCompleted: { - API.get().OHLCDataUpdated.connect(initChart) + API.get().candlestick_charts_mdl.modelReset.connect(chartUpdated) + API.get().candlestick_charts_mdl.chartFullyModelReset.connect(chartFullyReset) } - AreaSeries { - id: series_area + function chartFullyReset() { + updater.locked_min_max_value = true + update_last_value_y_timer.restart() + chartUpdated() + } - property double global_max: 0 + function chartUpdated() { + const mapper = cs_mapper + const model = mapper.model - color: Style.colorBlue + // Update last value line + const last_idx = series.count - 1 + const last_open = model.data(model.index(last_idx, mapper.openColumn), 0) + const last_close = model.data(model.index(last_idx, mapper.closeColumn), 0) + if(last_close === undefined) return - borderWidth: 0 - opacity: 0.3 + series.last_value = last_close + series.last_value_green = last_close >= last_open - axisX: series.axisX - axisY: ValueAxis { - id: value_axis_area - visible: false - onRangeChanged: { - // This will be always same, small size at bottom - value_axis_area.min = 0 - value_axis_area.max = series_area.global_max * 1/0.5 - } - } - upperSeries: LineSeries { visible: false } + // Get timestamp caps + first_value_timestamp = model.data(model.index(0, mapper.timestampColumn), 0) + last_value_timestamp = model.data(model.index(last_idx, mapper.timestampColumn), 0) + global_min_value = model.global_min_value + global_max_value = model.global_max_value + + // Update other stuff + updater.updateChart(true) } - // Moving Average 1 - LineSeries { - id: series_ma1 + property double first_value_timestamp + property double last_value_timestamp + property double global_min_value + property double global_max_value - readonly property int num: 20 + ChartView { + id: volume_chart - color: Style.colorChartMA1 + visible: chart.visible + anchors.top: chart.bottom + anchors.bottom: parent.bottom + anchors.left: chart.left + anchors.right: chart.right - width: 1 + margins.top: 0 + margins.left: 0 + margins.bottom: 0 + margins.right: 0 - pointsVisible: false + antialiasing: chart.antialiasing + legend.visible: chart.legend.visible + backgroundColor: chart.backgroundColor + plotArea: Qt.rect(chart.plotArea.x, 0, chart.plotArea.width, height) - axisX: series.axisX - axisYRight: series.axisYRight - } + CandlestickSeries { + id: series_area - // Moving Average 2 - LineSeries { - id: series_ma2 + HCandlestickModelMapper { + model: cs_mapper.model - readonly property int num: 50 + timestampColumn: 0 + openColumn: 6 + highColumn: 7 + lowColumn: 8 + closeColumn: 9 - color: Style.colorChartMA2 + firstSetRow: 0 + lastSetRow: model.series_size + } - width: series_ma1.width + increasingColor: Style.colorGreen3 + decreasingColor: Style.colorRed3 + bodyOutlineVisible: false + + property double visible_max: cs_mapper.model.visible_max_volume + onVisible_maxChanged: value_axis_area.updateAxes() + + axisX: DateTimeAxis { + min: cs_mapper.model.series_from + max: cs_mapper.model.series_to + + tickCount: 10 + titleVisible: false + lineVisible: true + labelsFont.family: Style.font_family + labelsFont.weight: Font.Bold + gridLineColor: Style.colorChartGrid + labelsColor: Style.colorChartText + color: Style.colorChartLegendLine + format: "MMM d" + } + axisY: ValueAxis { + id: value_axis_area - pointsVisible: false + function updateAxes() { + // This will be always same, small size at bottom + min = 0 + max = series_area.visible_max + } - axisX: series.axisX - axisYRight: series.axisYRight + visible: false + onRangeChanged: updateAxes() + } + } } - // Price, front - CandlestickSeries { - id: series + ChartView { + id: chart - property double global_max: 0 - property double last_value: 0 - property bool last_value_green: true - property double last_value_y: 0 + visible: pair_supported && series.count > 0 && series.count === cs_mapper.model.series_size && !cs_mapper.model.is_fetching + + height: parent.height * 0.9 + width: parent.width + + margins.top: 0 + margins.left: 0 + margins.bottom: 0 + margins.right: 0 + + antialiasing: true + legend.visible: false + backgroundColor: "transparent" - function updateLastValueY() { - series.last_value_y = chart.mapToPosition(Qt.point(0, series.last_value), series).y - } Timer { id: update_last_value_y_timer - interval: 200 + interval: 50 repeat: false running: false onTriggered: series.updateLastValueY() } - increasingColor: Style.colorGreen - decreasingColor: Style.colorRed - bodyOutlineVisible: false - - axisX: DateTimeAxis { - titleVisible: false - lineVisible: true - labelsFont.family: Style.font_family - labelsFont.weight: Font.Bold - gridLineColor: Style.colorChartGrid - labelsColor: Style.colorChartText - color: Style.colorChartLegendLine - format: "MMM d" - } - axisYRight: ValueAxis { - id: value_axis - titleVisible: series.axisX.titleVisible - lineVisible: series.axisX.lineVisible - labelsFont: series.axisX.labelsFont - gridLineColor: series.axisX.gridLineColor - labelsColor: series.axisX.labelsColor - color: series.axisX.color - - onRangeChanged: { - if(min < 0) value_axis.min = 0 - - const max_val = value_axis.global_max * (1 + y_margin) - if(max > max_val) value_axis.max = max_val + // Moving Average 1 + LineSeries { + id: series_ma1 + + VXYModelMapper { + model: cs_mapper.model + xColumn: 0 + yColumn: 10 } - } - } - function fixTimestamp(t) { - return t * 1000 - } + readonly property int num: 20 - function getChartSeconds() { - const idx = combo_time.currentIndex - const timescale = General.chart_times[idx] - return General.time_seconds[timescale] - } + color: Style.colorChartMA1 - function getHistorical() { - const seconds_str = "" + getChartSeconds() - const data = API.get().get_ohlc_data(seconds_str) - return data - } + width: 1 + + pointsVisible: false - function initChart() { - series.clear() - series_area.upperSeries.clear() - series_ma1.clear() - series_ma2.clear() - - series.global_max = 0 - series.last_value = 0 - series.last_value_y = 0 - series_area.global_max = 0 - - const historical = getHistorical() - console.log("Updating the chart...") - const count = historical.length - if(count === 0) return - - // Prepare the chart - let min_price = Infinity - let max_price = 0 - let min_other = Infinity - let max_other = 0 - - for(let i = 0; i < count; ++i) { - series.append(historical[i].open, historical[i].high, historical[i].low, historical[i].close, fixTimestamp(historical[i].timestamp)) - series_area.upperSeries.append(General.timestampToDate(historical[i].timestamp), historical[i].volume) - - if(series_area.global_max < historical[i].volume) series_area.global_max = historical[i].volume + axisX: series.axisX + axisYRight: series.axisYRight } - const first_idx = Math.floor(count * 0.9) - const last_idx = count - 1 + // Moving Average 2 + LineSeries { + id: series_ma2 - const last_elem = historical[last_idx] - series.last_value = last_elem.close - series.last_value_green = last_elem.close >= last_elem.open + VXYModelMapper { + model: cs_mapper.model + xColumn: 0 + yColumn: 11 + } - // Set min and max values - for(let j = first_idx; j <= last_idx; ++j) { - const price = historical[j].close - const other = historical[j].volume + readonly property int num: 50 - min_price = Math.min(min_price, price) - max_price = Math.max(max_price, price) - min_other = Math.min(min_other, other) - max_other = Math.max(max_other, other) - } + color: Style.colorChartMA2 + width: series_ma1.width - // Date - series.axisX.min = General.timestampToDate(historical[first_idx].timestamp) - series.axisX.max = General.timestampToDate(last_elem.timestamp) - series.axisX.tickCount = 10//count -/* - series2.axisX.min = series.axisX.min - series2.axisX.max = series.axisX.max - series2.axisX.tickCount = series.axisX.tickCount -*/ + pointsVisible: false - // Price - series.axisYRight.min = min_price * (1 - y_margin) - series.axisYRight.max = max_price * (1 + y_margin) + axisX: series.axisX + axisYRight: series.axisYRight + } - // Other - series_area.axisY.min = min_other * (1 - y_margin) - series_area.axisY.max = max_other * (1 + y_margin) + // Price, front + CandlestickSeries { + id: series + HCandlestickModelMapper { + id: cs_mapper + model: API.get().candlestick_charts_mdl - computeMovingAverage() + timestampColumn: 0 + openColumn: 1 + highColumn: 2 + lowColumn: 3 + closeColumn: 4 - update_last_value_y_timer.start() - updater.updateChart() - } + firstSetRow: 0 + lastSetRow: model.series_size + } - width: parent.width - height: parent.height - antialiasing: true + property double global_max: 0 + property double last_value: 0 + property bool last_value_green: true - legend.visible: false + function updateLastValueY() { + const area = chart.plotArea + horizontal_line.y = Math.max(Math.min(chart.mapToPosition(Qt.point(0, series.last_value), series).y, area.y + area.height), area.y) + } - backgroundColor: "transparent" + increasingColor: Style.colorGreen + decreasingColor: Style.colorRed + bodyOutlineVisible: false + + axisX: DateTimeAxis { + id: date_time_axis + min: cs_mapper.model.series_from + max: cs_mapper.model.series_to + + tickCount: 10 + titleVisible: false + lineVisible: true + labelsFont.family: Style.font_family + labelsFont.weight: Font.Bold + gridLineColor: Style.colorChartGrid + labelsColor: Style.colorChartText + color: Style.colorChartLegendLine + format: "MMM d" + } + axisYRight: ValueAxis { + id: value_axis - // Horizontal line - Canvas { - id: horizontal_line - readonly property color color: series.last_value_green ? Style.colorGreen : Style.colorRed - onColorChanged: requestPaint() - anchors.left: parent.left - width: parent.width - height: 1 + min: cs_mapper.model.min_value + max: cs_mapper.model.max_value - onPaint: { - var ctx = getContext("2d"); + titleVisible: series.axisX.titleVisible + lineVisible: series.axisX.lineVisible + labelsFont: series.axisX.labelsFont + gridLineColor: series.axisX.gridLineColor + labelsColor: series.axisX.labelsColor + color: series.axisX.color - ctx.setLineDash([1, 1]); - ctx.lineWidth = 1.5; - ctx.strokeStyle = color + labelFormat: "%llf" - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(width, 0) - ctx.stroke() - } + onRangeChanged: { + // if(min < 0) value_axis.min = 0 - Rectangle { - color: parent.color - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - width: Math.max(value_y_text.width, 30) - height: value_y_text.height - DefaultText { - id: value_y_text - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - text_value: General.formatDouble(series.last_value, General.recommendedPrecision) - font.pixelSize: series.axisYRight.labelsFont.pixelSize - color: Style.colorChartLineText + // const max_val = value_axis.global_max * (1 + y_margin) + // if(max > max_val) value_axis.max = max_val + } } } - } - // Cursor Horizontal line - Canvas { - id: cursor_horizontal_line - readonly property color color: Style.colorBlue - anchors.left: parent.left - width: parent.width - height: 1 + // Horizontal line + Canvas { + id: horizontal_line + readonly property color color: series.last_value_green ? Style.colorGreen : Style.colorRed + onColorChanged: requestPaint() - onPaint: { - var ctx = getContext("2d"); + anchors.left: parent.left + width: parent.width + height: 1 - ctx.setLineDash([1, 1]); - ctx.lineWidth = 1.5; - ctx.strokeStyle = color + onPaint: { + var ctx = getContext("2d"); - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(width, 0) - ctx.stroke() - } + ctx.setLineDash([1, 1]); + ctx.lineWidth = 1.5; + ctx.strokeStyle = color - Rectangle { - color: parent.color - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - width: Math.max(cursor_y_text.width, 30) - height: cursor_y_text.height - DefaultText { - id: cursor_y_text + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(width, 0) + ctx.stroke() + } + + Rectangle { + color: parent.color + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: series.axisYRight.labelsFont.pixelSize + + width: Math.max(value_y_text.width, 30) + height: value_y_text.height + DefaultText { + id: value_y_text + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + text_value: General.formatDouble(series.last_value, General.recommendedPrecision) + font.pixelSize: series.axisYRight.labelsFont.pixelSize + color: Style.colorChartLineText + } } } - } - // Cursor Vertical line - Canvas { - id: cursor_vertical_line - property double x_position: 0 - readonly property color color: Style.colorBlue - anchors.top: parent.top - width: 1 - height: parent.height - - onPaint: { - var ctx = getContext("2d"); - - ctx.setLineDash([1, 1]); - ctx.lineWidth = 1.5; - ctx.strokeStyle = color - - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(0, height) - ctx.stroke() + // Cursor Horizontal line + Rectangle { + id: cursor_horizontal_line + anchors.left: parent.left + width: parent.width + height: 1 + + visible: mouse_area.containsMouse + + color: Style.colorBlue + + Rectangle { + color: parent.color + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + width: Math.max(cursor_y_text.width, 30) + height: cursor_y_text.height + DefaultText { + id: cursor_y_text + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: series.axisYRight.labelsFont.pixelSize + } + } } + // Cursor Vertical line Rectangle { - color: parent.color - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter + id: cursor_vertical_line - width: cursor_x_text.width - height: cursor_x_text.height + anchors.top: parent.top + width: 1 + height: parent.height + volume_chart.height + 6 - DefaultText { - id: cursor_x_text - anchors.verticalCenter: parent.verticalCenter + visible: cursor_horizontal_line.visible + color: cursor_horizontal_line.color + + Rectangle { + color: parent.color + anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - font.pixelSize: series.axisYRight.labelsFont.pixelSize + + width: cursor_x_text.width + height: cursor_x_text.height + + DefaultText { + id: cursor_x_text + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: series.axisYRight.labelsFont.pixelSize + } } } - } - MouseArea { - id: mouse_area - anchors.fill: parent + MouseArea { + id: mouse_area + anchors.fill: parent - onWheel: updater.delta_wheel_y += wheel.angleDelta.y + onWheel: updater.delta_wheel_y += wheel.angleDelta.y - // Drag scroll - hoverEnabled: true - } + // Drag scroll + hoverEnabled: true + } + // Time selection + DefaultComboBox { + id: combo_time + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + anchors.leftMargin: 35 + width: 75 + height: 30 + flat: true + font.pixelSize: Style.textSizeSmall3 + + currentIndex: 5 // 1h + model: General.chart_times + + property bool initialized: false + onCurrentTextChanged: { + if(initialized) cs_mapper.model.current_range = "" + getChartSeconds() + else initialized = true + } + } - function addMovingAverage(historical, serie, sums, i) { - if(i >= serie.num) serie.append(fixTimestamp(historical[i].timestamp), (sums[i] - sums[i - serie.num]) / serie.num) - } + // Cursor values + DefaultText { + id: cursor_values + anchors.left: combo_time.right + anchors.top: combo_time.top + anchors.leftMargin: 10 + color: series.axisX.labelsColor + font.pixelSize: Style.textSizeSmall + } - function computeMovingAverage() { - series_ma1.clear() - series_ma2.clear() + // MA texts + DefaultText { + anchors.left: cursor_values.left + anchors.bottom: combo_time.bottom + font.pixelSize: cursor_values.font.pixelSize + text_value: `MA ${series_ma1.num}    MA ${series_ma2.num}` + } - const historical = getHistorical() - const count = historical.length - let result = [] - let sums = [] - for(let i = 0; i < count; ++i) { - // Accumulate - if(i === 0) sums.push(historical[i].open) - else sums.push(historical[i].open + sums[i - 1]) - // Calculate MA - addMovingAverage(historical, series_ma1, sums, i) - addMovingAverage(historical, series_ma2, sums, i) + // Canvas updater + Timer { + id: update_block_timer + running: false + repeat: false + interval: 1 + onTriggered: updater.can_update = true } - } + Timer { + id: updater + property bool can_update: true + + readonly property double scroll_speed_x: 0.0001 + readonly property double scroll_speed_y: 0.05 + property double delta_wheel_y: 0 + property double click_started_inside_area + property double prev_mouse_pressed + property double prev_mouse_x + property double prev_mouse_y + + interval: 1 + running: mouse_area.containsMouse + repeat: true + onTriggered: updateChart() + + property bool locked_min_max_value: true + readonly property double visible_min_value: cs_mapper.model.visible_min_value + readonly property double visible_max_value: cs_mapper.model.visible_max_value + + onVisible_min_valueChanged: { + if(locked_min_max_value) { + cs_mapper.model.min_value = visible_min_value + } + } + onVisible_max_valueChanged: { + if(locked_min_max_value) { + cs_mapper.model.max_value = visible_max_value + } + } - // Time selection - DefaultComboBox { - id: combo_time - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: 25 - anchors.leftMargin: 35 - width: 75 - height: 30 - flat: true - font.pixelSize: Style.textSizeSmall3 - - currentIndex: 5 // 1h - model: General.chart_times - - property bool initialized: false - onCurrentTextChanged: { - if(initialized) initChart() - else initialized = true - } - } + function capDateStart(timestamp, current_distance) { + return Math.max(timestamp, first_value_timestamp - current_distance*0.9) + } - // Cursor values - DefaultText { - id: cursor_values - anchors.left: combo_time.right - anchors.top: combo_time.top - anchors.leftMargin: 10 - color: series.axisX.labelsColor - font.pixelSize: Style.textSizeSmall - } + function capDateEnd(timestamp, current_distance) { + return Math.min(timestamp, last_value_timestamp + current_distance*0.9) + } - // MA texts - DefaultText { - anchors.left: cursor_values.left - anchors.bottom: combo_time.bottom - font.pixelSize: cursor_values.font.pixelSize - text_value: `MA ${series_ma1.num}    MA ${series_ma2.num}` - } + function capPriceMin(price) { + return Math.max(price, global_min_value) + } + function capPriceMax(price) { + return Math.min(price, global_max_value) + } + function getMinTimeDifference() { + return 20 * getChartSeconds() * 1000 + } + function getMinValueDifference() { + return series.last_value * 0.05 + } - // Canvas updater - Timer { - id: update_block_timer - running: false - repeat: false - interval: 1 - onTriggered: updater.can_update = true - } - Timer { - id: updater - property bool can_update: true - - readonly property double scroll_speed: 0.1 - property double delta_wheel_y: 0 - property double prev_mouse_x - property double prev_mouse_y - - interval: 1 - running: mouse_area.containsMouse - repeat: true - onTriggered: updateChart() - - function updateChart() { - if(!can_update) return - can_update = false - - // Update - const mouse_x = mouse_area.mouseX - const mouse_y = mouse_area.mouseY - const diff_x = mouse_x - prev_mouse_x - const diff_y = mouse_y - prev_mouse_y - prev_mouse_x = mouse_x - prev_mouse_y = mouse_y - - // Update drag - if(mouse_area.containsPress) { - if(diff_x > 0) chart.scrollLeft(diff_x) - else if(diff_x < 0) chart.scrollRight(-diff_x) - if(diff_y > 0) chart.scrollUp(diff_y) - else if(diff_y < 0) chart.scrollDown(-diff_y) - - if(diff_y !== 0) series.updateLastValueY() + function scrollHorizontal(pixels) { + const model = cs_mapper.model + const min = model.series_from.getTime() + const max = model.series_to.getTime() + + const diff = max - min + const scale = pixels / chart.plotArea.width + const amount = diff * scale + + // Cap without zooming, more complex + let new_max = capDateEnd(max - amount, diff) + const new_min = capDateStart(new_max - diff, diff) + new_max = capDateEnd(new_min + diff, diff) + + if(new_max - new_min < getMinTimeDifference()) return + model.series_from = new Date(new_min) + model.series_to = new Date(new_max) } - // Update zoom - const zoomed = delta_wheel_y !== 0 - if (zoomed) { - chart.zoom(1 + (-delta_wheel_y/360) * scroll_speed) - series.updateLastValueY() - delta_wheel_y = 0 + function scrollVertical(pixels) { + if(locked_min_max_value) return + + const model = cs_mapper.model + const min = model.min_value + const max = model.max_value + const scale = pixels / chart.plotArea.height + const amount = (max - min) * scale + + const new_min = capPriceMin(model.min_value + amount) + const new_max = capPriceMax(model.max_value + amount) + if(new_max - new_min < getMinValueDifference()) return + model.min_value = new_min + model.max_value = new_max } - // Update cursor line - if(zoomed || diff_x !== 0 || diff_y !== 0) { - // Map mouse position to value - const cp = chart.mapToValue(Qt.point(mouse_x, mouse_y), series) + function zoomHorizontal(factor) { + const model = cs_mapper.model + const min = model.series_from.getTime() + const max = model.series_to.getTime() - // Find closest real data - const realData = API.get().find_closest_ohlc_data(getChartSeconds(), cp.x / 1000) - const realDataFound = realData.timestamp - if(realDataFound) { - cursor_vertical_line.x = chart.mapToPosition(Qt.point(realData.timestamp*1000, 0), series).x - } + const diff = max - min - // Texts - cursor_x_text.text_value = realDataFound ? General.timestampToDate(realData.timestamp).toString() : "" - cursor_y_text.text_value = General.formatDouble(cp.y, General.recommendedPrecision) - - const highlightColor = realDataFound && realData.close >= realData.open ? Style.colorGreen : Style.colorRed - cursor_values.text_value = realDataFound ? ( - `O:${realData.open}    ` + - `H:${realData.high}    ` + - `L:${realData.low}    ` + - `C:${realData.close}    ` + - `Vol:${realData.volume.toFixed(0)}K` - ) : `` - - // Positions - horizontal_line.y = series.last_value_y - cursor_horizontal_line.y = mouse_y + const new_min = capDateStart(min * (1 - factor), diff) + const new_max = capDateEnd(max * (1 + 0.2*factor), diff) + if(new_max - new_min < getMinTimeDifference()) return + model.series_from = new Date(new_min) + model.series_to = new Date(new_max) } - // Block this function for a while to allow engine to render - update_block_timer.start() - } - } -} + function zoomVertical(factor) { + locked_min_max_value = false + + const model = cs_mapper.model + + const new_min = capPriceMin(model.min_value * (1 - factor)) + const new_max = capPriceMax(model.max_value * (1 + factor)) + if(new_max - new_min < getMinValueDifference()) return + model.min_value = new_min + model.max_value = new_max + } + + function updateChart(force) { + if(!can_update && !force) return + can_update = false + + // Update + const mouse_x = mouse_area.mouseX + const mouse_y = mouse_area.mouseY + const diff_x = mouse_x - prev_mouse_x + const diff_y = mouse_y - prev_mouse_y + prev_mouse_x = mouse_x + prev_mouse_y = mouse_y + + const area = chart.plotArea + const inside_plot_area = mouse_x < area.x + area.width + const curr_mouse_pressed = mouse_area.containsPress + const clicked = !prev_mouse_pressed && curr_mouse_pressed + prev_mouse_pressed = curr_mouse_pressed + if(clicked) { + click_started_inside_area = inside_plot_area + } + + // Update drag + if(curr_mouse_pressed) { + if(click_started_inside_area && diff_x !== 0) { + scrollHorizontal(diff_x) + } + + if(diff_y !== 0) { + if(click_started_inside_area) { + scrollVertical(diff_y) + } + else { + zoomVertical((diff_y/area.height) * scroll_speed_y) + } + } + } + + // Update zoom + const zoomed = delta_wheel_y !== 0 + if (zoomed) { + if(inside_plot_area) zoomHorizontal((-delta_wheel_y/360) * scroll_speed_x) + else zoomVertical((-delta_wheel_y/360) * scroll_speed_y) + + delta_wheel_y = 0 + } + + // Update cursor line + if(curr_mouse_pressed || zoomed || diff_x !== 0 || diff_y !== 0) { + // Map mouse position to value + const cp = chart.mapToValue(Qt.point(mouse_x, mouse_y), series) + + // Find closest real data + const realData = API.get().find_closest_ohlc_data(getChartSeconds(), cp.x / 1000) + const realDataFound = realData.timestamp + if(realDataFound) { + cursor_vertical_line.x = chart.mapToPosition(Qt.point(realData.timestamp*1000, 0), series).x + } + + // Texts + cursor_x_text.text_value = realDataFound ? General.timestampToDate(realData.timestamp).toString() : "" + cursor_y_text.text_value = General.formatDouble(cp.y, General.recommendedPrecision) + + const highlightColor = realDataFound && realData.close >= realData.open ? Style.colorGreen : Style.colorRed + cursor_values.text_value = realDataFound ? ( + `O:${realData.open}    ` + + `H:${realData.high}    ` + + `L:${realData.low}    ` + + `C:${realData.close}    ` + + `Vol:${realData.volume.toFixed(0)}K` + ) : `` + + // Positions + cursor_horizontal_line.y = mouse_y + } + series.updateLastValueY() + + // Block this function for a while to allow engine to render + update_block_timer.start() + } + } + } + + DefaultBusyIndicator { + visible: !chart.visible + anchors.centerIn: parent + } +} /*##^## diff --git a/atomic_qt_design/qml/Exchange/Trade/OrderForm.qml b/atomic_qt_design/qml/Exchange/Trade/OrderForm.qml index 428f62c8ba..7ef564cfce 100644 --- a/atomic_qt_design/qml/Exchange/Trade/OrderForm.qml +++ b/atomic_qt_design/qml/Exchange/Trade/OrderForm.qml @@ -34,6 +34,7 @@ FloatingBackground { recursive_update = new_ticker !== undefined ticker_list = my_side ? General.getTickersAndBalances(getFilteredCoins()) : General.getTickers(getFilteredCoins()) + update_timer.running = true } @@ -112,26 +113,7 @@ FloatingBackground { } function getTicker() { - if(combo.currentIndex === -1) return '' - const coins = getFilteredCoins() - - const coin = coins[combo.currentIndex] - - // If invalid index - if(coin === undefined) { - // If there are other coins, select first - if(coins.length > 0) { - combo.currentIndex = 0 - return coins[combo.currentIndex].ticker - } - // If there isn't any, reset index - else { - combo.currentIndex = -1 - return '' - } - } - - return coin.ticker + return ticker_list.length > 0 ? ticker_list[combo.currentIndex].value : "" } function setTicker(ticker) { @@ -248,14 +230,13 @@ FloatingBackground { model: ticker_list + + textRole: "text" + onCurrentTextChanged: { if(!recursive_update) { - resetTradeInfo() - - setPair(my_side) - if(my_side) prev_base = getTicker() - else prev_rel = getTicker() updateForms(my_side, combo.currentText) + setPair(my_side) } } diff --git a/atomic_qt_design/qml/Exchange/Trade/Trade.qml b/atomic_qt_design/qml/Exchange/Trade/Trade.qml index d2a9c79eae..20c875dc1b 100644 --- a/atomic_qt_design/qml/Exchange/Trade/Trade.qml +++ b/atomic_qt_design/qml/Exchange/Trade/Trade.qml @@ -10,8 +10,6 @@ Item { id: exchange_trade property string action_result - property string prev_base - property string prev_rel // Override property var onOrderSuccess: () => {} @@ -24,8 +22,6 @@ Item { function fullReset() { reset(true) - prev_base = '' - prev_rel = '' orderbook_timer.running = false } @@ -212,6 +208,7 @@ Item { updateOrderbook() reset(true) updateForms() + setPair(true) } function updateForms(my_side, new_ticker) { @@ -278,26 +275,6 @@ Item { else form_rel.setTicker(ticker) } - function swapPair() { - let base = getTicker(true) - let rel = getTicker(false) - - // Fill previous ones if they are blank - if(prev_base === '') prev_base = form_base.getAnyAvailableCoin(rel) - if(prev_rel === '') prev_rel = form_rel.getAnyAvailableCoin(base) - - // Get different value if they are same - if(base === rel) { - if(base !== prev_base) base = prev_base - else if(rel !== prev_rel) rel = prev_rel - } - - // Swap - const curr_base = base - setTicker(true, rel) - setTicker(false, curr_base) - } - function validBaseRel() { const base = getTicker(true) const rel = getTicker(false) @@ -305,19 +282,21 @@ Item { } function setPair(is_base) { - if(getTicker(true) === getTicker(false)) swapPair() - else { - if(validBaseRel()) { - const new_base = getTicker(true) - const rel = getTicker(false) - console.log("Setting current orderbook with params: ", new_base, rel) - API.get().current_coin_info.ticker = new_base - API.get().set_current_orderbook(new_base, rel) - reset(true, is_base) - updateOrderbook() - updateCexPrice(new_base, rel) - exchange.onTradeTickerChanged(new_base) - } + if(getTicker(true) === getTicker(false)) { + // Base got selected, same as rel + // Change rel ticker + form_rel.setAnyTicker() + } + + if(validBaseRel()) { + const new_base = getTicker(true) + const rel = getTicker(false) + console.log("Setting current orderbook with params: ", new_base, rel) + API.get().set_current_orderbook(new_base, rel) + reset(true, is_base) + updateOrderbook() + updateCexPrice(new_base, rel) + exchange.onTradeTickerChanged(new_base) } } @@ -402,7 +381,6 @@ Item { visible: form_base.ticker_list.length > 0 -// anchors.centerIn: parent anchors.fill: parent @@ -411,7 +389,7 @@ Item { Layout.alignment: Qt.AlignTop - visible: chart.has_data + visible: chart.pair_supported Layout.fillWidth: true Layout.fillHeight: true diff --git a/atomic_qt_design/qml/Settings/Settings.qml b/atomic_qt_design/qml/Settings/Settings.qml index b8e803f6ce..0fd5cb8469 100644 --- a/atomic_qt_design/qml/Settings/Settings.qml +++ b/atomic_qt_design/qml/Settings/Settings.qml @@ -52,7 +52,7 @@ Item { } } Component.onCompleted: { - field.currentIndex = fiats.indexOf(API.get().current_fiat) + field.currentIndex = field.model.indexOf(API.get().current_fiat) initialized = true } } diff --git a/atomic_qt_design/qml/main.qml b/atomic_qt_design/qml/main.qml index 048ddab5e1..94d0e1be8d 100644 --- a/atomic_qt_design/qml/main.qml +++ b/atomic_qt_design/qml/main.qml @@ -12,6 +12,9 @@ Window { minimumHeight: General.minimumHeight title: API.get().empty_string + (qsTr("AtomicDEX Pro")) flags: Qt.Window | Qt.WindowFullscreenButtonHint + + Component.onCompleted: showMaximized() + onVisibilityChanged: API.get().change_state(visibility) App { anchors.fill: parent diff --git a/main.cpp b/main.cpp index e47c71f47b..9764ec4fdb 100644 --- a/main.cpp +++ b/main.cpp @@ -28,6 +28,14 @@ inline constexpr size_t g_spdlog_max_file_size = 7777777; inline constexpr size_t g_spdlog_max_file_rotation = 3; +void +signal_handler(int signal) +{ + spdlog::trace("sigabort received, cleaning mm2"); + atomic_dex::kill_executable("mm2"); + std::exit(signal); +} + #if defined(WINDOWS_RELEASE_MAIN) INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT) @@ -37,6 +45,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) #endif { + std::signal(SIGABRT, signal_handler); //! Project #if defined(_WIN32) || defined(WIN32) using namespace std::string_literals; @@ -72,7 +81,7 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) spdlog::set_pattern("[%H:%M:%S %z] [%L] [thr %t] %v"); spdlog::info("Logger successfully initialized"); - //spdlog::info("asan report: {}", (fs::temp_directory_path() / "asan.log").string().c_str()); + // spdlog::info("asan report: {}", (fs::temp_directory_path() / "asan.log").string().c_str()); //__sanitizer_set_report_path((fs::temp_directory_path() / "asan.log").string().c_str()); //! App declaration diff --git a/src/atomic.dex.app.cpp b/src/atomic.dex.app.cpp index 8139364d94..fc9bd61f40 100644 --- a/src/atomic.dex.app.cpp +++ b/src/atomic.dex.app.cpp @@ -234,7 +234,7 @@ namespace atomic_dex } } - if (not this->m_actions_queue.empty()) + if (not this->m_actions_queue.empty() && not this->m_about_to_exit_app) { action last_action; this->m_actions_queue.pop(last_action); @@ -255,7 +255,15 @@ namespace atomic_dex case action::refresh_ohlc: if (mm2.is_mm2_running()) { - emit OHLCDataUpdated(); + // emit OHLCDataUpdated(); + if (this->m_candlestick_need_a_reset) + { + this->m_candlestick_chart_ohlc->init_data(); + } + else + { + this->m_candlestick_chart_ohlc->update_data(); + } } break; case action::refresh_transactions: @@ -396,7 +404,8 @@ namespace atomic_dex m_update_status(QJsonObject{ {"update_needed", false}, {"changelog", ""}, {"current_version", ""}, {"download_url", ""}, {"new_version", ""}, {"rpc_code", 0}, {"status", ""}}), m_coin_info(new current_coin_info(dispatcher_, this)), m_addressbook(new addressbook_model(this->m_wallet_manager, this)), - m_portfolio(new portfolio_model(this->system_manager_, this->m_config, this)), m_orders(new orders_model(this->system_manager_, this)) + m_portfolio(new portfolio_model(this->system_manager_, this->m_config, this)), m_orders(new orders_model(this->system_manager_, this)), + m_candlestick_chart_ohlc(new candlestick_charts_model(this->system_manager_, this)) { get_dispatcher().sink().connect<&application::on_refresh_update_status_event>(*this); //! MM2 system need to be created before the GUI and give the instance to the gui @@ -450,14 +459,20 @@ namespace atomic_dex atomic_dex::application::on_enabled_coins_event([[maybe_unused]] const enabled_coins_event& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_enabled_coin); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_enabled_coin); + } } void application::on_enabled_default_coins_event([[maybe_unused]] const enabled_default_coins_event& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_enabled_coin); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_enabled_coin); + } } void @@ -507,7 +522,10 @@ namespace atomic_dex application::on_change_ticker_event([[maybe_unused]] const change_ticker_event& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_current_ticker); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_current_ticker); + } } void @@ -586,7 +604,10 @@ namespace atomic_dex atomic_dex::t_broadcast_request req{.tx_hex = tx_hex.toStdString(), .coin = m_coin_info->get_ticker().toStdString()}; std::error_code ec; auto answer = get_mm2().broadcast(std::move(req), ec); - this->m_actions_queue.push(action::refresh_current_ticker); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_current_ticker); + } refresh_infos(); return QString::fromStdString(answer.tx_hash); } @@ -597,7 +618,10 @@ namespace atomic_dex atomic_dex::t_broadcast_request req{.tx_hex = tx_hex.toStdString(), .coin = m_coin_info->get_ticker().toStdString()}; std::error_code ec; auto answer = get_mm2().send_rewards(std::move(req), ec); - this->m_actions_queue.push(action::refresh_current_ticker); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_current_ticker); + } refresh_infos(); return QString::fromStdString(answer.tx_hash); } @@ -606,7 +630,10 @@ namespace atomic_dex application::on_tx_fetch_finished_event([[maybe_unused]] const tx_fetch_finished& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_transactions); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_transactions); + } } bool @@ -671,8 +698,10 @@ namespace atomic_dex application::on_coin_disabled_event([[maybe_unused]] const coin_disabled& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_enabled_coin); - // m_refresh_enabled_coin_event = true; + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_enabled_coin); + } } QString @@ -725,6 +754,9 @@ namespace atomic_dex void application::set_current_orderbook(const QString& base, const QString& rel) { + auto& provider = this->system_manager_.get_system(); + auto [normal, quoted] = provider.is_pair_supported(base.toStdString(), rel.toStdString()); + this->m_candlestick_chart_ohlc->set_is_pair_supported(normal || quoted); this->dispatcher_.trigger(base.toStdString(), rel.toStdString()); } @@ -761,7 +793,10 @@ namespace atomic_dex application::on_refresh_update_status_event([[maybe_unused]] const refresh_update_status& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_update_status); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_update_status); + } } void @@ -819,6 +854,7 @@ namespace atomic_dex this->m_orders->removeRows(0, count, QModelIndex()); } this->m_orders->clear_registry(); + this->m_candlestick_chart_ohlc->clear_data(); //! Mark systems system_manager_.mark_system(); @@ -838,6 +874,7 @@ namespace atomic_dex get_dispatcher().sink().disconnect<&application::on_refresh_ohlc_event>(*this); get_dispatcher().sink().disconnect<&application::on_process_orders_finished_event>(*this); get_dispatcher().sink().disconnect<&application::on_process_swaps_finished_event>(*this); + get_dispatcher().sink().disconnect<&application::on_start_fetching_new_ohlc_data_event>(*this); this->m_need_a_full_refresh_of_mm2 = true; @@ -860,6 +897,7 @@ namespace atomic_dex get_dispatcher().sink().connect<&application::on_refresh_ohlc_event>(*this); get_dispatcher().sink().connect<&application::on_process_orders_finished_event>(*this); get_dispatcher().sink().connect<&application::on_process_swaps_finished_event>(*this); + get_dispatcher().sink().connect<&application::on_start_fetching_new_ohlc_data_event>(*this); } QString @@ -962,6 +1000,7 @@ namespace atomic_dex application::set_qt_app(std::shared_ptr app) noexcept { this->m_app = app; + connect(m_app.get(), SIGNAL(aboutToQuit()), this, SLOT(exit_handler())); set_current_lang(QString::fromStdString(m_config.current_lang)); } @@ -1204,6 +1243,12 @@ namespace atomic_dex //! OHLC Relative functions namespace atomic_dex { + candlestick_charts_model* + application::get_candlestick_charts() const noexcept + { + return m_candlestick_chart_ohlc; + } + QVariantList application::get_ohlc_data(const QString& range) { @@ -1248,14 +1293,21 @@ namespace atomic_dex application::on_refresh_ohlc_event([[maybe_unused]] const refresh_ohlc_needed& evt) noexcept { spdlog::debug("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_ohlc); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_ohlc); + this->m_candlestick_need_a_reset = evt.is_a_reset; + } } void application::on_ticker_balance_updated_event(const ticker_balance_updated& evt) noexcept { spdlog::trace("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::refresh_portfolio_ticker_balance); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::refresh_portfolio_ticker_balance); + } *this->m_ticker_balance_to_refresh = evt.ticker; } } // namespace atomic_dex @@ -1277,14 +1329,20 @@ namespace atomic_dex application::on_process_swaps_finished_event([[maybe_unused]] const process_swaps_finished& evt) noexcept { spdlog::trace("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::post_process_swaps_finished); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::post_process_swaps_finished); + } } void application::on_process_orders_finished_event([[maybe_unused]] const process_orders_finished& evt) noexcept { spdlog::trace("{} l{}", __FUNCTION__, __LINE__); - this->m_actions_queue.push(action::post_process_orders_finished); + if (not m_about_to_exit_app) + { + this->m_actions_queue.push(action::post_process_orders_finished); + } } orders_model* @@ -1419,4 +1477,17 @@ namespace atomic_dex m_coin_info->set_trend_7d(nlohmann_json_array_to_qt_json_array(paprika.get_ticker_historical(ticker).answer)); } } + + void + application::exit_handler() + { + spdlog::trace("will quit app, prevent all threading event"); + this->m_about_to_exit_app = true; + } + + void + application::on_start_fetching_new_ohlc_data_event(const start_fetching_new_ohlc_data& evt) + { + this->m_candlestick_chart_ohlc->set_is_currently_fetching(evt.is_a_reset); + } } // namespace atomic_dex \ No newline at end of file diff --git a/src/atomic.dex.app.hpp b/src/atomic.dex.app.hpp index cfd2440538..b722c4bc52 100644 --- a/src/atomic.dex.app.hpp +++ b/src/atomic.dex.app.hpp @@ -34,6 +34,7 @@ #include "atomic.dex.provider.coinpaprika.hpp" #include "atomic.dex.qt.addressbook.model.hpp" #include "atomic.dex.qt.bindings.hpp" +#include "atomic.dex.qt.candlestick.charts.model.hpp" #include "atomic.dex.qt.current.coin.infos.hpp" #include "atomic.dex.qt.orders.model.hpp" #include "atomic.dex.qt.portfolio.model.hpp" @@ -56,6 +57,7 @@ namespace atomic_dex Q_PROPERTY(QObject* current_coin_info READ get_current_coin_info NOTIFY coinInfoChanged) Q_PROPERTY(addressbook_model* addressbook_mdl READ get_addressbook NOTIFY addressbookChanged) Q_PROPERTY(orders_model* orders_mdl READ get_orders NOTIFY ordersChanged) + Q_PROPERTY(candlestick_charts_model* candlestick_charts_mdl READ get_candlestick_charts NOTIFY candlestickChartsChanged) Q_PROPERTY(QVariant update_status READ get_update_status NOTIFY updateStatusChanged) Q_PROPERTY(portfolio_model* portfolio_mdl READ get_portfolio NOTIFY portfolioChanged) Q_PROPERTY(QString current_currency READ get_current_currency WRITE set_current_currency NOTIFY on_currency_changed) @@ -107,17 +109,20 @@ namespace atomic_dex void on_refresh_update_status_event(const refresh_update_status&) noexcept; void on_process_orders_finished_event(const process_orders_finished&) noexcept; void on_process_swaps_finished_event(const process_swaps_finished&) noexcept; + void on_start_fetching_new_ohlc_data_event(const start_fetching_new_ohlc_data&); //! Properties Getter - static const QString& get_empty_string(); - mm2& get_mm2() noexcept; - const mm2& get_mm2() const noexcept; - coinpaprika_provider& get_paprika() noexcept; - entt::dispatcher& get_dispatcher() noexcept; - QObject* get_current_coin_info() const noexcept; - addressbook_model* get_addressbook() const noexcept; - portfolio_model* get_portfolio() const noexcept; - orders_model* get_orders() const noexcept; + static const QString& get_empty_string(); + mm2& get_mm2() noexcept; + const mm2& get_mm2() const noexcept; + coinpaprika_provider& get_paprika() noexcept; + entt::dispatcher& get_dispatcher() noexcept; + QObject* get_current_coin_info() const noexcept; + addressbook_model* get_addressbook() const noexcept; + portfolio_model* get_portfolio() const noexcept; + orders_model* get_orders() const noexcept; + candlestick_charts_model* get_candlestick_charts() const noexcept; + ; QVariantList get_enabled_coins() const noexcept; QVariantList get_enableable_coins() const noexcept; QString get_current_currency() const noexcept; @@ -201,12 +206,14 @@ namespace atomic_dex Q_INVOKABLE bool do_i_have_enough_funds(const QString& ticker, const QString& amount) const; Q_INVOKABLE bool disable_coins(const QStringList& coins); Q_INVOKABLE bool is_claiming_ready(const QString& ticker); - Q_INVOKABLE QObject* claim_rewards(const QString& ticker); - Q_INVOKABLE QVariantList get_ohlc_data(const QString& range); - Q_INVOKABLE QVariantMap find_closest_ohlc_data(int range, int timestamp); - Q_INVOKABLE QString get_cex_rates(const QString& base, const QString& rel); - Q_INVOKABLE QString get_fiat_from_amount(const QString& ticker, const QString& amount); + Q_INVOKABLE QObject* claim_rewards(const QString& ticker); + + Q_INVOKABLE QString get_cex_rates(const QString& base, const QString& rel); + Q_INVOKABLE QString get_fiat_from_amount(const QString& ticker, const QString& amount); + + Q_INVOKABLE QVariantList get_ohlc_data(const QString& range); + Q_INVOKABLE QVariantMap find_closest_ohlc_data(int range, int timestamp); Q_INVOKABLE bool is_supported_ohlc_data_ticker_pair(const QString& base, const QString& rel); Q_INVOKABLE QVariant get_coin_info(const QString& ticker); Q_INVOKABLE bool export_swaps(const QString& csv_filename) noexcept; @@ -235,6 +242,10 @@ namespace atomic_dex void portfolioChanged(); void updateStatusChanged(); void ordersChanged(); + void candlestickChartsChanged(); + public slots: + void exit_handler(); + ; private: void process_refresh_enabled_coin_action(); @@ -274,5 +285,11 @@ namespace atomic_dex //! Orders model based on the current wallet orders_model* m_orders; + + //! Candlestick charts + candlestick_charts_model* m_candlestick_chart_ohlc; + std::atomic_bool m_candlestick_need_a_reset{false}; + + std::atomic_bool m_about_to_exit_app{false}; }; } // namespace atomic_dex diff --git a/src/atomic.dex.events.hpp b/src/atomic.dex.events.hpp index 7b05177fd9..0c97854b69 100644 --- a/src/atomic.dex.events.hpp +++ b/src/atomic.dex.events.hpp @@ -29,12 +29,20 @@ namespace atomic_dex using enabled_default_coins_event = entt::tag<"gui_enabled_default_coins"_hs>; using change_ticker_event = entt::tag<"gui_change_ticker"_hs>; using tx_fetch_finished = entt::tag<"gui_tx_fetch_finished"_hs>; - //using refresh_order_needed = entt::tag<"gui_refresh_order_needed"_hs>; - using refresh_ohlc_needed = entt::tag<"gui_refresh_ohlc_needed"_hs>; using refresh_update_status = entt::tag<"gui_refresh_update_status"_hs>; using process_orders_finished = entt::tag<"gui_process_orders_finished"_hs>; using process_swaps_finished = entt::tag<"gui_process_swaps_finished"_hs>; + struct refresh_ohlc_needed + { + bool is_a_reset; + }; + + struct start_fetching_new_ohlc_data + { + bool is_a_reset; + }; + struct ticker_balance_updated { std::string ticker; diff --git a/src/atomic.dex.kill.hpp b/src/atomic.dex.kill.hpp index 15613202e2..02dfe031d5 100644 --- a/src/atomic.dex.kill.hpp +++ b/src/atomic.dex.kill.hpp @@ -16,6 +16,7 @@ #pragma once -namespace atomic_dex { +namespace atomic_dex +{ void kill_executable(const char* exec_name); } \ No newline at end of file diff --git a/src/atomic.dex.ma.series.data.hpp b/src/atomic.dex.ma.series.data.hpp new file mode 100644 index 0000000000..f056436b9d --- /dev/null +++ b/src/atomic.dex.ma.series.data.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace atomic_dex +{ + struct ma_series_data + { + std::size_t m_timestamp; + double m_average; + }; +} // namespace atomic_dex \ No newline at end of file diff --git a/src/atomic.dex.mm2.cpp b/src/atomic.dex.mm2.cpp index b7d56819ac..161f614c25 100644 --- a/src/atomic.dex.mm2.cpp +++ b/src/atomic.dex.mm2.cpp @@ -138,7 +138,7 @@ namespace atomic_dex dispatcher_.sink().connect<&mm2::on_gui_leave_trading>(*this); dispatcher_.sink().connect<&mm2::on_refresh_orderbook>(*this); - m_swaps_registry.insert("result", t_my_recent_swaps_answer{.total = 0}); + m_swaps_registry.insert("result", t_my_recent_swaps_answer{.limit = 0, .total = 0}); } void @@ -734,7 +734,8 @@ namespace atomic_dex void mm2::process_swaps() { - t_my_recent_swaps_request request{.limit = 50}; + std::size_t total = this->m_swaps_registry.at("result").total; + t_my_recent_swaps_request request{.limit = total > 0 ? total : 50}; auto answer = rpc_my_recent_swaps(std::move(request)); if (answer.result.has_value()) { @@ -840,15 +841,15 @@ namespace atomic_dex { tx_infos current_info{ - .am_i_sender = current.my_balance_change[0] == '-', - .confirmations = current.confirmations.has_value() ? current.confirmations.value() : 0, - .from = current.from, - .to = current.to, - .date = current.timestamp_as_date, - .timestamp = current.timestamp, - .tx_hash = current.tx_hash, - .fees = current.fee_details.normal_fees.has_value() ? current.fee_details.normal_fees.value().amount - : current.fee_details.erc_fees.value().total_fee, + .am_i_sender = current.my_balance_change[0] == '-', + .confirmations = current.confirmations.has_value() ? current.confirmations.value() : 0, + .from = current.from, + .to = current.to, + .date = current.timestamp_as_date, + .timestamp = current.timestamp, + .tx_hash = current.tx_hash, + .fees = current.fee_details.normal_fees.has_value() ? current.fee_details.normal_fees.value().amount + : current.fee_details.erc_fees.value().total_fee, .my_balance_change = current.my_balance_change, .total_amount = current.total_amount, .block_height = current.block_height, diff --git a/src/atomic.dex.provider.cex.prices.cpp b/src/atomic.dex.provider.cex.prices.cpp index ef46f9f344..d7dc642034 100644 --- a/src/atomic.dex.provider.cex.prices.cpp +++ b/src/atomic.dex.provider.cex.prices.cpp @@ -57,12 +57,20 @@ namespace atomic_dex { spdlog::debug("{} l{} f[{}]", __FUNCTION__, __LINE__, fs::path(__FILE__).filename().string()); - m_current_ohlc_data = nlohmann::json::array(); - this->dispatcher_.trigger(); + if (auto [normal, quoted] = is_pair_supported(evt.base, evt.rel); !normal && !quoted) + { + m_current_ohlc_data->clear(); + m_current_orderbook_ticker_pair.first = ""; + m_current_orderbook_ticker_pair.second = ""; + this->dispatcher_.trigger(); + return; + } + + m_current_ohlc_data = nlohmann::json::array(); m_current_orderbook_ticker_pair = {boost::algorithm::to_lower_copy(evt.base), boost::algorithm::to_lower_copy(evt.rel)}; auto [base, rel] = m_current_orderbook_ticker_pair; spdlog::debug("new orderbook pair for cex provider [{} / {}]", base, rel); - m_pending_tasks.push(spawn([base = base, rel = rel, this]() { process_ohlc(base, rel); })); + m_pending_tasks.push(spawn([base = base, rel = rel, this]() { process_ohlc(base, rel, true); })); } void @@ -75,7 +83,8 @@ namespace atomic_dex spdlog::info("cex prices provider thread started"); using namespace std::chrono_literals; - do { + do + { spdlog::info("fetching ohlc value"); auto [base, rel] = m_current_orderbook_ticker_pair; if (not base.empty() && not rel.empty() && m_mm2_instance.is_orderbook_thread_active()) @@ -91,12 +100,13 @@ namespace atomic_dex } bool - cex_prices_provider::process_ohlc(const std::string& base, const std::string& rel) noexcept + cex_prices_provider::process_ohlc(const std::string& base, const std::string& rel, bool is_a_reset) noexcept { spdlog::debug("{} l{} f[{}]", __FUNCTION__, __LINE__, fs::path(__FILE__).filename().string()); if (auto [normal, quoted] = is_pair_supported(base, rel); normal || quoted) { spdlog::info("{} / {} is supported, processing", base, rel); + this->dispatcher_.trigger(is_a_reset); atomic_dex::ohlc_request req{base, rel}; if (quoted) { @@ -108,12 +118,8 @@ namespace atomic_dex if (answer.result.has_value()) { m_current_ohlc_data = answer.result.value().raw_result; - if (quoted) - { - //! It's quoted need to reverse all the value - this->reverse_ohlc_data(); - } - this->dispatcher_.trigger(); + this->updating_quote_and_average(quoted); + this->dispatcher_.trigger(is_a_reset); return true; } spdlog::error("http error: {}", answer.error.value_or("dummy")); @@ -170,17 +176,69 @@ namespace atomic_dex } void - cex_prices_provider::reverse_ohlc_data() noexcept + cex_prices_provider::reverse_ohlc_data(nlohmann::json& cur_range) noexcept + { + cur_range["open"] = 1 / cur_range.at("open").get(); + cur_range["high"] = 1 / cur_range.at("high").get(); + cur_range["low"] = 1 / cur_range.at("low").get(); + cur_range["close"] = 1 / cur_range.at("close").get(); + auto volume = cur_range.at("volume").get(); + cur_range["volume"] = cur_range["quote_volume"]; + cur_range["quote_volume"] = volume; + } + + nlohmann::json + cex_prices_provider::get_all_ohlc_data() noexcept + { + return *m_current_ohlc_data; + } + + void + cex_prices_provider::updating_quote_and_average(bool is_quoted) { - nlohmann::json& values = *this->m_current_ohlc_data; - for (auto&& item: values) { - for (auto&& cur_range : item) { - cur_range["open"] = 1 / cur_range.at("open").get(); - cur_range["high"] = 1 / cur_range.at("high").get(); - cur_range["low"] = 1 / cur_range.at("low").get(); - cur_range["close"] = 1 / cur_range.at("close").get(); + nlohmann::json ohlc_data = *this->m_current_ohlc_data; + auto add_moving_average_functor = [](nlohmann::json& current_item, std::size_t idx, const std::vector& sums, std::size_t num) { + int real_num = num; + int first_idx = static_cast(idx) - real_num; + if (first_idx < 0) + { + first_idx = 0; + num = idx; + } + + if (num == 0) + { + current_item["ma_" + std::to_string(real_num)] = current_item.at("open").get(); + } + else + { + current_item["ma_" + std::to_string(real_num)] = static_cast(sums.at(idx) - sums.at(first_idx)) / num; + } + }; + + for (auto&& [key, value]: ohlc_data.items()) + { + std::size_t idx = 0; + std::vector sums; + for (auto&& cur_range: value) + { + if (is_quoted) + { + this->reverse_ohlc_data(cur_range); + } + if (idx == 0) + { + sums.emplace_back(cur_range.at("open").get()); + } + else + { + sums.emplace_back(cur_range.at("open").get() + sums[idx - 1]); + } + add_moving_average_functor(cur_range, idx, sums, 20); + add_moving_average_functor(cur_range, idx, sums, 50); + ++idx; } } + this->m_current_ohlc_data = ohlc_data; } - } // namespace atomic_dex \ No newline at end of file diff --git a/src/atomic.dex.provider.cex.prices.hpp b/src/atomic.dex.provider.cex.prices.hpp index 19a071a6df..e1ab7d814c 100644 --- a/src/atomic.dex.provider.cex.prices.hpp +++ b/src/atomic.dex.provider.cex.prices.hpp @@ -19,12 +19,19 @@ #include "atomic.dex.pch.hpp" //! Project header +#include "atomic.dex.ma.series.data.hpp" #include "atomic.dex.mm2.hpp" inline constexpr const std::size_t nb_pair_supported = 40_sz; namespace atomic_dex { + enum class moving_average + { + twenty, + fifty + }; + namespace ag = antara::gaming; class cex_prices_provider final : public ag::ecs::pre_update_system @@ -37,7 +44,7 @@ namespace atomic_dex mm2& m_mm2_instance; //! OHLC Related - t_current_orderbook_ticker_pair m_current_orderbook_ticker_pair; + t_current_orderbook_ticker_pair m_current_orderbook_ticker_pair{"", ""}; t_supported_pairs m_supported_pair{"eth-btc", "eth-usdc", "btc-usdc", "btc-busd", "btc-tusd", "bat-btc", "bat-eth", "bat-usdc", "bat-tusd", "bat-busd", "bch-btc", "bch-eth", "bch-usdc", "bch-tusd", "bch-busd", "dash-btc", "dash-eth", "dgb-btc", "doge-btc", "kmd-btc", "kmd-eth", "ltc-btc", "ltc-eth", "ltc-usdc", @@ -45,7 +52,7 @@ namespace atomic_dex "rvn-btc", "xzc-btc", "xzc-eth", "zec-btc", "zec-eth", "zec-usdc", "zec-tusd", "zec-busd"}; //! OHLC Data - t_synchronized_json m_current_ohlc_data; + t_synchronized_json m_current_ohlc_data; //! Threads std::queue> m_pending_tasks; @@ -53,7 +60,7 @@ namespace atomic_dex timed_waiter m_provider_thread_timer; //! Private API - void reverse_ohlc_data() noexcept; + void reverse_ohlc_data(nlohmann::json& cur_range) noexcept; public: //! Constructor @@ -69,7 +76,7 @@ namespace atomic_dex void update() noexcept final; //! Process OHLC http rest request - bool process_ohlc(const std::string& base, const std::string& rel) noexcept; + bool process_ohlc(const std::string& base, const std::string& rel, bool is_a_reset = false) noexcept; //! Return true if json ohlc data is not empty, otherwise return false bool is_ohlc_data_available() const noexcept; @@ -82,8 +89,11 @@ namespace atomic_dex nlohmann::json get_ohlc_data(const std::string& range) noexcept; + nlohmann::json get_all_ohlc_data() noexcept; + //! Event that occur when the ticker pair is changed in the front end void on_current_orderbook_ticker_pair_changed(const orderbook_refresh& evt) noexcept; + void updating_quote_and_average(bool quoted); }; } // namespace atomic_dex diff --git a/src/atomic.dex.qt.candlestick.charts.model.cpp b/src/atomic.dex.qt.candlestick.charts.model.cpp new file mode 100644 index 0000000000..7d61b111fc --- /dev/null +++ b/src/atomic.dex.qt.candlestick.charts.model.cpp @@ -0,0 +1,486 @@ +/****************************************************************************** + * Copyright © 2013-2019 The Komodo Platform Developers. * + * * + * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at * + * the top-level directory of this distribution for the individual copyright * + * holder information and the developer policies on copyright and licensing. * + * * + * Unless otherwise agreed in a custom licensing agreement, no part of the * + * Komodo Platform software, including this file may be copied, modified, * + * propagated or distributed except according to the terms contained in the * + * LICENSE file * + * * + * Removal or modification of this copyright notice is prohibited. * + * * + ******************************************************************************/ + + +#include + +//! Project Headers +#include "atomic.dex.provider.cex.prices.hpp" +#include "atomic.dex.qt.candlestick.charts.model.hpp" +#include "atomic.threadpool.hpp" + +namespace atomic_dex +{ + candlestick_charts_model::candlestick_charts_model(ag::ecs::system_manager& system_manager, QObject* parent) : + QAbstractTableModel(parent), m_system_manager(system_manager) + { + spdlog::trace("{} l{} f[{}]", __FUNCTION__, __LINE__, fs::path(__FILE__).filename().string()); + spdlog::trace("candlestick charts model created"); + } + + candlestick_charts_model::~candlestick_charts_model() noexcept + { + spdlog::trace("{} l{} f[{}]", __FUNCTION__, __LINE__, fs::path(__FILE__).filename().string()); + spdlog::trace("candlestick charts model destroyed"); + } + + int + candlestick_charts_model::rowCount([[maybe_unused]] const QModelIndex& parent) const + { + if (m_model_data.empty()) + { + return 0; + } + return m_model_data.size(); + } + + int + candlestick_charts_model::columnCount([[maybe_unused]] const QModelIndex& parent) const + { + return 12; + } + + QVariant + candlestick_charts_model::data([[maybe_unused]] const QModelIndex& index, [[maybe_unused]] int role) const + { + Q_UNUSED(role) + + if (!index.isValid()) + { + return QVariant(); + } + + if (index.row() >= rowCount() || index.row() < 0) + { + return QVariant(); + } + + switch (index.column()) + { + case 0: + return m_model_data.at(index.row()).at("timestamp").get() * 1000ull; + case 1: + return m_model_data.at(index.row()).at("open").get(); + case 2: + return m_model_data.at(index.row()).at("high").get(); + case 3: + return m_model_data.at(index.row()).at("low").get(); + case 4: + return m_model_data.at(index.row()).at("close").get(); + case 5: + return m_model_data.at(index.row()).at("volume").get(); + + // Volume Candlestick chart + case 6: // Open + return m_model_data.at(index.row()).at("close").get() >= m_model_data.at(index.row()).at("open").get() + ? 0 + : m_model_data.at(index.row()).at("volume").get(); + case 7: // High + return m_model_data.at(index.row()).at("volume").get(); + case 8: // Low + return 0; + case 9: // Close + return m_model_data.at(index.row()).at("close").get() >= m_model_data.at(index.row()).at("open").get() + ? m_model_data.at(index.row()).at("volume").get() + : 0; + + //! MA 20 + case 10: + return m_model_data.at(index.row()).contains("ma_20") ? m_model_data.at(index.row()).at("ma_20").get() + : m_model_data.at(index.row()).at("open").get(); + //! MA 50 + case 11: + return m_model_data.at(index.row()).contains("ma_50") ? m_model_data.at(index.row()).at("ma_50").get() + : m_model_data.at(index.row()).at("open").get(); + default: + return QVariant(); + } + } + + bool + candlestick_charts_model::common_reset_data() + { + auto& provider = this->m_system_manager.get_system(); + if (not provider.is_ohlc_data_available()) + { + this->clear_data(); + return false; + } + + this->beginResetModel(); + this->m_model_data = provider.get_ohlc_data(m_current_range); + this->endResetModel(); + this->set_is_currently_fetching(false); + + return true; + } + + void + candlestick_charts_model::init_data() + { + if (not common_reset_data()) + { + return; + } + emit chartFullyModelReset(); + + assert(not m_model_data.empty()); + double max_value = std::numeric_limits::min(); + double min_value = std::numeric_limits::max(); + + for (auto&& cur: m_model_data) + { + if (auto min_to_compare = cur.at("low").get(); min_value > min_to_compare) + { + min_value = min_to_compare; + } + if (auto max_to_compare = cur.at("high").get(); max_value < max_to_compare) + { + max_value = max_to_compare; + } + } + spdlog::trace("new range value IS: min: {} / max: {}", min_value, max_value); + this->set_global_min_value(min_value); + this->set_global_max_value(max_value); + + auto date_start = m_model_data[int(this->m_model_data.size() * 0.9)].at("timestamp").get(); + auto date_end = m_model_data.back().at("timestamp").get(); + auto date_diff = date_end - date_start; + auto date_init_margin = date_diff * 0.1; + date_start += date_init_margin; + date_end += date_init_margin; + QDateTime from, to; + from.setSecsSinceEpoch(date_start); + to.setSecsSinceEpoch(date_end); + this->set_series_from(from); + this->set_series_to(to); + this->set_min_value(m_visible_min_value); + this->set_max_value(m_visible_max_value); + + emit seriesSizeChanged(get_series_size()); + } + + void + candlestick_charts_model::update_data() + { + /*auto& provider = this->m_system_manager.get_system(); + nlohmann::json ohlc_data = provider.get_ohlc_data(m_current_range); + if (ohlc_data.back().at("timestamp").get() != m_model_data.back().at("timestamp").get()) + { + this->beginInsertRows(QModelIndex(), this->m_model_data.size(), this->m_model_data.size()); + m_model_data.push_back(ohlc_data.back()); + this->endInsertRows(); + emit seriesSizeChanged(get_series_size()); + }*/ + + if (not common_reset_data()) + { + return; + } + + emit seriesSizeChanged(get_series_size()); + } + + int + candlestick_charts_model::get_series_size() const noexcept + { + return rowCount(); + } + + void + candlestick_charts_model::clear_data() + { + //! If it's already empty dont reset the model + if (this->m_model_data.empty()) + { + spdlog::trace("already empty, skipping"); + return; + } + + spdlog::trace("clearing the chart candlestick model"); + beginResetModel(); + this->m_model_data.clear(); + this->set_min_value(0); + this->set_max_value(0); + endResetModel(); + emit seriesFromChanged(get_series_from()); + emit seriesToChanged(get_series_to()); + emit seriesSizeChanged(get_series_size()); + } + + QString + candlestick_charts_model::get_current_range() const noexcept + { + return QString::fromStdString(m_current_range); + } + + void + candlestick_charts_model::set_current_range(const QString& range) noexcept + { + this->m_current_range = range.toStdString(); + init_data(); + emit rangeChanged(); + } + + QDateTime + atomic_dex::candlestick_charts_model::get_series_to() const noexcept + { + if (this->m_model_data.empty()) + { + return QDateTime(); + } + return m_series_to; + } + + QDateTime + atomic_dex::candlestick_charts_model::get_series_from() const noexcept + { + if (this->m_model_data.empty()) + { + return QDateTime(); + } + return m_series_from; + } + + double + candlestick_charts_model::get_min_value() const noexcept + { + return m_min_value; + } + + double + candlestick_charts_model::get_max_value() const noexcept + { + return m_max_value; + } + + double + candlestick_charts_model::get_global_min_value() const noexcept + { + return m_global_min_value; + } + + double + candlestick_charts_model::get_global_max_value() const noexcept + { + return m_global_max_value; + } + + void + candlestick_charts_model::set_global_max_value(double value) + { + if (qFuzzyCompare(m_global_max_value, value)) + { + return; + } + + m_global_max_value = value; + emit globalMaxValueChanged(m_global_max_value); + } + + void + candlestick_charts_model::set_global_min_value(double value) + { + if (qFuzzyCompare(m_global_min_value, value)) + { + return; + } + + m_global_min_value = value; + emit globalMinValueChanged(m_global_min_value); + } + + void + candlestick_charts_model::set_max_value(double value) + { + if (qFuzzyCompare(m_max_value, value)) + { + return; + } + + m_max_value = value; + emit maxValueChanged(m_max_value); + } + + void + candlestick_charts_model::set_min_value(double value) + { + if (qFuzzyCompare(m_min_value, value)) + { + return; + } + + m_min_value = value; + emit minValueChanged(m_min_value); + } + + void + candlestick_charts_model::set_series_from(QDateTime value) + { + m_series_from = std::move(value); + emit seriesFromChanged(m_series_from); + } + + void + candlestick_charts_model::set_series_to(QDateTime value) + { + m_series_to = std::move(value); + emit seriesToChanged(m_series_to); + this->update_visible_range(); + } + + void + candlestick_charts_model::update_visible_range() + { + auto from_timestamp = get_series_from().toSecsSinceEpoch(); + auto first_timestamp = m_model_data[0].at("timestamp").get(); + if (from_timestamp < first_timestamp) + { + from_timestamp = first_timestamp; + } + + auto to_timestamp = get_series_to().toSecsSinceEpoch(); + auto last_timestamp = m_model_data[m_model_data.size() - 1].at("timestamp").get(); + if (to_timestamp > last_timestamp) + { + to_timestamp = last_timestamp; + } + + auto from_it = std::lower_bound(begin(m_model_data), end(m_model_data), from_timestamp, [](const nlohmann::json& current_json, int timestamp) { + int res = current_json.at("timestamp").get(); + return res < timestamp; + }); + + auto to_it = std::lower_bound(begin(m_model_data), end(m_model_data), to_timestamp, [](const nlohmann::json& current_json, int timestamp) { + int res = current_json.at("timestamp").get(); + return res < timestamp; + }); + + if (from_it != m_model_data.end() && to_it != m_model_data.end()) + { + auto min_value_j = std::min_element(from_it, to_it, [](nlohmann::json& left, nlohmann::json& right) { + auto left_value = left.at("low").get(); + auto right_value = right.at("low").get(); + return left_value < right_value; + }); + + auto max_value_j = std::max_element(from_it, to_it, [](nlohmann::json& left, nlohmann::json& right) { + auto left_value = left.at("high").get(); + auto right_value = right.at("high").get(); + return left_value < right_value; + }); + + auto max_volume_j = std::max_element(from_it, to_it, [](nlohmann::json& left, nlohmann::json& right) { + auto left_value = left.at("volume").get(); + auto right_value = right.at("volume").get(); + return left_value < right_value; + }); + + auto min_value = min_value_j->at("low").get(); + auto max_value = max_value_j->at("high").get(); + auto max_volume = max_volume_j->at("volume").get(); + this->set_visible_min_value(min_value); + this->set_visible_max_value(max_value); + this->set_visible_max_volume(max_volume); + } + } + + double + candlestick_charts_model::get_visible_max_volume() const noexcept + { + return m_visible_max_volume; + } + + double + candlestick_charts_model::get_visible_max_value() const noexcept + { + return m_visible_max_value; + } + + double + candlestick_charts_model::get_visible_min_value() const noexcept + { + return m_visible_min_value; + } + + void + candlestick_charts_model::set_visible_max_volume(double value) + { + if (qFuzzyCompare(m_visible_max_volume, value)) + { + return; + } + + m_visible_max_volume = value; + emit visibleMaxVolumeChanged(m_visible_max_volume); + } + + void + candlestick_charts_model::set_visible_max_value(double value) + { + if (qFuzzyCompare(m_visible_max_value, value)) + { + return; + } + + m_visible_max_value = value; + emit visibleMaxValueChanged(m_visible_max_value); + } + + void + candlestick_charts_model::set_visible_min_value(double value) + { + if (qFuzzyCompare(m_visible_min_value, value)) + { + return; + } + + m_visible_min_value = value; + emit visibleMinValueChanged(m_visible_min_value); + } + + bool + candlestick_charts_model::is_pair_supported() const noexcept + { + return m_current_pair_supported; + } + + void + candlestick_charts_model::set_is_pair_supported(bool is_support) + { + if (is_support != m_current_pair_supported) + { + m_current_pair_supported = is_support; + emit pairSupportedChanged(m_current_pair_supported); + } + } + + bool + candlestick_charts_model::is_currently_fetching() const noexcept + { + return m_currently_fetching; + } + + void + candlestick_charts_model::set_is_currently_fetching(bool is_fetching) + { + if (is_fetching != m_currently_fetching) + { + this->m_currently_fetching = is_fetching; + emit fetchingStatusChanged(m_currently_fetching); + } + } +} // namespace atomic_dex \ No newline at end of file diff --git a/src/atomic.dex.qt.candlestick.charts.model.hpp b/src/atomic.dex.qt.candlestick.charts.model.hpp new file mode 100644 index 0000000000..ae229ada69 --- /dev/null +++ b/src/atomic.dex.qt.candlestick.charts.model.hpp @@ -0,0 +1,125 @@ +/****************************************************************************** + * Copyright © 2013-2019 The Komodo Platform Developers. * + * * + * See the AUTHORS, DEVELOPER-AGREEMENT and LICENSE files at * + * the top-level directory of this distribution for the individual copyright * + * holder information and the developer policies on copyright and licensing. * + * * + * Unless otherwise agreed in a custom licensing agreement, no part of the * + * Komodo Platform software, including this file may be copied, modified, * + * propagated or distributed except according to the terms contained in the * + * LICENSE file * + * * + * Removal or modification of this copyright notice is prohibited. * + * * + ******************************************************************************/ + +#pragma once + +#include +#include + +//! PCH +#include "atomic.dex.pch.hpp" + +namespace atomic_dex +{ + class candlestick_charts_model final : public QAbstractTableModel + { + Q_OBJECT + Q_PROPERTY(int series_size READ get_series_size NOTIFY seriesSizeChanged) + Q_PROPERTY(QString current_range READ get_current_range WRITE set_current_range NOTIFY rangeChanged) + Q_PROPERTY(QDateTime series_from READ get_series_from WRITE set_series_from NOTIFY seriesFromChanged) + Q_PROPERTY(QDateTime series_to READ get_series_to WRITE set_series_to NOTIFY seriesToChanged) + Q_PROPERTY(double min_value READ get_min_value WRITE set_min_value NOTIFY minValueChanged) + Q_PROPERTY(double max_value READ get_max_value WRITE set_max_value NOTIFY maxValueChanged) + Q_PROPERTY(double global_max_value READ get_global_max_value NOTIFY globalMaxValueChanged) + Q_PROPERTY(double global_min_value READ get_global_min_value NOTIFY globalMinValueChanged) + Q_PROPERTY(double visible_min_value READ get_visible_min_value WRITE set_visible_min_value NOTIFY visibleMinValueChanged) + Q_PROPERTY(double visible_max_value READ get_visible_max_value WRITE set_visible_max_value NOTIFY visibleMaxValueChanged) + Q_PROPERTY(double visible_max_volume READ get_visible_max_volume WRITE set_visible_max_volume NOTIFY visibleMaxVolumeChanged) + Q_PROPERTY(bool is_current_pair_supported READ is_pair_supported WRITE set_is_pair_supported NOTIFY pairSupportedChanged) + Q_PROPERTY(bool is_fetching READ is_currently_fetching WRITE set_is_currently_fetching NOTIFY fetchingStatusChanged) + + public: + candlestick_charts_model(ag::ecs::system_manager& system_manager, QObject* parent = nullptr); + ~candlestick_charts_model() noexcept final; + + [[nodiscard]] int rowCount(const QModelIndex& parent = QModelIndex()) const final; + [[nodiscard]] int columnCount(const QModelIndex& parent) const final; + [[nodiscard]] QVariant data(const QModelIndex& index, int role) const final; + + //! Public API + void init_data(); + void update_data(); + void clear_data(); + + //! Property + [[nodiscard]] bool is_pair_supported() const noexcept; + void set_is_pair_supported(bool is_support); + [[nodiscard]] bool is_currently_fetching() const noexcept;; + void set_is_currently_fetching(bool is_fetching);; + [[nodiscard]] int get_series_size() const noexcept; + [[nodiscard]] QDateTime get_series_from() const noexcept; + [[nodiscard]] QDateTime get_series_to() const noexcept; + [[nodiscard]] double get_min_value() const noexcept; + [[nodiscard]] double get_max_value() const noexcept; + [[nodiscard]] double get_visible_min_value() const noexcept; + [[nodiscard]] double get_visible_max_value() const noexcept; + [[nodiscard]] double get_visible_max_volume() const noexcept; + [[nodiscard]] double get_global_min_value() const noexcept; + [[nodiscard]] double get_global_max_value() const noexcept; + [[nodiscard]] QString get_current_range() const noexcept; + void set_current_range(const QString& range) noexcept; + void set_min_value(double value); + void set_max_value(double value); + void set_visible_min_value(double value); + void set_visible_max_value(double value); + void set_visible_max_volume(double value); + void set_series_from(QDateTime value); + void set_series_to(QDateTime value); + + signals: + void seriesSizeChanged(int value); + void seriesFromChanged(QDateTime date); + void seriesToChanged(QDateTime date); + void minValueChanged(double value); + void maxValueChanged(double value); + void visibleMinValueChanged(double value); + void visibleMaxValueChanged(double value); + void visibleMaxVolumeChanged(double value); + void globalMinValueChanged(double value); + void globalMaxValueChanged(double value); + void pairSupportedChanged(bool supported); + void fetchingStatusChanged(bool fetching_status); + void rangeChanged(); + void maTwentySeriesChanged(); + void maFiftySeriesChanged(); + void chartFullyModelReset(); + + private: + void set_global_min_value(double value); + void set_global_max_value(double value); + void update_visible_range(); + + bool common_reset_data(); + + ag::ecs::system_manager& m_system_manager; + + nlohmann::json m_model_data; + + std::string m_current_range{"3600"}; //! 1h + + bool m_current_pair_supported{false}; + bool m_currently_fetching{false}; + double m_visible_min_value{0}; + double m_visible_max_value{0}; + double m_visible_max_volume{0}; + double m_max_value{0}; + double m_min_value{0}; + double m_global_max_value{0}; + double m_global_min_value{0}; + QDateTime m_series_from; + QDateTime m_series_to; + }; +} // namespace atomic_dex \ No newline at end of file diff --git a/src/atomic.dex.qt.orders.data.hpp b/src/atomic.dex.qt.orders.data.hpp index 5df96728dc..c6fb3d5c3a 100644 --- a/src/atomic.dex.qt.orders.data.hpp +++ b/src/atomic.dex.qt.orders.data.hpp @@ -28,7 +28,7 @@ namespace atomic_dex QString human_date; //! eg: 1595406178 - int unix_timestamp; + unsigned long long unix_timestamp; //! eg: b741646a-5738-4012-b5b0-dcd1375affd1 QString order_id; diff --git a/src/atomic.dex.qt.orders.model.cpp b/src/atomic.dex.qt.orders.model.cpp index 3713d16cec..d6ae49bcba 100644 --- a/src/atomic.dex.qt.orders.model.cpp +++ b/src/atomic.dex.qt.orders.model.cpp @@ -111,7 +111,7 @@ namespace atomic_dex item.human_date = value.toString(); break; case UnixTimestampRole: - item.unix_timestamp = value.toInt(); + item.unix_timestamp = value.toULongLong(); break; case OrderIdRole: item.order_id = value.toString(); @@ -293,7 +293,7 @@ namespace atomic_dex .rel_amount = is_maker ? QString::fromStdString(contents.taker_amount) : QString::fromStdString(contents.maker_amount), .order_type = is_maker ? "maker" : "taker", .human_date = not contents.events.empty() ? QString::fromStdString(contents.events.back().at("human_timestamp").get()) : "", - .unix_timestamp = not contents.events.empty() ? contents.events.back().at("timestamp").get() : 0, + .unix_timestamp = not contents.events.empty() ? contents.events.back().at("timestamp").get() : 0, .order_id = QString::fromStdString(contents.uuid), .order_status = determine_order_status_from_last_event(contents), .maker_payment_id = determine_payment_id(contents, is_maker, false), @@ -322,7 +322,8 @@ namespace atomic_dex bool is_maker = boost::algorithm::to_lower_copy(contents.type) == "maker"; update_value(OrdersRoles::IsRecoverableRole, contents.funds_recoverable, idx, *this); update_value(OrdersRoles::OrderStatusRole, determine_order_status_from_last_event(contents), idx, *this); - update_value(OrdersRoles::UnixTimestampRole, not contents.events.empty() ? contents.events.back().at("timestamp").get() : 0, idx, *this); + update_value( + OrdersRoles::UnixTimestampRole, not contents.events.empty() ? contents.events.back().at("timestamp").get() : 0, idx, *this); update_value( OrdersRoles::HumanDateRole, not contents.events.empty() ? QString::fromStdString(contents.events.back().at("human_timestamp").get()) : "", idx, *this); @@ -348,7 +349,7 @@ namespace atomic_dex .rel_amount = QString::fromStdString(contents.rel_amount), .order_type = QString::fromStdString(contents.order_type), .human_date = QString::fromStdString(contents.human_timestamp), - .unix_timestamp = static_cast(contents.timestamp), + .unix_timestamp = static_cast(contents.timestamp), .order_id = QString::fromStdString(contents.order_id), .order_status = "matching", .is_swap = false, diff --git a/src/atomic.dex.qt.orders.proxy.model.cpp b/src/atomic.dex.qt.orders.proxy.model.cpp index 9980d5056d..ad1b765713 100644 --- a/src/atomic.dex.qt.orders.proxy.model.cpp +++ b/src/atomic.dex.qt.orders.proxy.model.cpp @@ -60,7 +60,7 @@ namespace atomic_dex case orders_model::HumanDateRole: break; case orders_model::UnixTimestampRole: - return left_data.toInt() < right_data.toInt(); + return left_data.toULongLong() < right_data.toULongLong(); case orders_model::OrderIdRole: break; case orders_model::OrderStatusRole: