Skip to main content

Studies Provider

The Studies Provider is responsible for managing studies (indicators) data for the chart. It handles the creation, updating, and removal of studies (e.g., technical indicators) based on the provided settings and candle data. This provider is essential for dynamically calculating and displaying indicators such as moving averages, Bollinger Bands, MACD, etc.

/// A protocol defining the methods required to manage studies (indicators) for a chart.
///
/// A `StudiesProvider` is responsible for creating, updating, and removing studies (e.g., technical indicators)
/// based on the provided settings and candle data. It is typically used in conjunction with a chart to dynamically
/// calculate and display indicators such as moving averages, Bollinger Bands, MACD, etc.
public protocol StudiesProvider {
/// Creates studies (indicators) based on the provided settings and candle data.
///
/// This method calculates the studies (e.g., technical indicators) using the provided `indicators` settings
/// and `candles` data. The calculated studies are returned via the `completion` handler.
///
/// - Parameters:
/// - indicators: An array of `Indicator` objects defining the studies to be calculated.
/// - candles: An array of `Candle` objects representing the historical data used for calculations.
/// - schedule: An optional `Trading.Schedule` object representing the trading schedule. This can be used
/// to adjust calculations based on market hours.
/// - completion: A closure that is called with the calculated studies as an array of `StudyData` objects.
func makeStudies(
indicators: [Indicator],
candles: [Candle],
schedule: Trading.Schedule?,
completion: @escaping ([StudyData]) -> Void
)
/// Updates existing studies with new candle data or settings.
///
/// This method recalculates the studies based on the provided `indicators` settings and optionally
/// new `candles` data. The updated studies are returned via the `completion` handler.
///
/// - Parameters:
/// - indicators: An array of `Indicator` objects defining the studies to be updated.
/// - newCandles: An optional array of `Candle` objects representing new or updated historical data.
/// - schedule: An optional `Trading.Schedule` object representing the trading schedule.
/// - completion: A closure that is called with the updated studies as an array of `StudyData` objects.
func updateStudy(
indicators: [Indicator],
newCandles: [Candle]?,
schedule: Trading.Schedule?,
completion: @escaping ([StudyData]) -> Void
)
/// Notifies the provider that studies (indicators) are no longer needed and can be cleaned up.
///
/// This method informs the provider that the studies are no longer required, allowing it to release
/// any associated resources or stop ongoing calculations. If specific indicators are provided,
/// only those studies will be removed; otherwise, all studies will be cleaned up.
///
/// - Parameters:
/// - indicators: An optional array of `Indicator` objects specifying which studies to remove.
/// If `nil`, all studies will be removed.
/// - completion: An optional closure that is called when the provider has finished cleaning up.
func removeStudies(
for indicators: [Indicator]?,
completion: (() -> Void)?
)
}

Default Provider

If you haven’t implemented a custom data provider and are using the DXStudies framework, you can use the default provider included in the framework. This provider is designed to work seamlessly with the DXStudies framework and handles the calculation and management of studies (indicators) for your chart.

