feat: production paper trading platform
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
- Kelly/размер позиции: текущий размер, Kelly-цель, занятая экспозиция, остаток, множители edge/P(up)/skill.
|
||||
- Обзор equity/cash/exposure/PnL и последних решений.
|
||||
- Удалённый запуск retrain через очередь заданий на боте и закреплённый Windows-компьютер обучения.
|
||||
- Расписание retrain на телефоне: Android отправляет команду по расписанию, но обучение идёт на Windows-машине.
|
||||
- Расписание retrain через Android WorkManager: команда отправляется только при наличии сети, а обучение идёт на Windows-машине.
|
||||
- Настройки API, токена команд, тёмной/светлой темы.
|
||||
- Live-чеклист: приложение показывает, готов ли сервер к реальной торговле, и не включает live одной опасной кнопкой.
|
||||
|
||||
@@ -42,7 +42,7 @@ https://tb.kusoft.xyz
|
||||
|
||||
## Переобучение
|
||||
|
||||
Телефон не обучает модель локально. Вкладка `Обучение` ставит задание в очередь на `tb.kusoft.xyz`, а Windows-agent на закреплённой машине `SEVENHILL` (`G:\Repos\TradeBot`) сам выходит в интернет, забирает задание, обучает модель и отправляет артефакты обратно боту. Так телефон становится пультом запуска/расписания, а тяжёлый PyTorch retrain остаётся на нормальном компьютере даже если он находится в другой сети.
|
||||
Телефон не обучает модель локально. Вкладка `Обучение` ставит задание в очередь на `tb.kusoft.xyz`, а Windows-agent на этой машине сам выходит в интернет, забирает задание, обучает модель и отправляет проверенный bundle обратно боту. Имя и путь активного worker приложение получает от сервера, без прошитого имени компьютера.
|
||||
|
||||
## Live-торговля
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@ plugins {
|
||||
|
||||
android {
|
||||
namespace = "xyz.kusoft.tradebotmonitor"
|
||||
compileSdk = 36
|
||||
compileSdk = 37
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "xyz.kusoft.tradebotmonitor"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 21
|
||||
versionName = "0.4.2"
|
||||
targetSdk = 37
|
||||
versionCode = 22
|
||||
versionName = "0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.work:work-runtime:2.11.2")
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="TradeBot AI"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@drawable/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
@@ -22,16 +23,5 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".RetrainAlarmReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -18,12 +18,9 @@ class AppPrefs(context: Context) {
|
||||
}
|
||||
val trainingComputerName = prefs.getString("training_computer_name", null)?.trim()
|
||||
val trainingComputerPath = prefs.getString("training_computer_path", null)?.trim()
|
||||
if (
|
||||
trainingComputerName.isNullOrBlank() ||
|
||||
trainingComputerName == LEGACY_TRAINING_COMPUTER_NAME ||
|
||||
trainingComputerPath.isNullOrBlank() ||
|
||||
trainingComputerPath == LEGACY_TRAINING_COMPUTER_PATH
|
||||
) {
|
||||
val staleFallback = trainingComputerName in setOf("SEVENHILL", "DESKTOP-TMFDL0H") ||
|
||||
trainingComputerPath in setOf("G:\\Repos\\TradeBot", "C:\\Repos\\TradeBot")
|
||||
if (trainingComputerName.isNullOrBlank() || trainingComputerPath.isNullOrBlank() || staleFallback) {
|
||||
pinDefaultTrainingComputer()
|
||||
}
|
||||
}
|
||||
@@ -141,10 +138,8 @@ 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 = "SEVENHILL"
|
||||
const val DEFAULT_TRAINING_COMPUTER_PATH = "G:\\Repos\\TradeBot"
|
||||
const val LEGACY_TRAINING_COMPUTER_NAME = "DESKTOP-TMFDL0H"
|
||||
const val LEGACY_TRAINING_COMPUTER_PATH = "C:\\Repos\\TradeBot"
|
||||
const val DEFAULT_TRAINING_COMPUTER_NAME = "Ожидание Windows-agent"
|
||||
const val DEFAULT_TRAINING_COMPUTER_PATH = "Имя и путь поступят от сервера"
|
||||
const val TOKEN_KEY_ALIAS = "tradebot_api_auth_v1"
|
||||
}
|
||||
}
|
||||
|
||||
+5
-23
@@ -1512,12 +1512,15 @@ class MainActivity : Activity() {
|
||||
private fun rankedMarkets(data: BotSnapshot): List<MarketItem> =
|
||||
orderedMarkets(data.markets).sortedWith(
|
||||
compareByDescending<MarketItem> { marketRankScore(it, data.signalsBySymbol[it.symbol]) }
|
||||
.thenBy { fixedSymbolIndex(it.symbol) }
|
||||
.thenByDescending { it.ticker?.turnover24h ?: 0.0 }
|
||||
.thenBy { it.symbol },
|
||||
)
|
||||
|
||||
private fun orderedMarkets(markets: List<MarketItem>): List<MarketItem> =
|
||||
markets.sortedWith(compareBy({ fixedSymbolIndex(it.symbol) }, { it.symbol }))
|
||||
markets.sortedWith(
|
||||
compareByDescending<MarketItem> { it.ticker?.turnover24h ?: 0.0 }
|
||||
.thenBy { it.symbol },
|
||||
)
|
||||
|
||||
private fun marketRankScore(market: MarketItem, signal: SignalData?): Double {
|
||||
val actionScore = when (normalizedAction(signal?.action)) {
|
||||
@@ -1615,11 +1618,6 @@ class MainActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixedSymbolIndex(symbol: String): Int {
|
||||
val index = FIXED_SYMBOLS.indexOf(symbol.uppercase(Locale.US))
|
||||
return if (index >= 0) index else FIXED_SYMBOLS.size + 1
|
||||
}
|
||||
|
||||
private fun trainingStatusSignature(retrain: JSONObject): String =
|
||||
(retrain.optJSONObject("coordination") ?: JSONObject()).toString()
|
||||
|
||||
@@ -1895,20 +1893,4 @@ class MainActivity : Activity() {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val FIXED_SYMBOLS = listOf(
|
||||
"BTCUSDT",
|
||||
"ETHUSDT",
|
||||
"HYPEUSDT",
|
||||
"SOLUSDT",
|
||||
"XRPUSDT",
|
||||
"XPLUSDT",
|
||||
"WLDUSDT",
|
||||
"MNTUSDT",
|
||||
"HUSDT",
|
||||
"XAUTUSDT",
|
||||
"IPUSDT",
|
||||
"AAVEUSDT",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+37
-47
@@ -1,65 +1,55 @@
|
||||
package xyz.kusoft.tradebotmonitor
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.util.concurrent.Executors
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object RetrainScheduler {
|
||||
private const val ACTION_RETRAIN = "xyz.kusoft.tradebotmonitor.RETRAIN"
|
||||
private const val REQUEST_CODE = 6406
|
||||
private const val UNIQUE_WORK_NAME = "tradebot-periodic-retrain"
|
||||
|
||||
fun schedule(context: Context, hours: Int) {
|
||||
val interval = hours.coerceAtLeast(1) * 60L * 60L * 1000L
|
||||
val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
manager.setInexactRepeating(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
System.currentTimeMillis() + interval,
|
||||
interval,
|
||||
pendingIntent(context),
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val request = PeriodicWorkRequest.Builder(
|
||||
RetrainWorker::class.java,
|
||||
hours.coerceAtLeast(1).toLong(),
|
||||
TimeUnit.HOURS,
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
UNIQUE_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request,
|
||||
)
|
||||
}
|
||||
|
||||
fun cancel(context: Context) {
|
||||
val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
manager.cancel(pendingIntent(context))
|
||||
WorkManager.getInstance(context).cancelUniqueWork(UNIQUE_WORK_NAME)
|
||||
}
|
||||
|
||||
private fun pendingIntent(context: Context): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
REQUEST_CODE,
|
||||
Intent(context, RetrainAlarmReceiver::class.java).setAction(ACTION_RETRAIN),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
}
|
||||
|
||||
class RetrainAlarmReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val pending = goAsync()
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
executor.execute {
|
||||
try {
|
||||
val prefs = AppPrefs(context)
|
||||
if (prefs.retrainScheduleEnabled) {
|
||||
TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
|
||||
}
|
||||
} finally {
|
||||
pending.finish()
|
||||
executor.shutdown()
|
||||
}
|
||||
class RetrainWorker(
|
||||
context: Context,
|
||||
parameters: WorkerParameters,
|
||||
) : Worker(context, parameters) {
|
||||
override fun doWork(): Result {
|
||||
val prefs = AppPrefs(applicationContext)
|
||||
if (!prefs.retrainScheduleEnabled || prefs.commandToken.isBlank()) {
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
|
||||
val prefs = AppPrefs(context)
|
||||
if (prefs.retrainScheduleEnabled) {
|
||||
RetrainScheduler.schedule(context, prefs.retrainIntervalHours)
|
||||
return try {
|
||||
TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
|
||||
Result.success()
|
||||
} catch (_: Exception) {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ class TradeBotApi(
|
||||
qualityScore = quality.optDouble("score", 0.0),
|
||||
)
|
||||
}
|
||||
return output.sortedBy { it.symbol }
|
||||
return output
|
||||
}
|
||||
|
||||
private fun parseTicker(row: JSONObject): TickerData =
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup disableIfNoEncryptionCapabilities="true">
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -142,7 +142,7 @@
|
||||
<rect x="168" y="29" width="84" height="8" rx="4" fill="#151922"/>
|
||||
<text x="338" y="48" class="text small">91%</text>
|
||||
<text x="34" y="88" class="text h2">Рынки</text>
|
||||
<text x="34" y="114" class="muted small">12 фиксированных spot-пар</text>
|
||||
<text x="34" y="114" class="muted small">Динамические Bybit spot-пары</text>
|
||||
<rect x="34" y="136" width="340" height="42" rx="7" fill="#11141a" stroke="#242a36"/>
|
||||
<text x="52" y="162" class="dim small">Поиск пары или сигнала</text>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
networkTimeout=10000
|
||||
networkTimeout=60000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Reference in New Issue
Block a user