feat: train forecasts on trade outcomes
This commit is contained in:
@@ -10,7 +10,7 @@ android {
|
||||
applicationId = "xyz.kusoft.tradebotmonitor"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 20
|
||||
versionName = "0.4.1"
|
||||
versionCode = 21
|
||||
versionName = "0.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
+46
-10
@@ -328,7 +328,7 @@ class MainActivity : Activity() {
|
||||
val reason = signal?.reason?.ifBlank { market.forecast?.reason.orEmpty() } ?: market.forecast?.reason.orEmpty()
|
||||
|
||||
setText(binding.price, price(latestPrice(market)), colorForSigned(edge))
|
||||
setText(binding.edgeLine, "Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h")
|
||||
setText(binding.edgeLine, "Edge ${signedPercent(edge, 2)} · ${probabilityMetricLabel(signal, market.forecast)} ${probability(probability)} · 1h")
|
||||
setText(binding.equity, money(data.account.equity))
|
||||
setText(
|
||||
binding.status,
|
||||
@@ -553,7 +553,7 @@ class MainActivity : Activity() {
|
||||
addView(priceText.top(dp(6)))
|
||||
val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
|
||||
val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
|
||||
val edgeLine = text("Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h", 13f, Typeface.NORMAL, palette.muted)
|
||||
val edgeLine = text("Edge ${signedPercent(edge, 2)} · ${probabilityMetricLabel(signal, market.forecast)} ${probability(probability)} · 1h", 13f, Typeface.NORMAL, palette.muted)
|
||||
tradingBindings?.edgeLine = edgeLine
|
||||
addView(edgeLine.top(dp(2)))
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
@@ -728,7 +728,7 @@ class MainActivity : Activity() {
|
||||
addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text(market.symbol, 15f, Typeface.BOLD))
|
||||
addView(text("P(up) ${probability(probability)} · Kelly ${number(signal?.positionNotionalUsdt ?: 0.0, 1)}", 11f, Typeface.NORMAL, palette.muted).top(dp(3)))
|
||||
addView(text("${probabilityMetricLabel(signal, market.forecast)} ${probability(probability)} · Kelly ${number(signal?.positionNotionalUsdt ?: 0.0, 1)}", 11f, Typeface.NORMAL, palette.muted).top(dp(3)))
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.7f))
|
||||
|
||||
addView(text(price(latestPrice(market)), 14f, Typeface.BOLD).apply {
|
||||
@@ -778,7 +778,7 @@ class MainActivity : Activity() {
|
||||
addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
addView(text("Edge: ${signedPercent(signal?.expectedReturnPercent ?: forecast?.expectedReturnPercent ?: 0.0, 2)}", 13f, Typeface.NORMAL, palette.green), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(text("P(up): ${probability(signal?.probabilityUp ?: forecast?.probabilityUp ?: 0.0)}", 13f, Typeface.NORMAL, palette.green), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(text("${probabilityMetricLabel(signal, forecast)}: ${probability(signal?.probabilityUp ?: forecast?.probabilityUp ?: 0.0)}", 13f, Typeface.NORMAL, palette.green), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
})
|
||||
addView(text(signal?.reason?.ifBlank { forecast?.reason.orEmpty() }?.ifBlank { "Нет объяснения от модели" } ?: "Нет объяснения от модели", 13f, Typeface.BOLD).top(dp(12)))
|
||||
val candle = market.candles.lastOrNull()
|
||||
@@ -1090,17 +1090,32 @@ class MainActivity : Activity() {
|
||||
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
|
||||
val activeJob = coordination.optJSONObject("active_job")
|
||||
val isTrainingActive = retrainRequestInFlight || (activeJob?.optStringClean("status") in setOf("pending", "running"))
|
||||
val latestEvaluation = latestTrainingEvaluation(retrain)
|
||||
val displayedEvaluation = latestEvaluation ?: retrain
|
||||
val replay = displayedEvaluation
|
||||
.optJSONObject("candidate")
|
||||
?.optJSONObject("full_replay")
|
||||
?: backtest.optJSONObject("full_replay")
|
||||
?: JSONObject()
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(trainingComputerPanel(retrain).top(dp(12)))
|
||||
addView(thinDivider().top(dp(12)))
|
||||
addView(trainingProcessPanel(coordination))
|
||||
if (retrain.optBoolean("available", false)) {
|
||||
addView(guardSummaryPanel(retrain).top(dp(10)))
|
||||
if (displayedEvaluation.optJSONObject("candidate") != null) {
|
||||
addView(guardSummaryPanel(displayedEvaluation).top(dp(10)))
|
||||
}
|
||||
val replay = backtest.optJSONObject("full_replay") ?: JSONObject()
|
||||
addView(keyValueLine("Replay сделок", replay.optInt("trades", 0).toString()).top(dp(4)))
|
||||
addView(keyValueLine("Replay PnL", signedMoney(replay.optDouble("net_pnl", 0.0)), colorForSigned(replay.optDouble("net_pnl", 0.0))).top(dp(4)))
|
||||
val replayTotalPercent = replay.optDoubleOrNull("total_net_percent")
|
||||
val replayAveragePercent = replay.optDoubleOrNull("avg_net_percent")
|
||||
val replayPnl = when {
|
||||
replayTotalPercent != null && replayAveragePercent != null ->
|
||||
"${signedPercent(replayTotalPercent)} (средняя ${signedPercent(replayAveragePercent)})"
|
||||
replayTotalPercent != null -> signedPercent(replayTotalPercent)
|
||||
else -> signedMoney(replay.optDouble("net_pnl", 0.0))
|
||||
}
|
||||
val replayPnlValue = replayTotalPercent ?: replay.optDouble("net_pnl", 0.0)
|
||||
addView(keyValueLine("Replay P&L", replayPnl, colorForSigned(replayPnlValue)).top(dp(4)))
|
||||
if (isTrainingActive) {
|
||||
addView(disabledActionButton(if (retrainRequestInFlight) "Отправляю команду" else "Обучение уже идет").top(dp(10)))
|
||||
} else {
|
||||
@@ -1163,11 +1178,26 @@ class MainActivity : Activity() {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(keyValueLine("Последний gate", status, if (accepted) palette.green else palette.amber))
|
||||
addView(text(explanation, 12f, Typeface.NORMAL, palette.text).top(dp(7)))
|
||||
if (!candidateScore.isNaN() && !currentScore.isNaN()) {
|
||||
addView(keyValueLine("Score candidate / текущая", "${number(candidateScore, 2)} / ${number(currentScore, 2)}", if (accepted) palette.green else palette.amber).top(dp(7)))
|
||||
when {
|
||||
!candidateScore.isNaN() && !currentScore.isNaN() ->
|
||||
addView(keyValueLine("SCORE кандидата / действующей", "${number(candidateScore, 3)} / ${number(currentScore, 3)}", if (accepted) palette.green else palette.amber).top(dp(7)))
|
||||
!candidateScore.isNaN() ->
|
||||
addView(keyValueLine("SCORE кандидата", number(candidateScore, 3), if (accepted) palette.green else palette.amber).top(dp(7)))
|
||||
!currentScore.isNaN() ->
|
||||
addView(keyValueLine("SCORE действующей", number(currentScore, 3), palette.muted).top(dp(7)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun latestTrainingEvaluation(retrain: JSONObject): JSONObject? {
|
||||
val latestJob = retrain
|
||||
.optJSONObject("coordination")
|
||||
?.optJSONObject("latest_job")
|
||||
?: return null
|
||||
if (latestJob.optStringClean("status") !in setOf("completed", "failed")) return null
|
||||
val summary = latestJob.optJSONObject("summary") ?: return null
|
||||
return summary.takeIf { it.optJSONObject("candidate") != null }
|
||||
}
|
||||
|
||||
private fun liveSettingsBlock(data: BotSnapshot?): View {
|
||||
val config = data?.config ?: JSONObject()
|
||||
return LinearLayout(this).apply {
|
||||
@@ -1812,6 +1842,12 @@ class MainActivity : Activity() {
|
||||
private fun probability(value: Double): String =
|
||||
percent(normalizedProbability(value), 0)
|
||||
|
||||
private fun probabilityMetricLabel(signal: SignalData?, forecast: ForecastData?): String =
|
||||
if (
|
||||
signal?.targetTransform == "barrier_net_return" ||
|
||||
forecast?.targetTransform == "barrier_net_return"
|
||||
) "P(TP<SL)" else "P(up)"
|
||||
|
||||
private fun normalizedProbability(value: Double): Double =
|
||||
if (abs(value) <= 1.0) value * 100.0 else value
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ data class ForecastData(
|
||||
val model: String,
|
||||
val expectedReturnPercent: Double,
|
||||
val probabilityUp: Double,
|
||||
val probabilityTakeProfitFirst: Double?,
|
||||
val targetTransform: String,
|
||||
val skill: Double,
|
||||
val volatilityPercent: Double,
|
||||
val horizon: Int,
|
||||
@@ -73,10 +75,17 @@ data class SignalData(
|
||||
?: 0.0
|
||||
|
||||
val probabilityUp: Double
|
||||
get() = diagnostics.optDoubleOrNull("probability_up")
|
||||
get() = diagnostics.optDoubleOrNull("probability_take_profit_first")
|
||||
?: diagnostics.optJSONObject("forecast")?.optDoubleOrNull("probability_take_profit_first")
|
||||
?: diagnostics.optDoubleOrNull("probability_up")
|
||||
?: diagnostics.optJSONObject("forecast")?.optDoubleOrNull("probability_up")
|
||||
?: 0.0
|
||||
|
||||
val targetTransform: String
|
||||
get() = diagnostics.optString("target_transform").ifBlank {
|
||||
diagnostics.optJSONObject("forecast")?.optString("target_transform").orEmpty()
|
||||
}
|
||||
|
||||
val positionNotionalUsdt: Double
|
||||
get() = diagnostics.optDoubleOrNull("position_notional_usdt")
|
||||
?: diagnostics.optJSONObject("position_sizing")?.optDoubleOrNull("notional_usdt")
|
||||
|
||||
@@ -242,7 +242,10 @@ class TradeBotApi(
|
||||
return ForecastData(
|
||||
model = row.optStringClean("model"),
|
||||
expectedReturnPercent = row.optDouble("expected_return_percent", 0.0),
|
||||
probabilityUp = row.optDouble("probability_up", 0.0),
|
||||
probabilityUp = row.optDoubleOrNull("probability_take_profit_first")
|
||||
?: row.optDouble("probability_up", 0.0),
|
||||
probabilityTakeProfitFirst = row.optDoubleOrNull("probability_take_profit_first"),
|
||||
targetTransform = row.optStringClean("target_transform"),
|
||||
skill = row.optDouble("skill", 0.0),
|
||||
volatilityPercent = row.optDouble("volatility_percent", 0.0),
|
||||
horizon = row.optInt("horizon", 0),
|
||||
|
||||
Reference in New Issue
Block a user