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
+2 -2
View File
@@ -10,7 +10,7 @@
- Параметры Torch: edge, P(up), confidence, skill, quantiles, gate, причина решения.
- Kelly/размер позиции: текущий размер, Kelly-цель, занятая экспозиция, остаток, множители edge/P(up)/skill.
- Обзор equity/cash/exposure/PnL и последних решений.
- Удалённый запуск retrain через настраиваемый webhook.
- Удалённый запуск retrain через очередь заданий на боте и закреплённый Windows-компьютер обучения.
- Расписание retrain на телефоне: Android отправляет команду по расписанию, но обучение идёт на Windows-машине.
- Настройки API, токена команд, тёмной/светлой темы.
- Live-чеклист: приложение показывает, готов ли сервер к реальной торговле, и не включает live одной опасной кнопкой.
@@ -46,7 +46,7 @@ https://tb.kusoft.xyz
## Переобучение
Телефон не обучает модель локально. Вкладка `Обучение` отправляет POST на указанный webhook Windows-машины. Так телефон становится пультом запуска/расписания, а тяжёлый PyTorch retrain остаётся на нормальном компьютере.
Телефон не обучает модель локально. Вкладка `Обучение` ставит задание в очередь на `tb.kusoft.xyz`, а Windows-agent на закреплённой машине `DESKTOP-TMFDL0H` сам выходит в интернет, забирает задание, обучает модель и отправляет артефакты обратно боту. Так телефон становится пультом запуска/расписания, а тяжёлый PyTorch retrain остаётся на нормальном компьютере даже если он находится в другой сети.
## Live-торговля
+2 -2
View File
@@ -10,7 +10,7 @@ android {
applicationId = "xyz.kusoft.tradebotmonitor"
minSdk = 26
targetSdk = 36
versionCode = 5
versionName = "0.2.2"
versionCode = 6
versionName = "0.2.3"
}
}
@@ -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