feat: production paper trading platform

This commit is contained in:
Курнат Андрей
2026-07-14 22:52:47 +03:00
parent 7186acb9a1
commit 5c4aecfe5f
29 changed files with 396 additions and 564 deletions
@@ -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"
}
}
@@ -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",
)
}
}
@@ -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>