diff --git a/android/TradeBotMonitor/app/build.gradle.kts b/android/TradeBotMonitor/app/build.gradle.kts index c5deb35..e558e8d 100644 --- a/android/TradeBotMonitor/app/build.gradle.kts +++ b/android/TradeBotMonitor/app/build.gradle.kts @@ -10,7 +10,7 @@ android { applicationId = "xyz.kusoft.tradebotmonitor" minSdk = 26 targetSdk = 36 - versionCode = 4 - versionName = "0.2.1" + versionCode = 5 + versionName = "0.2.2" } } diff --git a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/AppPrefs.kt b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/AppPrefs.kt index 856df6f..aba2f56 100644 --- a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/AppPrefs.kt +++ b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/AppPrefs.kt @@ -10,6 +10,9 @@ class AppPrefs(context: Context) { if (saved.isNullOrBlank() || saved == LEGACY_PI_API_BASE_URL) { prefs.edit().putString("api_base_url", DEFAULT_API_BASE_URL).apply() } + if (prefs.getString("training_computer_name", null).isNullOrBlank()) { + pinDefaultTrainingComputer() + } } var apiBaseUrl: String @@ -40,6 +43,23 @@ class AppPrefs(context: Context) { get() = prefs.getInt("retrain_interval_hours", 6) set(value) = prefs.edit().putInt("retrain_interval_hours", value.coerceAtLeast(1)).apply() + val trainingComputerName: String + get() = prefs.getString("training_computer_name", DEFAULT_TRAINING_COMPUTER_NAME) ?: DEFAULT_TRAINING_COMPUTER_NAME + + val trainingComputerPath: String + get() = prefs.getString("training_computer_path", DEFAULT_TRAINING_COMPUTER_PATH) ?: DEFAULT_TRAINING_COMPUTER_PATH + + val trainingComputerPinned: Boolean + get() = prefs.getBoolean("training_computer_pinned", true) + + fun pinDefaultTrainingComputer() { + prefs.edit() + .putString("training_computer_name", DEFAULT_TRAINING_COMPUTER_NAME) + .putString("training_computer_path", DEFAULT_TRAINING_COMPUTER_PATH) + .putBoolean("training_computer_pinned", true) + .apply() + } + private fun normalizeBaseUrl(value: String): String { val trimmed = value.trim().trimEnd('/') if (trimmed.isBlank()) return DEFAULT_API_BASE_URL @@ -53,5 +73,7 @@ class AppPrefs(context: Context) { private companion object { const val DEFAULT_API_BASE_URL = "https://tb.kusoft.xyz" const val LEGACY_PI_API_BASE_URL = "http://192.168.0.185:8787" + const val DEFAULT_TRAINING_COMPUTER_NAME = "DESKTOP-TMFDL0H" + const val DEFAULT_TRAINING_COMPUTER_PATH = "C:\\Repos\\TradeBot" } } diff --git a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt index c1124ec..0979cf4 100644 --- a/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt +++ b/android/TradeBotMonitor/app/src/main/java/xyz/kusoft/tradebotmonitor/MainActivity.kt @@ -101,18 +101,22 @@ class MainActivity : Activity() { try { val data = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).fetchSnapshot() mainHandler.post { + val hadSnapshot = snapshot != null + val hadError = lastError.isNotBlank() snapshot = data lastError = "" ensureSelectedSymbol() - if (shouldRenderAfterRefresh()) { + if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) { render() } } } catch (error: Exception) { mainHandler.post { + val hadSnapshot = snapshot != null + val hadError = lastError.isNotBlank() lastError = error.message ?: "ошибка подключения" if (!silent) toast(lastError) - if (shouldRenderAfterRefresh()) { + if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError)) { render() } } @@ -120,9 +124,15 @@ class MainActivity : Activity() { } } - private fun shouldRenderAfterRefresh(): Boolean { + private fun shouldRenderAfterRefresh(silent: Boolean, hadSnapshot: Boolean, hadError: Boolean): Boolean { val focused = contentHost.findFocus() - return activeTab != "settings" || focused !is EditText + if (activeTab == "settings" && focused is EditText) { + return false + } + if (!silent) { + return true + } + return !hadSnapshot || hadError } private fun renderBottomNav() { @@ -555,12 +565,31 @@ class MainActivity : Activity() { return section("Открытые позиции", box) } + private fun trainingComputerPanel(): View = + LinearLayout(this).apply { + 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(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 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(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))) @@ -942,7 +971,7 @@ class MainActivity : Activity() { if (stop) api.stopBot() else api.startBot() mainHandler.post { toast(if (stop) "Команда остановки отправлена" else "Команда запуска отправлена") - refreshData(silent = true) + refreshData(silent = false) } } catch (error: Exception) { mainHandler.post { toast(error.message ?: "Команда не выполнена") }