Skip to main content

Events Provider

Provider that supplies data about events for the selected trading instrument - corporate actions, corporate calendar, earning reports.

/**
* Interface for receiving events data
*
* Events: dividends, splits, earnings, conference calls
*
* After loading dxCharts library chart, events data is taken from this interface
*
* When connecting dxCharts library, developer can implement this interface or use the default implementation [com.devexperts.dxcharts.provider.events.DxFeedEventsProvider] and pass it to the library using [DxChartsDataProviders] data class *
*
* Use [dataFlow] to get events data
*
* Use [changeSymbol] to change events' instrument symbol
*/
interface DxChartsEventsProvider {
/**
* Flow of receiving events data
*
* Events data is represented by [Events] - [Events.dividends], [Events.splits], [Events.earnings], [Events.conferenceCalls]
*/
val dataFlow: StateFlow<Events>
/**
* Changes events' instrument symbol
*
* @param symbol instrument symbol
*/
fun changeSymbol(symbol: String)
}

Method: changeSymbol​

This method is used to change the symbol of the instrument for which the events data is provided. It takes the following parameter:

  • symbol: The symbol of the selected instrument. A symbol is a unique identifier used for trading on the exchange. For example, the symbol for Apple stocks on NASDAQ is "AAPL".

Example​

Here is an example of how to use the changeSymbol method:

eventsProvider.changeSymbol("AAPL")

Data is sent by updating the state of the dataFlow variable. Class com.devexperts.dxcharts.provider.domain.Events - an object storing arrays of all events on the chart:

/**
* Data class for storing data about events (dividends, earnings, splits, conference calls)
*
* @property dividends List of [DividendsEvent] objects representing the dividends events.
* @property earnings List of [EarningsEvent] objects representing the earnings events.
* @property splits List of [SplitsEvent] objects representing the splits events.
* @property conferenceCalls List of [ConferenceCallsEvent] objects representing the conference calls events.
*/
data class Events(
val dividends: List<DividendsEvent>,
val earnings: List<EarningsEvent>,
val splits: List<SplitsEvent>,
val conferenceCalls: List<ConferenceCallsEvent>
) {
/**
* Data class for storing data about a dividends event.
*
* @property gross Gross value of the dividends.
* @property timestamp Timestamp of the dividends event in milliseconds.
*/
data class DividendsEvent(
val gross: Float,
val timestamp: Long
)
/**
* Data class for storing data about an earnings event.
*
* @property basic Basic value of earnings (e.g., 4.19).
* @property diluted Diluted value of earnings (e.g., 4.16).
* @property timestamp Timestamp of the earnings event in milliseconds.
* @property periodEnding Timestamp in milliseconds of the period ending (e.g., Sep 2023).
*/
data class EarningsEvent(
val basic: Float,
val diluted: Float,
val timestamp: Long,
val periodEnding: Long
)
/**
* Data class for storing data about a splits event.
*
* @property splitFrom "Split from" value.
* @property splitTo "Split to" value.
* @property timestamp Timestamp of the splits event in milliseconds.
*/
data class SplitsEvent(
val splitFrom: Float,
val splitTo: Float,
val timestamp: Long
)
/**
* Data class for storing data about a conference calls event.
*
* @property timestamp Timestamp of the conference calls event in milliseconds.
* @property referencePeriod Reference period of the conference calls event. This is a string that usually represents a quarter and year (e.g., "Q3 2021"), but the format may vary depending on the data source.
*/
data class ConferenceCallsEvent(
val timestamp: Long,
val referencePeriod: String,
)
}

Events set on the chart look as follows:

events_on_chart

Here is the default implementation of DxChartsEventsProvider:

