Show retrain progress in Android
This commit is contained in:
@@ -10,7 +10,7 @@ android {
|
||||
applicationId = "xyz.kusoft.tradebotmonitor"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 6
|
||||
versionName = "0.2.3"
|
||||
versionCode = 7
|
||||
versionName = "0.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
+105
-24
@@ -38,6 +38,7 @@ class MainActivity : Activity() {
|
||||
private var lastError: String = ""
|
||||
private var activeTab: String = "ai"
|
||||
private var palette: AppPalette = AppPalette.dark()
|
||||
private var trainingStatusSignature: String = ""
|
||||
|
||||
private val refreshRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
@@ -103,10 +104,13 @@ class MainActivity : Activity() {
|
||||
mainHandler.post {
|
||||
val hadSnapshot = snapshot != null
|
||||
val hadError = lastError.isNotBlank()
|
||||
val newTrainingStatusSignature = trainingStatusSignature(data.retrain)
|
||||
val trainingChanged = newTrainingStatusSignature != trainingStatusSignature
|
||||
snapshot = data
|
||||
trainingStatusSignature = newTrainingStatusSignature
|
||||
lastError = ""
|
||||
ensureSelectedSymbol()
|
||||
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) {
|
||||
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged)) {
|
||||
render()
|
||||
}
|
||||
}
|
||||
@@ -116,7 +120,7 @@ class MainActivity : Activity() {
|
||||
val hadError = lastError.isNotBlank()
|
||||
lastError = error.message ?: "ошибка подключения"
|
||||
if (!silent) toast(lastError)
|
||||
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) {
|
||||
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged = false)) {
|
||||
render()
|
||||
}
|
||||
}
|
||||
@@ -124,7 +128,7 @@ class MainActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean): Boolean {
|
||||
private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean, trainingChanged: Boolean): Boolean {
|
||||
val focused = contentHost.findFocus()
|
||||
if (activeTab == "settings" && focused is EditText) {
|
||||
return false
|
||||
@@ -132,6 +136,9 @@ class MainActivity : Activity() {
|
||||
if (!silent) {
|
||||
return true
|
||||
}
|
||||
if (activeTab == "settings" && trainingChanged) {
|
||||
return true
|
||||
}
|
||||
return !hadSnapshot || hadError
|
||||
}
|
||||
|
||||
@@ -568,42 +575,71 @@ class MainActivity : Activity() {
|
||||
private fun trainingComputerPanel(retrain: JSONObject): View =
|
||||
LinearLayout(this).apply {
|
||||
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
|
||||
val activeJob = coordination.optJSONObject("active_job")
|
||||
val agentOnline = coordination.optBoolean("agent_online", false)
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Компьютер обучения", 12f, Typeface.NORMAL, palette.muted))
|
||||
addView(text(prefs.trainingComputerName, 18f, Typeface.BOLD, palette.green).top(dp(5)))
|
||||
addView(text(prefs.trainingComputerPath, 12f, Typeface.NORMAL, palette.muted).top(dp(4)))
|
||||
addView(keyValueLine("Статус", if (prefs.trainingComputerPinned) "закреплен" else "не закреплен", if (prefs.trainingComputerPinned) palette.green else palette.amber).top(dp(8)))
|
||||
addView(keyValueLine("Связь агента", if (agentOnline) "онлайн" else "нет связи", if (agentOnline) palette.green else palette.amber).top(dp(4)))
|
||||
if (activeJob != null) {
|
||||
addView(keyValueLine("Задание", activeJob.optStringClean("status").ifBlank { "активно" }).top(dp(4)))
|
||||
addView(text("Бот ставит задания через tb.kusoft.xyz, а этот Windows-agent сам забирает их через интернет и возвращает результат.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
|
||||
}
|
||||
|
||||
private fun trainingProcessPanel(coordination: JSONObject): View =
|
||||
LinearLayout(this).apply {
|
||||
val activeJob = coordination.optJSONObject("active_job")
|
||||
val latestJob = coordination.optJSONObject("latest_job")
|
||||
val job = activeJob ?: latestJob
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Процесс переобучения", 12f, Typeface.NORMAL, palette.muted))
|
||||
if (job == null) {
|
||||
val agentOnline = coordination.optBoolean("agent_online", false)
|
||||
addView(keyValueLine("Состояние", if (agentOnline) "готов к запуску" else "ждет Windows-agent", if (agentOnline) palette.green else palette.amber).top(dp(8)))
|
||||
return@apply
|
||||
}
|
||||
val status = job.optStringClean("status")
|
||||
val phase = job.optStringClean("phase")
|
||||
val progress = job.optInt("progress_percent", if (status == "completed") 100 else 0).coerceIn(0, 100)
|
||||
val message = job.optStringClean("message")
|
||||
addView(keyValueLine("Состояние", trainingJobLabel(status, phase), trainingJobColor(status, phase)).top(dp(8)))
|
||||
addView(allocationBar(progress / 100.0).top(dp(10)))
|
||||
addView(keyValueLine("Прогресс", "$progress%", trainingJobColor(status, phase)).top(dp(8)))
|
||||
if (message.isNotBlank()) {
|
||||
addView(text(message.take(160), 12f, Typeface.NORMAL, palette.text).top(dp(8)))
|
||||
}
|
||||
val completedAt = job.optStringClean("completed_at")
|
||||
if (completedAt.isNotBlank()) {
|
||||
addView(keyValueLine("Завершено", completedAt.take(19).replace("T", " ")).top(dp(8)))
|
||||
}
|
||||
val artifacts = job.optJSONArray("artifacts")
|
||||
if (artifacts != null && artifacts.length() > 0) {
|
||||
addView(keyValueLine("Артефакты", artifacts.length().toString(), palette.green).top(dp(4)))
|
||||
}
|
||||
addView(text("Переобучение Torch закреплено за этой Windows-машиной. Телефон только управляет запуском и расписанием, Pi только исполняет бота.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
|
||||
addView(actionRow(
|
||||
"Закрепить эту машину" to {
|
||||
prefs.pinDefaultTrainingComputer()
|
||||
toast("Компьютер обучения закреплен")
|
||||
render()
|
||||
},
|
||||
).top(dp(10)))
|
||||
}
|
||||
|
||||
private fun retrainSettingsBlock(retrain: JSONObject, backtest: JSONObject): View {
|
||||
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
|
||||
val activeJob = coordination.optJSONObject("active_job")
|
||||
val isTrainingActive = activeJob?.optStringClean("status") in setOf("pending", "running")
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(keyValueLine("Доступно", yesNo(retrain.optBoolean("available", false))))
|
||||
addView(keyValueLine("Принята новая модель", yesNo(retrain.optBoolean("accepted", false))).top(dp(4)))
|
||||
addView(trainingComputerPanel(retrain).top(dp(12)))
|
||||
addView(thinDivider().top(dp(12)))
|
||||
addView(trainingProcessPanel(coordination))
|
||||
if (retrain.optBoolean("available", false)) {
|
||||
addView(keyValueLine("Последний guard", if (retrain.optBoolean("accepted", false)) "модель принята" else "модель отклонена", if (retrain.optBoolean("accepted", false)) palette.green else palette.amber).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)))
|
||||
addView(actionRow(
|
||||
"Запустить сейчас" to {
|
||||
triggerRetrainNow()
|
||||
},
|
||||
).top(dp(10)))
|
||||
if (isTrainingActive) {
|
||||
addView(disabledActionButton("Обучение уже идет").top(dp(10)))
|
||||
} else {
|
||||
addView(actionRow(
|
||||
"Запустить сейчас" to {
|
||||
triggerRetrainNow()
|
||||
},
|
||||
).top(dp(10)))
|
||||
}
|
||||
addView(actionRow(
|
||||
"1ч" to { setRetrainInterval(1) },
|
||||
"3ч" to { setRetrainInterval(3) },
|
||||
@@ -842,6 +878,21 @@ class MainActivity : Activity() {
|
||||
isFocusableInTouchMode = false
|
||||
}
|
||||
|
||||
private fun disabledActionButton(title: String): TextView =
|
||||
TextView(this).apply {
|
||||
text = title
|
||||
textSize = 13f
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
gravity = Gravity.CENTER
|
||||
setTextColor(palette.muted)
|
||||
background = boxDrawable(withAlpha(palette.muted, if (palette.isDark) 14 else 8), palette.line, 7)
|
||||
isEnabled = false
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
isFocusableInTouchMode = false
|
||||
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48))
|
||||
}
|
||||
|
||||
private fun navItem(title: String, selected: Boolean, action: () -> Unit): TextView =
|
||||
TextView(this).apply {
|
||||
text = title
|
||||
@@ -957,9 +1008,15 @@ class MainActivity : Activity() {
|
||||
executor.execute {
|
||||
try {
|
||||
val message = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
|
||||
mainHandler.post { toast(message.take(180)) }
|
||||
mainHandler.post {
|
||||
toast(message.take(180))
|
||||
refreshData(silent = false)
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
mainHandler.post { toast(error.message ?: "retrain не запущен") }
|
||||
mainHandler.post {
|
||||
toast(error.message ?: "retrain не запущен")
|
||||
refreshData(silent = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,6 +1041,30 @@ class MainActivity : Activity() {
|
||||
return if (index >= 0) index else FIXED_SYMBOLS.size + 1
|
||||
}
|
||||
|
||||
private fun trainingStatusSignature(retrain: JSONObject): String =
|
||||
(retrain.optJSONObject("coordination") ?: JSONObject()).toString()
|
||||
|
||||
private fun trainingJobLabel(status: String, phase: String): String =
|
||||
when {
|
||||
status == "pending" -> "ждет Windows-agent"
|
||||
phase == "claimed" -> "задание получено"
|
||||
phase == "training" -> "идет обучение"
|
||||
phase == "guard" -> "проверка качества"
|
||||
phase == "uploading" -> "загрузка результата"
|
||||
status == "completed" -> "завершено успешно"
|
||||
status == "failed" -> "ошибка обучения"
|
||||
status == "running" -> "идет работа"
|
||||
else -> "готов к запуску"
|
||||
}
|
||||
|
||||
private fun trainingJobColor(status: String, phase: String): Int =
|
||||
when {
|
||||
status == "completed" -> palette.green
|
||||
status == "failed" -> palette.red
|
||||
status == "pending" || status == "running" || phase.isNotBlank() -> palette.amber
|
||||
else -> palette.text
|
||||
}
|
||||
|
||||
private fun normalizedAction(raw: String?): String {
|
||||
val value = raw.orEmpty().uppercase(Locale.US)
|
||||
return when {
|
||||
|
||||
Reference in New Issue
Block a user