Add global Windows training agent queue

This commit is contained in:
Codex
2026-06-27 08:56:54 +03:00
parent 98d6277d65
commit f8611a1c3f
12 changed files with 723 additions and 54 deletions
@@ -23,10 +23,6 @@ class AppPrefs(context: Context) {
get() = prefs.getString("command_token", "") ?: ""
set(value) = prefs.edit().putString("command_token", value.trim()).apply()
var retrainWebhookUrl: String
get() = prefs.getString("retrain_webhook_url", "") ?: ""
set(value) = prefs.edit().putString("retrain_webhook_url", value.trim()).apply()
var selectedSymbol: String
get() = prefs.getString("selected_symbol", "BTCUSDT") ?: "BTCUSDT"
set(value) = prefs.edit().putString("selected_symbol", value).apply()
@@ -565,13 +565,20 @@ class MainActivity : Activity() {
return section("Открытые позиции", box)
}
private fun trainingComputerPanel(): View =
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("Переобучение Torch закреплено за этой Windows-машиной. Телефон только управляет запуском и расписанием, Pi только исполняет бота.", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
addView(actionRow(
"Закрепить эту машину" to {
@@ -583,24 +590,17 @@ class MainActivity : Activity() {
}
private fun retrainSettingsBlock(retrain: JSONObject, backtest: JSONObject): View {
val webhookInput = input("URL запуска retrain на Windows", prefs.retrainWebhookUrl)
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().top(dp(12)))
addView(trainingComputerPanel(retrain).top(dp(12)))
addView(thinDivider().top(dp(12)))
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(webhookInput.top(dp(12)))
addView(actionRow(
"Сохранить URL" to {
prefs.retrainWebhookUrl = webhookInput.text.toString()
toast("URL retrain сохранен")
},
"Запустить сейчас" to {
prefs.retrainWebhookUrl = webhookInput.text.toString()
triggerRetrainNow()
},
).top(dp(10)))
@@ -956,7 +956,7 @@ class MainActivity : Activity() {
private fun triggerRetrainNow() {
executor.execute {
try {
val message = RetrainCommandClient(prefs.retrainWebhookUrl, prefs.commandToken).triggerNow()
val message = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
mainHandler.post { toast(message.take(180)) }
} catch (error: Exception) {
mainHandler.post { toast(error.message ?: "retrain не запущен") }
@@ -42,8 +42,8 @@ class RetrainAlarmReceiver : BroadcastReceiver() {
Executors.newSingleThreadExecutor().execute {
try {
val prefs = AppPrefs(context)
if (prefs.retrainScheduleEnabled && prefs.retrainWebhookUrl.isNotBlank()) {
RetrainCommandClient(prefs.retrainWebhookUrl, prefs.commandToken).triggerNow()
if (prefs.retrainScheduleEnabled) {
TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
}
} finally {
pending.finish()
@@ -52,11 +52,22 @@ class TradeBotApi(
postJson("/api/control/stop")
}
fun requestRetrain(): String {
val response = postJson("/api/training/retrain", "{\"source\":\"android\"}")
if (response.optBoolean("queued", false)) {
return "Задание переобучения отправлено на закрепленный компьютер"
}
return when (response.optStringClean("reason")) {
"active_job_exists" -> "Задание переобучения уже выполняется или ждет агента"
else -> "Задание переобучения принято ботом"
}
}
private fun getJson(path: String): JSONObject = request(path, "GET")
private fun postJson(path: String): JSONObject = request(path, "POST")
private fun postJson(path: String, body: String = "{}"): JSONObject = request(path, "POST", body)
private fun request(path: String, method: String): JSONObject {
private fun request(path: String, method: String, body: String = "{}"): JSONObject {
val root = baseUrl.trim().trimEnd('/')
val url = URL(root + path)
val connection = (url.openConnection() as HttpURLConnection).apply {
@@ -68,7 +79,7 @@ class TradeBotApi(
if (method == "POST") {
doOutput = true
setRequestProperty("Content-Type", "application/json")
outputStream.use { stream -> stream.write("{}".toByteArray(StandardCharsets.UTF_8)) }
outputStream.use { stream -> stream.write(body.toByteArray(StandardCharsets.UTF_8)) }
}
}
val code = connection.responseCode
@@ -216,35 +227,6 @@ class TradeBotApi(
}
}
class RetrainCommandClient(
private val webhookUrl: String,
private val token: String,
) {
fun triggerNow(): String {
if (webhookUrl.isBlank()) {
throw IllegalStateException("URL запуска retrain не задан")
}
val connection = (URL(webhookUrl).openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
connectTimeout = 6000
readTimeout = 15000
doOutput = true
setRequestProperty("Content-Type", "application/json")
setRequestProperty("Accept", "application/json,text/plain,*/*")
applyAuthHeaders(this, token)
outputStream.use { stream -> stream.write("{\"source\":\"android\"}".toByteArray(StandardCharsets.UTF_8)) }
}
val code = connection.responseCode
val stream = if (code in 200..299) connection.inputStream else connection.errorStream
val text = stream?.bufferedReader(StandardCharsets.UTF_8)?.use(BufferedReader::readText).orEmpty()
connection.disconnect()
if (code !in 200..299) {
throw IllegalStateException("Retrain HTTP $code: ${text.take(240)}")
}
return text.ifBlank { "Команда retrain отправлена" }
}
}
private fun applyAuthHeaders(connection: HttpURLConnection, token: String) {
val value = token.trim()
if (value.isBlank()) return