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: