feat: train forecasts on trade outcomes

This commit is contained in:
Курнат Андрей
2026-07-14 07:49:32 +03:00
parent 668e606ee2
commit 7186acb9a1
18 changed files with 867 additions and 94 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
- Русский интерфейс без bubble/pill-оформления.
- Современная биржевая компоновка: список пар, один выбранный график, компактные параметры ниже.
- Свечной график 1h: тела свечей, фитили, объём, EMA50, EMA200, последняя цена.
- Параметры Torch: edge, P(up), confidence, skill, quantiles, gate, причина решения.
- Параметры Torch: ожидаемый чистый edge, P(TP<SL) для новых торговых моделей, confidence, skill, quantiles, gate, причина решения; для старых артефактов сохраняется P(up).
- Kelly/размер позиции: текущий размер, Kelly-цель, занятая экспозиция, остаток, множители edge/P(up)/skill.
- Обзор equity/cash/exposure/PnL и последних решений.
- Удалённый запуск retrain через очередь заданий на боте и закреплённый Windows-компьютер обучения.
+2 -2
View File
@@ -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"
}
}
@@ -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),