Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.6.53: add analytics for SL/TP #325

Merged
merged 14 commits into from
May 1, 2024
Merged
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.6.52"
version = "1.6.53"

repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import exchange.dydx.abacus.utils.mutable
import exchange.dydx.abacus.utils.safeSet
import kotlin.math.abs

internal object TriggerOrdersConstants {
const val TRIGGER_ORDER_DEFAULT_DURATION_DAYS = 90.0
}

@Suppress("UNCHECKED_CAST")
internal class TriggerOrdersInputCalculator(val parser: ParserProtocol) {
internal fun calculate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ enum class AnalyticsEvent(val rawValue: String) {
// App
NetworkStatus("NetworkStatus"),

// Trade Events
// Trade
TradePlaceOrderClick("TradePlaceOrderClick"),
TradeCancelOrderClick("TradeCancelOrderClick"),
TradePlaceOrder("TradePlaceOrder"),
Expand All @@ -221,6 +221,9 @@ enum class AnalyticsEvent(val rawValue: String) {
TradeCancelOrderConfirmed("TradeCancelOrderConfirmed"),
TradePlaceOrderConfirmed("TradePlaceOrderConfirmed"),

// Trigger Order
TriggerOrderClick("TriggerOrderClick"),

// Transfers
TransferFaucetConfirmed("TransferFaucetConfirmed");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package exchange.dydx.abacus.state.manager

import exchange.dydx.abacus.calculator.TriggerOrdersConstants.TRIGGER_ORDER_DEFAULT_DURATION_DAYS
import exchange.dydx.abacus.output.Compliance
import exchange.dydx.abacus.output.ComplianceAction
import exchange.dydx.abacus.output.ComplianceStatus
Expand Down Expand Up @@ -120,9 +121,6 @@ open class StateManagerAdaptor(
var stateNotification: StateNotificationProtocol?,
var dataNotification: DataNotificationProtocol?,
) {
@Suppress("LocalVariableName", "PropertyName")
private val TRIGGER_ORDER_DEFAULT_DURATION_DAYS = 28.0

var stateMachine: TradingStateMachine = PerpTradingStateMachine(
environment,
uiImplementations.localizer,
Expand Down Expand Up @@ -1878,12 +1876,13 @@ open class StateManagerAdaptor(
fun triggerOrdersPayload(): HumanReadableTriggerOrdersPayload {
val placeOrderPayloads = mutableListOf<HumanReadablePlaceOrderPayload>()
val cancelOrderPayloads = mutableListOf<HumanReadableCancelOrderPayload>()
val triggerOrders = stateMachine.state?.input?.triggerOrders
val triggerOrders = requireNotNull(stateMachine.state?.input?.triggerOrders) { "triggerOrders input was null" }

val marketId = requireNotNull(triggerOrders.marketId) { "triggerOrders.marektId was null" }
val subaccountNumber = connectedSubaccountNumber ?: throw Exception("subaccountNumber is null")
val subaccount = stateMachine.state?.subaccount(subaccountNumber) ?: throw Exception("subaccount is null")

val marketId = triggerOrders?.marketId ?: throw Exception("marketId is null")
val position = subaccount.openPositions?.find { it.id == marketId }
val positionSize = position?.size?.current

fun updateTriggerOrder(triggerOrder: TriggerOrder) {
// Cases
Expand Down Expand Up @@ -1923,6 +1922,8 @@ open class StateManagerAdaptor(
}

return HumanReadableTriggerOrdersPayload(
marketId,
positionSize,
placeOrderPayloads,
cancelOrderPayloads,
)
Expand Down Expand Up @@ -2152,6 +2153,7 @@ open class StateManagerAdaptor(
?: throw Exception("subaccount is null")
val order = subaccount.orders?.firstOrNull { it.id == orderId }
?: throw Exception("order is null")
val type = order.type.rawValue
val clientId = order.clientId ?: throw Exception("clientId is null")
val orderFlags = order.orderFlags ?: throw Exception("orderFlags is null")
val clobPairId = order.clobPairId ?: throw Exception("clobPairId is null")
Expand All @@ -2160,6 +2162,7 @@ open class StateManagerAdaptor(

return HumanReadableCancelOrderPayload(
subaccountNumber,
type,
orderId,
clientId,
orderFlags,
Expand Down Expand Up @@ -2291,13 +2294,19 @@ open class StateManagerAdaptor(
}
}

internal open fun fromSlTpDialogParams(fromSlTpDialog: Boolean): IMap<String, Any> {
return iMapOf(
"fromSlTpDialog" to fromSlTpDialog,
)
}

internal open fun trackingParams(interval: Double): IMap<String, Any> {
return iMapOf(
"roundtripMs" to interval,
)
}

internal fun tracking(eventName: String, params: IMap<String, Any>?) {
internal fun tracking(eventName: String, params: IMap<String, Any?>?) {
val paramsAsString = jsonEncoder.encode(params)
ioImplementations.threading?.async(ThreadingType.main) {
ioImplementations.tracking?.log(eventName, paramsAsString)
Expand Down Expand Up @@ -2326,10 +2335,14 @@ open class StateManagerAdaptor(
if (placeOrderRecord != null) {
val interval = Clock.System.now().toEpochMilliseconds()
.toDouble() - placeOrderRecord.timestampInMilliseconds
val extraParams = ParsingHelper.merge(
trackingParams(interval),
fromSlTpDialogParams(placeOrderRecord.fromSlTpDialog),
)
tracking(
AnalyticsEvent.TradePlaceOrderConfirmed.rawValue,
ParsingHelper.merge(
trackingParams(interval),
extraParams,
orderAnalyticsPayload,
)?.toIMap(),
)
Expand All @@ -2342,10 +2355,14 @@ open class StateManagerAdaptor(
if (cancelOrderRecord != null) {
val interval = Clock.System.now().toEpochMilliseconds()
.toDouble() - cancelOrderRecord.timestampInMilliseconds
val extraParams = ParsingHelper.merge(
trackingParams(interval),
fromSlTpDialogParams(cancelOrderRecord.fromSlTpDialog),
)
tracking(
AnalyticsEvent.TradeCancelOrderConfirmed.rawValue,
ParsingHelper.merge(
trackingParams(interval),
extraParams,
orderAnalyticsPayload,
)?.toIMap(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import exchange.dydx.abacus.protocols.TransactionCallback
import exchange.dydx.abacus.protocols.TransactionType
import exchange.dydx.abacus.protocols.run
import exchange.dydx.abacus.responses.ParsingError
import exchange.dydx.abacus.responses.ParsingErrorType
import exchange.dydx.abacus.responses.ParsingException
import exchange.dydx.abacus.state.app.adaptors.V4TransactionErrors
import exchange.dydx.abacus.state.manager.configs.V4StateManagerConfigs
import exchange.dydx.abacus.state.model.TransferInputField
Expand Down Expand Up @@ -1000,11 +1002,15 @@ class V4StateManagerAdaptor(
callback: TransactionCallback,
payload: HumanReadablePlaceOrderPayload,
analyticsPayload: IMap<String, Any>?,
uiClickTimeMs: Double,
isTriggerOrder: Boolean = false,
): HumanReadablePlaceOrderPayload {
val clientId = payload.clientId
val string = Json.encodeToString(payload)
val uiClickTimeMs = trackOrderClick(analyticsPayload)
val marketId = payload.marketId

val position = stateMachine.state?.subaccount(subaccountNumber)?.openPositions?.find { it.id == marketId }
val positionSize = position?.size?.current

stopWatchingLastOrder()

Expand All @@ -1019,6 +1025,7 @@ class V4StateManagerAdaptor(
subaccountNumber,
clientId,
submitTimeMs,
fromSlTpDialog = isTriggerOrder,
),
)
}
Expand All @@ -1040,6 +1047,8 @@ class V4StateManagerAdaptor(
callback,
if (isTriggerOrder) {
HumanReadableTriggerOrdersPayload(
marketId,
positionSize,
listOf(payload),
emptyList(),
)
Expand All @@ -1056,15 +1065,19 @@ class V4StateManagerAdaptor(

private fun submitCancelOrder(
orderId: String,
marketId: String,
callback: TransactionCallback,
payload: HumanReadableCancelOrderPayload,
analyticsPayload: IMap<String, Any>?,
uiClickTimeMs: Double,
isTriggerOrder: Boolean = false,
) {
val clientId = payload.clientId
val string = Json.encodeToString(payload)

val uiClickTimeMs = trackOrderClick(analyticsPayload, isCancel = true)
val position = stateMachine.state?.subaccount(subaccountNumber)?.openPositions?.find { it.id == marketId }
val positionSize = position?.size?.current

val isShortTermOrder = payload.orderFlags == 0

stopWatchingLastOrder()
Expand All @@ -1080,6 +1093,7 @@ class V4StateManagerAdaptor(
subaccountNumber,
clientId,
submitTimeMs,
fromSlTpDialog = isTriggerOrder,
),
)
}
Expand All @@ -1101,6 +1115,8 @@ class V4StateManagerAdaptor(
callback,
if (isTriggerOrder) {
HumanReadableTriggerOrdersPayload(
marketId,
positionSize,
emptyList(),
listOf(payload),
)
Expand All @@ -1114,12 +1130,12 @@ class V4StateManagerAdaptor(
}

private fun trackOrderClick(
analyticsPayload: IMap<String, Any>?,
isCancel: Boolean = false
analyticsPayload: IMap<String, Any?>?,
analyticsEvent: AnalyticsEvent,
): Double {
val uiClickTimeMs = Clock.System.now().toEpochMilliseconds().toDouble()
tracking(
if (isCancel) AnalyticsEvent.TradeCancelOrderClick.rawValue else AnalyticsEvent.TradePlaceOrderClick.rawValue,
analyticsEvent.rawValue,
analyticsPayload,
)
return uiClickTimeMs
Expand All @@ -1135,8 +1151,7 @@ class V4StateManagerAdaptor(

tracking(
if (isCancel) AnalyticsEvent.TradeCancelOrder.rawValue else AnalyticsEvent.TradePlaceOrder.rawValue,
ParsingHelper.merge(uiTrackingParams(uiDelayTimeMs), analyticsPayload)
?.toIMap(),
ParsingHelper.merge(uiTrackingParams(uiDelayTimeMs), analyticsPayload)?.toIMap(),
)

return submitTimeMs
Expand All @@ -1163,59 +1178,86 @@ class V4StateManagerAdaptor(
override fun commitPlaceOrder(callback: TransactionCallback): HumanReadablePlaceOrderPayload {
val payload = placeOrderPayload()
val midMarketPrice = stateMachine.state?.marketOrderbook(payload.marketId)?.midPrice
val analyticsPayload = analyticsUtils.placeOrderAnalyticsPayload(payload, midMarketPrice)
return submitPlaceOrder(callback, payload, analyticsPayload)
val analyticsPayload = analyticsUtils.placeOrderAnalyticsPayload(payload, midMarketPrice, fromSlTpDialog = false, isClosePosition = false)
val uiClickTimeMs = trackOrderClick(analyticsPayload, AnalyticsEvent.TradePlaceOrderClick)

return submitPlaceOrder(callback, payload, analyticsPayload, uiClickTimeMs)
}

override fun commitClosePosition(callback: TransactionCallback): HumanReadablePlaceOrderPayload {
val payload = closePositionPayload()
val midMarketPrice = stateMachine.state?.marketOrderbook(payload.marketId)?.midPrice
val analyticsPayload = analyticsUtils.placeOrderAnalyticsPayload(payload, midMarketPrice, true)
return submitPlaceOrder(callback, payload, analyticsPayload)
val analyticsPayload = analyticsUtils.placeOrderAnalyticsPayload(payload, midMarketPrice, fromSlTpDialog = false, isClosePosition = true)
val uiClickTimeMs = trackOrderClick(analyticsPayload, AnalyticsEvent.TradePlaceOrderClick)

return submitPlaceOrder(callback, payload, analyticsPayload, uiClickTimeMs)
}

override fun cancelOrder(orderId: String, callback: TransactionCallback) {
val payload = cancelOrderPayload(orderId)
val subaccount = stateMachine.state?.subaccount(subaccountNumber)
val existingOrder = subaccount?.orders?.firstOrNull { it.id == orderId }
val existingOrder = subaccount?.orders?.firstOrNull { it.id == orderId } ?: throw ParsingException(
ParsingErrorType.MissingRequiredData,
"no existing order to be cancelled for $orderId",
)
val marketId = existingOrder.marketId
val analyticsPayload = analyticsUtils.cancelOrderAnalyticsPayload(
payload,
existingOrder,
fromSlTpDialog = false,
)
val uiClickTimeMs = trackOrderClick(analyticsPayload, AnalyticsEvent.TradeCancelOrderClick)

submitCancelOrder(orderId, callback, payload, analyticsPayload)
submitCancelOrder(orderId, marketId, callback, payload, analyticsPayload, uiClickTimeMs)
}

override fun commitTriggerOrders(callback: TransactionCallback): HumanReadableTriggerOrdersPayload {
val payloads = triggerOrdersPayload()
val payload = triggerOrdersPayload()

// this is a diff payload that summarizes the actions to be taken
val analyticsPayload = analyticsUtils.triggerOrdersAnalyticsPayload(payload)
val uiClickTimeMs = trackOrderClick(analyticsPayload, AnalyticsEvent.TriggerOrderClick)

payloads.cancelOrderPayloads.forEach { payload ->
payload.cancelOrderPayloads.forEach { cancelPayload ->
val subaccount = stateMachine.state?.subaccount(subaccountNumber)
val existingOrder = subaccount?.orders?.firstOrNull { it.id == payload.orderId }
val analyticsPayload = analyticsUtils.cancelOrderAnalyticsPayload(
payload,
val existingOrder = subaccount?.orders?.firstOrNull { it.id == cancelPayload.orderId }
?: throw ParsingException(
ParsingErrorType.MissingRequiredData,
"no existing order to be cancelled for $cancelPayload.orderId",
)
val marketId = existingOrder.marketId
val cancelOrderAnalyticsPayload = analyticsUtils.cancelOrderAnalyticsPayload(
cancelPayload,
existingOrder,
fromSlTpDialog = true,
)
submitCancelOrder(
cancelPayload.orderId,
marketId,
callback,
cancelPayload,
cancelOrderAnalyticsPayload,
uiClickTimeMs,
true,
)
submitCancelOrder(payload.orderId, callback, payload, analyticsPayload, true)
}

payloads.placeOrderPayloads.forEach { payload ->
val midMarketPrice = stateMachine.state?.marketOrderbook(payload.marketId)?.midPrice
val analyticsPayload = analyticsUtils.placeOrderAnalyticsPayload(
payload,
payload.placeOrderPayloads.forEach { placePayload ->
val midMarketPrice = stateMachine.state?.marketOrderbook(placePayload.marketId)?.midPrice
val placeOrderAnalyticsPayload = analyticsUtils.placeOrderAnalyticsPayload(
placePayload,
midMarketPrice,
isClosePosition = false,
fromSlTpDialog = true,
isClosePosition = false,
)
submitPlaceOrder(callback, payload, analyticsPayload, true)
submitPlaceOrder(callback, placePayload, placeOrderAnalyticsPayload, uiClickTimeMs, true)
}

if (payloads.cancelOrderPayloads.isEmpty() && payloads.placeOrderPayloads.isEmpty()) {
send(null, callback, payloads)
if (payload.cancelOrderPayloads.isEmpty() && payload.placeOrderPayloads.isEmpty()) {
send(null, callback, payload)
}

return payloads
return payload
}

override fun commitTransfer(callback: TransactionCallback) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ data class PlaceOrderRecord(
val subaccountNumber: Int,
val clientId: Int,
val timestampInMilliseconds: Double,
val fromSlTpDialog: Boolean,
)

data class CancelOrderRecord(
val subaccountNumber: Int,
val clientId: Int,
val timestampInMilliseconds: Double,
val fromSlTpDialog: Boolean,
)

@JsExport
Expand Down Expand Up @@ -61,6 +63,7 @@ data class HumanReadablePlaceOrderPayload(
@Serializable
data class HumanReadableCancelOrderPayload(
val subaccountNumber: Int,
val type: String,
val orderId: String,
val clientId: Int,
val orderFlags: Int,
Expand All @@ -72,6 +75,8 @@ data class HumanReadableCancelOrderPayload(
@JsExport
@Serializable
data class HumanReadableTriggerOrdersPayload(
val marketId: String,
val positionSize: Double?,
val placeOrderPayloads: List<HumanReadablePlaceOrderPayload>,
val cancelOrderPayloads: List<HumanReadableCancelOrderPayload>,
)
Expand Down
Loading
Loading