Add global Windows training agent queue
This commit is contained in:
@@ -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-торговля
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
+10
-10
@@ -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 не запущен") }
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+14
-32
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user