/**
* Default implementation of [DxChartsEventsProvider]
*
* Uses dxFeed API to get events data
*
* Uses the DxFeed api via Http requests using [OkHttpClient]
*
* Path to get dividends data: corporate-action/symbol/ [CORPORATE_ACTIONS]
*
* Path to get events data: corporate-action/symbol/ [CORPORATE_ACTIONS]
*
* Path to get conference calls data: corporate-calendar/symbol/ [EARNING]
*
* Path to get earnings data: earning-report/symbol/ [EARNING]
*
* A token [token] is used for authorization with Base64 encoding
*
* When loading dxCharts library and starting the provider ([changeSymbol]), a request is sent with the current symbol of the instrument ([symbol])
* with 2 paths - [CORPORATE_ACTIONS], [EARNING] using [requestCorporateActions] and [requestEarnings] methods
*
* Query responses results are put into objects [EventsResponse].
*
* Then the data is parsed and divided into objects [DxFeedCorporateAction] and [DxFeedEarning].
*
* The next step is the conversion and normalization of data into data classes used by the library [Events.SplitsEvent], [Events.DividendsEvent], [Events.EarningsEvent], [Events.ConferenceCallsEvent] with extensions [convertAndNormalizeSplits], [convertAndNormalizeDividends], [convertAndNormalizeEarnings], [convertAndNormalizeCalls]
*
* After this, the data is transferred to [Events] object and sent to [dataFlow]
*
* Requests occurs once every 10 seconds ([UPDATE_DELAY]).
* If an error occurs during the request, it is repeated every 500 milliseconds ([REQUEST_DELAY])
*
* The data retrieval logic is executed in a separate thread using [ExecutorService] [executorService].
*
* Inside a separate thread [requester] method id executed - method that executes queries and processes responses.
*
* Changing the symbol of the instrument and starting the work of the provider occurs using [changeSymbol] method - if the provider has already been started, then the thread with it stops, a new thread with a new symbol is started
*
* To manually stop the provider, you can use [disconnect] method. It stops the provider, cleans [executorService] and disconnects from dxFeed API.
*
* @property dataFlow flow of received and processed events data
* @property gson Gson object for parsing json
* @property executorService executor service with for running requests in separate thread (creating with Executors.newFixedThreadPool(1))
* @property _dataFlow internal [MutableStateFlow] of received and processed events data
* @property dataFlow public [StateFlow] of received and processed events data that is getting state from [_dataFlow]
* @property client [OkHttpClient] for sending requests
* @property request [Request] for sending requests with "Authorization" header with [token]
* @property loaded flag that shows if data was successfully loaded in [requester] method
* @property connected flag that shows if provider is working and connected to dxFeed API
* @property symbol current symbol of selected instrument
* @property _errorFlow Internal [MutableStateFlow] for sending errors.
* @property errorFlow [StateFlow] for sending errors.
*/
class DxFeedEventsProvider(
private val url: String = "",
private val token: String = "",
) : DxChartsEventsProvider, DxChartsErrorProvider<EventsProviderError> {
override val dataFlow: StateFlow<Events>
get() = _dataFlow
private val gson = Gson()
private var executorService: ExecutorService? = null
private val _dataFlow = MutableStateFlow(Events.EMPTY)
private val _errorFlow: MutableStateFlow<EventsProviderError?> = MutableStateFlow(null)
override val errorFlow: StateFlow<EventsProviderError?> get() = _errorFlow
private val client = OkHttpClient()
.newBuilder()
.build()
private val request = Request.Builder()
.addHeader("Authorization", token)
@Volatile
private var loaded = false
@Volatile
private var connected = false
@Volatile
private var symbol: String? = null
/**
* Changes events' instrument symbol and starts/restarts provider
* If provider is already running, then it will be stopped and new thread with new symbol will be started
*
* [requester] method will be called with new symbol in new thread
*
* Initial events data after starting/restarting provider will be empty [Events.EMPTY]
*
* [connected] flag will be set to true
*
* @param symbol instrument symbol
*/
override fun changeSymbol(symbol: String) {
executorService?.let {
if (!it.isShutdown) {
try {
it.shutdownNow()
} catch (e: Exception) {
_errorFlow.tryEmit(EventsProviderError.UnknownError("Failed to shutdown executor service: $e.message", e))
}
}
}
this.symbol = symbol
connected = true
_dataFlow.tryEmit(Events.EMPTY)
executorService = Executors.newFixedThreadPool(1)
executorService?.execute {
requester()
}
}
/**
* Stops provider, cleans [executorService] and disconnects from dxFeed API
*
* [connected] flag will be set to false
*
* [loaded] flag will be set to true
*
* [executorService] will be shutdown
*/
fun disconnect() {
connected = false
loaded = true
try {
executorService?.shutdownNow()
} catch (e: Exception) {
_errorFlow.tryEmit(EventsProviderError.UnknownError("Failed to shutdown executor service: $e.message", e))
}
}
/**
* Method for a looped requesting and processing ([requestEvents]) events data from dxFeed API with period [UPDATE_DELAY]
*
* Loop will be stopped if [connected] flag will be set to false
*
* Method also contains a second loop that will repeat requests at [REQUEST_DELAY] intervals if an error occurs during the request
*/
private fun requester() {
try {
_errorFlow.tryEmit(null)
while (connected) {
loaded = false
while (!loaded) {
requestEvents()
try {
Thread.sleep(REQUEST_DELAY)
} catch (e: InterruptedException) {
_errorFlow.tryEmit(EventsProviderError.InterruptionError("Requester loop interrupted by thread interruption"))
}
}
try {
Thread.sleep(UPDATE_DELAY)
} catch (e: InterruptedException) {
_errorFlow.tryEmit(EventsProviderError.InterruptionError("Requester loop interrupted by thread interruption"))
}
}
executorService?.shutdown()
} catch (e: Exception) {
_errorFlow.tryEmit(EventsProviderError.UnknownError("Requester loop interrupted: $e.message", e))
Log.e(TAG, "Requester loop interrupted", e)
}
}
/**
* Method for requesting and processing events data from dxFeed API
*
* Method sends a request with the current symbol of the instrument ([symbol]) ([symbol])
* with 3 paths - [CORPORATE_ACTIONS], [EARNING] using [requestCorporateActions] and [requestEarnings] methods.
*
* Query responses results are put into objects [EventsResponse]
*
* Then the data is parsed and divided into objects [DxFeedCorporateAction] and [DxFeedEarning]
*
* The next step is the conversion and normalization of data into data classes used by the library [Events.SplitsEvent], [Events.DividendsEvent], [Events.EarningsEvent], [Events.ConferenceCallsEvent] with extensions [convertAndNormalizeSplits], [convertAndNormalizeDividends], [convertAndNormalizeEarnings], [convertAndNormalizeCalls] with extensions [convertAndNormalizeSplits], [convertAndNormalizeDividends], [convertAndNormalizeEarnings], [convertAndNormalizeCalls]
*
* After this, the data is transferred to [Events] object and sent to [dataFlow]
*/
private fun requestEvents() {
val corporateActionsResponse = requestCorporateActions()
val earningsAndCallsResponse = requestEarnings()
val earningsAndCalls = if (earningsAndCallsResponse.status == OK) {
parseListData<DxFeedEarning>(earningsAndCallsResponse.data ?: "[]")
} else emptyList()
val dividendsAndSplits = if (corporateActionsResponse.status == OK) {
parseListData<DxFeedCorporateAction>(corporateActionsResponse.data ?: "[]")
} else emptyList()
val earnings = earningsAndCalls.filter { it.isEarning() }
val conferenceCalls = earningsAndCalls.filter { it.isConferenceCall() }
val dividends = dividendsAndSplits.filter { it.isDividend() }
val splits = dividendsAndSplits.filter { it.isStockSplit() }
_dataFlow.tryEmit(
Events(
dividends = dividends.convertAndNormalizeDividends(),
splits = splits.convertAndNormalizeSplits(),
conferenceCalls = conferenceCalls.convertAndNormalizeCalls(),
earnings = earnings.convertAndNormalizeEarnings()
).apply {
Log.d(TAG, "events loaded")
}
)
if (earningsAndCallsResponse.status == OK || corporateActionsResponse.status == OK) {
loaded = true
}
}
/**
* Method for requesting corporate actions data from dxFeed API
*
* Sends a request for dividends and splits data with the current instrument symbol ([symbol]) to the path [URL] + [CORPORATE_ACTIONS]
*
* The request is made using [OkHttpClient] [client] and [Request] [request]
*
* @return [EventsResponse] with status [EventsResponse.Status.OK] and data if request was successful or with status [EventsResponse.Status.FAILED] and message if request was failed
*/
private fun requestCorporateActions(): EventsResponse<String> {
val corpActionsRequest = request.url(url + CORPORATE_ACTIONS + symbol).build()
val corpActionsCall = client.newCall(corpActionsRequest)
return try {
corpActionsCall.execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string()
?: return EventsResponse(status = EventsResponse.Status.EMPTY)
Log.d(TAG, "corporate actions loaded")
EventsResponse(status = OK, data = body)
} else {
_errorFlow.tryEmit(EventsProviderError.NetworkError("Corporate actions request failed: $response.message $response.code"))
EventsResponse(
status = EventsResponse.Status.FAILED,
message = "$response.message $response.code"
)
}
}
} catch (e: Exception) {
_errorFlow.tryEmit(EventsProviderError.NetworkError("Corporate actions request exception: $e.message"))
EventsResponse(
status = EventsResponse.Status.FAILED,
message = e.message
)
}
}
/**
* Method for requesting earnings data from dxFeed API
*
* Sends a request for conference calls data with the current instrument symbol ([symbol]) to the path [url] + [EARNING]
*
* The request is made using [OkHttpClient] [client] and [Request] [request]
*
* @return [EventsResponse] with status [EventsResponse.Status.OK] and data if request was successful or with status [EventsResponse.Status.FAILED] and message if request was failed
*/
private fun requestEarnings(): EventsResponse<String> {
val earningRequest = request.url(url + EARNING + symbol).build()
val earningCall = client.newCall(earningRequest)
return try {
earningCall.execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string()
?: return EventsResponse(status = EventsResponse.Status.EMPTY)
Log.d(TAG, "earnings loaded")
return EventsResponse(status = OK, data = body)
} else {
_errorFlow.tryEmit(EventsProviderError.NetworkError("Earning reports request failed: $response.message $response.code"))
EventsResponse(
status = EventsResponse.Status.FAILED,
message = "$response.message $response.code"
)
}
}
} catch (e: Exception) {
_errorFlow.tryEmit(EventsProviderError.NetworkError("Earning reports request exception: $e.message"))
EventsResponse(
status = EventsResponse.Status.FAILED,
message = e.message
)
}
}
/**
* Internal data class for storing response from dxFeed API
*
* @param status status of the response
* @param data data of the response
* @param message message of the response
*/
private data class EventsResponse<T>(
val status: Status,
val data: T? = null,
val message: String? = null
) {
/**
* Status of the response
*/
enum class Status {
OK, FAILED, EMPTY
}
}
/**
* Extension function for parsing json string to list of objects of type [T]
*
* @param str json string
* @return list of objects
*/
@Suppress("UNCHECKED_CAST")
private inline fun <reified T> parseListData(str: String): List<T> {
val type = TypeToken.getParameterized(List::class.java, T::class.java)
return try {
gson.fromJson(str, type) as List<T>? ?: emptyList()
} catch (e: Exception) {
_errorFlow.tryEmit(EventsProviderError.ParsingError("Parsing error: $e.message", e))
emptyList()
}
}
/**
* Extension function for converting and normalizing list of [DxFeedEarning] to list of [Events.EarningsEvent]
*/
private fun List<DxFeedEarning>.convertAndNormalizeEarnings(): List<Events.EarningsEvent> {
return filter { !it.eps.isNaN() && !it.estimatedEPS.isNaN() }
.map {
Events.EarningsEvent(
basic = it.eps,
diluted = it.estimatedEPS,
periodEnding = it.ymd.parseYmd(),
timestamp = it.ymd.parseYmd()
)
}
.distinctBy { it.timestamp }
}
/**
* Extension function for converting and normalizing list of [DxFeedEarning] to list of [Events.ConferenceCallsEvent]
*
* @return list of [Events.ConferenceCallsEvent]
*/
private fun List<DxFeedEarning>.convertAndNormalizeCalls(): List<Events.ConferenceCallsEvent> {
return map {
Events.ConferenceCallsEvent(
timestamp = it.ymd.parseYmd(),
referencePeriod = it.referencePeriod ?: "",
)
}.distinctBy { it.timestamp }
}
/**
* Extension function for converting and normalizing list of [DxFeedCorporateAction] to list of [Events.SplitsEvent]
*
* @return list of [Events.SplitsEvent]
*/
private fun List<DxFeedCorporateAction>.convertAndNormalizeSplits(): List<Events.SplitsEvent> {
return map {
Events.SplitsEvent(
splitFrom = it.ext?.splitFrom ?: 0f,
splitTo = it.ext?.splitTo ?: 0f,
timestamp = it.exYmd.parseYmd()
)
}.distinctBy { it.timestamp }
}
/**
* Extension function for converting and normalizing list of [DxFeedCorporateAction] to list of [Events.DividendsEvent]
*
* @return list of [Events.DividendsEvent]
*/
private fun List<DxFeedCorporateAction>.convertAndNormalizeDividends(): List<Events.DividendsEvent> {
return map {
Events.DividendsEvent(
gross = it.adjustmentValue ?: 0f,
timestamp = it.exYmd.parseYmd()
)
}
}
/**
* Extension function for converting [Int] in format YYYYMMDD to [Long] timestamp in milliseconds
*
* @return timestamp in milliseconds
*/
private fun Int.parseYmd(): Long {
val year = this / 10000
val month = (this - year * 10000) / 100
val day = this - year * 10000 - month * 100
return Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
set(Calendar.YEAR, year)
set(Calendar.MONTH, month - 1)
set(Calendar.DAY_OF_MONTH, day)
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
companion object {
const val TAG = "DxFeedEventsProvider"
const val REQUEST_DELAY = 2000L
private const val UPDATE_DELAY = 10000L
private const val CORPORATE_ACTIONS = "corporate-action/symbol/"
private const val EARNING = "earning/symbol/"
}
}
/**
- Sealed class for representing different types of errors in the DxFeedEventsProvider.
*/
sealed class EventsProviderError(override val message: String, override val error: Throwable?) :
ProviderError {
data class NetworkError(
override val message: String,
override val error: Throwable? = null
) : EventsProviderError(message, error)
data class InterruptionError(
override val message: String,
override val error: Throwable? = null
) : EventsProviderError(message, error)
data class ParsingError(
override val message: String,
override val error: Throwable? = null
) : EventsProviderError(message, error)
data class UnknownError(
override val message: String,
override val error: Throwable? = null
) : EventsProviderError(message, error)
}