import DXChart
import os
#if !targetEnvironment(simulator)
import dxstudies
final class DXStudiesProvider: DXChart.StudiesProvider {
private var lastCandleTimeStamp: Int64 = 0
private var candles: [DXChart.Candle] = []
private var dxStudies: DxStudies<StudiesCandle>?
private var calculatorsData: [CalculatorsData] = []
private var lastCandleIndex = 0
private var isFinished = false
func makeStudies(
indicators: [DXChart.StudiesSetting],
candles: [DXChart.Candle],
schedule: Trading.Schedule?,
completion: @escaping ([StudyData]) -> Void
) {
self.isFinished = false
self.candles = candles
self.calculatorsData.removeAll()
dxStudies = createCalculator(candles: candles)
calculatorsData = indicators.compactMap { indicator -> CalculatorsData? in
guard let parameters = indicator.parameters else {
return nil
}
let studiesParams = self.createKotlinStudyParamArray(from: parameters)
guard let studyData = self.createStudy(
id: indicator.id,
studiesParams: studiesParams,
candles: candles,
schedule: schedule
) else {
return nil
}
return CalculatorsData(id: indicator.id, study: studyData)
}
let studyData = self.calculateStudyData(self.calculatorsData)
let swiftStudyData = self.convertToSwiftData(kotlinData: studyData)
if !candles.isEmpty {
lastCandleTimeStamp = candles.last!.timestamp
lastCandleIndex = candles.count - 1
}
isFinished = true
completion(swiftStudyData)
}
func updateStudy(
indicators: [StudiesSetting],
newCandles: [Candle]?,
schedule: Trading.Schedule?,
completion: @escaping ([StudyData]) -> Void
) {
guard
isFinished,
!indicators.isEmpty,
candles.count > lastCandleIndex,
let newCandle = newCandles?.last
else { return }
let indexIfExist = candles.firstIndex { $0.timestamp == newCandle.timestamp }
guard indexIfExist == nil || indexIfExist == lastCandleIndex else { return }
let isSameCandle = lastCandleTimeStamp == newCandle.timestamp
if isSameCandle {
candles[lastCandleIndex] = newCandle
} else {
candles.append(newCandle)
}
dxStudies?.addCandleItems(items: KotlinArray(size: 1, init: { int in
StudiesCandle(candle: newCandle)
}))
let newLastIndex = self.lastCandleIndex + (isSameCandle ? 0 : 1)
lastCandleTimeStamp = newCandle.timestamp
calculatorsData.forEach { calcData in
var calcStudyData: [KotlinDoubleArray?] = []
var updatedStudyData = [StudyDataKotlin]()
for i in self.lastCandleIndex...newLastIndex {
calcStudyData.append(calcData.study.calculateAt(index: Int32(i)))
}
let kotlinArray = KotlinArray(size: Int32(calcStudyData.count)) { int in
calcStudyData[int.intValue]
}
updatedStudyData.append(StudyDataKotlin(array: kotlinArray, id: calcData.id))
let result = convertToSwiftData(kotlinData: updatedStudyData)
completion(result)
}
lastCandleIndex = newLastIndex
}
func removeStudies(for indicators: [DXChart.Indicator]?, completion: (() -> Void)?) {
candles.removeAll()
calculatorsData.removeAll()
dxStudies = nil
}
}
private extension DXStudiesProvider {
private func createCalculator(candles: [Candle]) -> DxStudies<StudiesCandle> {
let array = KotlinArray<StudiesCandle>(size: Int32(candles.count)) { int in
let index = Int(truncating: int)
let studiesCandle = StudiesCandle(candle: candles[index])
return studiesCandle
}
return DxStudies(maxElements: Int32.max, candles: array)
}
private func createStudy(
id: String,
studiesParams: KotlinArray<StudyParam>,
candles: [Candle]? = nil,
schedule: Trading.Schedule?
) -> Study? {
if id == "VWAP", let dataSession = schedule {
let sessions = dataSession.days.compactMap(.sessions).flatMap { $0 }
let session = KotlinArray(size: Int32(sessions.count)) { int in
let highlights = sessions[Int(truncating: int)]
return TradingSessionClass(
sessionType: highlights.type.rawValue,
from: Double(highlights.startTime.millisecondsSince1970),
to: Double(highlights.endTime.millisecondsSince1970)
) as TradingSessionData
}
dxStudies?.setTradingSessions(sessions: session)
}
let calcStudy = dxStudies!.createStudy(id: id, params: studiesParams)
return calcStudy
}
private func createKotlinStudyParamArray(from parameters: [StudiesSetting.Parameter]) -> KotlinArray<StudyParam> {
let studiesParams = parameters.map { item in
let value = getTypedValue(param: item)
let studyParameter = StudyParam(key: item.id, value: value!)
return studyParameter
}
let arrayParams = KotlinArray(size: Int32(studiesParams.count), init: { int in
studiesParams[Int(truncating: int)]
})
return arrayParams
}
private func calculateStudyData(_ studiesCalculator: [CalculatorsData]) -> [StudyDataKotlin] {
return studiesCalculator.map { element in
let calcStudyData = element.study.calculateAll()
return StudyDataKotlin(array: calcStudyData, id: element.id)
}
}
private func convertToSwiftData(kotlinData: [StudyDataKotlin]) -> [StudyData] {
return kotlinData.compactMap { data in
var swiftStudyData: [[Double]] = []
guard let dataArray = data.array else { return nil }
for i in 0..<dataArray.size {
let kotlinDoubleArray = dataArray.get(index: i)
var swiftDoubleArray: [Double] = []
for j in 0..<kotlinDoubleArray!.size {
let kotlinDouble = kotlinDoubleArray!.get(index: j)
let swiftDouble = Double(kotlinDouble)
swiftDoubleArray.append(swiftDouble)
}
swiftStudyData.append(swiftDoubleArray)
}
return StudyData(id: data.id, array: swiftStudyData)
}
}
private func getTypedValue(param: StudiesSetting.Parameter) -> Any? {
let paramType = param.studyParamType
let paramValue = param.value
switch paramType {
case .intRange:
var value: KotlinInt
do {
value = try KotlinInt(int: Int32(paramValue, format: .number))
} catch {
value = 0
}
return value
case .doubleRange:
return Double(paramValue)
case .stringType:
return paramValue
case .boolean:
return Bool(paramValue.lowercased())
default:
return paramValue
}
}
}
private class StudiesCandle: CandleDataItem {
var isVisible: Bool = true
var open: Double
var close: Double
var high: Double
var low: Double
var time: Double
var volume: Double
/// Volume-weighted average price
var vwap: KotlinDouble?
var impVolatility: KotlinDouble? = nil
init(candle: Candle) {
self.close = candle.close
self.open = candle.open
self.high = candle.hi
self.low = candle.lo
self.time = Double(candle.timestamp)
self.volume = candle.volume
if let vwap = candle.vwap {
self.vwap = KotlinDouble(value: vwap)
} else {
self.vwap = nil
}
}
}
private struct StudyDataKotlin {
var array: KotlinArray<KotlinDoubleArray>?
var id: String
}
private struct CalculatorsData {
var id: String
var study: Study
}
private class TradingSessionClass: TradingSessionData {
var sessionType: String
var from: Double
var high_: KotlinDouble?
var low_: KotlinDouble?
var to: Double
init(
sessionType: String,
from: Double,
high_: KotlinDouble? = nil,
low_: KotlinDouble? = nil,
to: Double
) {
self.sessionType = sessionType
self.from = from
self.high_ = high_
self.low_ = low_
self.to = to
}
}
#endif