Improve Android monitor incremental updates
This commit is contained in:
@@ -10,7 +10,7 @@ android {
|
||||
applicationId = "xyz.kusoft.tradebotmonitor"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 12
|
||||
versionName = "0.2.9"
|
||||
versionCode = 15
|
||||
versionName = "0.2.12"
|
||||
}
|
||||
}
|
||||
|
||||
+490
-65
@@ -11,10 +11,14 @@ import android.text.InputType
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.HorizontalScrollView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import org.json.JSONObject
|
||||
@@ -36,10 +40,59 @@ class MainActivity : Activity() {
|
||||
|
||||
private var snapshot: BotSnapshot? = null
|
||||
private var lastError: String = ""
|
||||
private var activeTab: String = "ai"
|
||||
private var activeTab: String = "assets"
|
||||
private var palette: AppPalette = AppPalette.dark()
|
||||
private var trainingStatusSignature: String = ""
|
||||
private var retrainRequestInFlight: Boolean = false
|
||||
private var assetsBindings: AssetsBindings? = null
|
||||
private var tradingBindings: TradingBindings? = null
|
||||
private var settingsBindings: SettingsBindings? = null
|
||||
|
||||
private data class AssetsBindings(
|
||||
var positionSignature: String,
|
||||
var closedTradesSignature: String,
|
||||
var equity: TextView? = null,
|
||||
var openPnl: TextView? = null,
|
||||
var realizedPnl: TextView? = null,
|
||||
var mode: TextView? = null,
|
||||
var cash: TextView? = null,
|
||||
var exposure: TextView? = null,
|
||||
var positionCount: TextView? = null,
|
||||
var positionsContainer: LinearLayout? = null,
|
||||
var positionsList: LinearLayout? = null,
|
||||
var closedTradesContainer: LinearLayout? = null,
|
||||
val positions: MutableMap<String, PositionBinding> = linkedMapOf(),
|
||||
)
|
||||
|
||||
private data class PositionBinding(
|
||||
var value: TextView? = null,
|
||||
var quantity: TextView? = null,
|
||||
var entryPrice: TextView? = null,
|
||||
var markPrice: TextView? = null,
|
||||
var pnl: TextView? = null,
|
||||
var takeProfit: TextView? = null,
|
||||
var highestPrice: TextView? = null,
|
||||
)
|
||||
|
||||
private data class TradingBindings(
|
||||
val symbol: String,
|
||||
var price: TextView? = null,
|
||||
var edgeLine: TextView? = null,
|
||||
var equity: TextView? = null,
|
||||
var status: TextView? = null,
|
||||
var chart: CandleChartView? = null,
|
||||
var chartTime: TextView? = null,
|
||||
var decision: TextView? = null,
|
||||
var kelly: TextView? = null,
|
||||
var reason: TextView? = null,
|
||||
)
|
||||
|
||||
private data class SettingsBindings(
|
||||
var retrainSignature: String,
|
||||
var liveSignature: String,
|
||||
var retrainContainer: LinearLayout? = null,
|
||||
var liveContainer: LinearLayout? = null,
|
||||
)
|
||||
|
||||
private val refreshRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
@@ -111,6 +164,9 @@ class MainActivity : Activity() {
|
||||
trainingStatusSignature = newTrainingStatusSignature
|
||||
lastError = ""
|
||||
ensureSelectedSymbol()
|
||||
if (silent && hadSnapshot && !hadError && updateVisibleScreen(data)) {
|
||||
return@post
|
||||
}
|
||||
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged)) {
|
||||
render()
|
||||
}
|
||||
@@ -140,16 +196,17 @@ class MainActivity : Activity() {
|
||||
if (activeTab == "settings" && trainingChanged) {
|
||||
return true
|
||||
}
|
||||
if (activeTab == "assets" || activeTab == "trading") {
|
||||
return true
|
||||
}
|
||||
return !hadSnapshot || hadError
|
||||
}
|
||||
|
||||
private fun renderBottomNav() {
|
||||
bottomNav.removeAllViews()
|
||||
listOf(
|
||||
"markets" to "Рынки",
|
||||
"ai" to "AI",
|
||||
"risk" to "Риск",
|
||||
"assets" to "Активы",
|
||||
"trading" to "Торговля",
|
||||
"settings" to "Настройки",
|
||||
).forEach { (id, title) ->
|
||||
bottomNav.addView(
|
||||
@@ -171,16 +228,122 @@ class MainActivity : Activity() {
|
||||
if (saveScroll) {
|
||||
saveCurrentScroll(tabForRender)
|
||||
}
|
||||
contentHost.removeAllViews()
|
||||
assetsBindings = null
|
||||
tradingBindings = null
|
||||
settingsBindings = null
|
||||
val view = when (activeTab) {
|
||||
"markets" -> marketsScreen()
|
||||
"risk" -> riskScreen()
|
||||
"assets" -> assetsScreen()
|
||||
"trading" -> tradingScreen()
|
||||
"settings" -> settingsScreen()
|
||||
else -> aiScreen()
|
||||
else -> assetsScreen()
|
||||
}
|
||||
val previous = contentHost.getChildAt(0)
|
||||
contentHost.addView(view)
|
||||
restoreScroll(view, tabForRender)
|
||||
if (previous != null) {
|
||||
contentHost.removeView(previous)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVisibleScreen(data: BotSnapshot): Boolean =
|
||||
when (activeTab) {
|
||||
"assets" -> updateAssetsScreen(data)
|
||||
"trading" -> updateTradingScreen(data)
|
||||
"settings" -> updateSettingsScreen(data)
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun updateAssetsScreen(data: BotSnapshot): Boolean {
|
||||
val binding = assetsBindings ?: return false
|
||||
val positionSignature = positionsSignature(data.positions)
|
||||
if (binding.positionSignature != positionSignature) {
|
||||
binding.positionsContainer?.let { container ->
|
||||
rebuildPositionsContent(container, data.positions)
|
||||
binding.positionSignature = positionSignature
|
||||
} ?: return false
|
||||
}
|
||||
val closedSignature = closedTradesSignature(data.closedTrades, data.closedTradesSummary)
|
||||
if (binding.closedTradesSignature != closedSignature) {
|
||||
binding.closedTradesContainer?.let { container ->
|
||||
container.removeAllViews()
|
||||
container.addView(closedTradesPanel(data.closedTrades, data.closedTradesSummary))
|
||||
binding.closedTradesSignature = closedSignature
|
||||
} ?: return false
|
||||
}
|
||||
|
||||
val openPnl = data.positions.sumOf { it.unrealizedPnl }
|
||||
val realizedPnl = data.closedTradesSummary.netPnl
|
||||
setText(binding.equity, money(data.account.equity))
|
||||
setText(binding.openPnl, "P&L открытых позиций ${signedMoney(openPnl)}", colorForSigned(openPnl))
|
||||
setText(binding.realizedPnl, "Прибыль закрытых сделок ${signedMoney(realizedPnl)}", colorForSigned(realizedPnl))
|
||||
setText(binding.mode, "Режим: ${modeLabel(data.mode)} · ${if (data.running) "цикл работает" else "цикл остановлен"}")
|
||||
setText(binding.cash, money(data.account.cash))
|
||||
setText(binding.exposure, money(data.account.exposure))
|
||||
setText(binding.positionCount, "Открытые позиции: ${data.positions.size}")
|
||||
|
||||
data.positions.forEach { position ->
|
||||
val row = binding.positions[positionKey(position)] ?: return@forEach
|
||||
setText(row.value, "${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", colorForSigned(position.unrealizedPnlPercent))
|
||||
setText(row.quantity, number(position.qty, 8))
|
||||
setText(row.entryPrice, price(position.entryPrice))
|
||||
setText(row.markPrice, price(position.markPrice))
|
||||
setText(row.pnl, signedMoney(position.unrealizedPnl), colorForSigned(position.unrealizedPnl))
|
||||
setText(row.takeProfit, priceOrNone(position.takeProfit))
|
||||
setText(row.highestPrice, priceOrNone(position.highestPrice))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateTradingScreen(data: BotSnapshot): Boolean {
|
||||
val binding = tradingBindings ?: return false
|
||||
if (binding.symbol != prefs.selectedSymbol) return false
|
||||
val market = selectedMarket(data) ?: return false
|
||||
if (market.symbol != binding.symbol) return false
|
||||
val signal = data.signalsBySymbol[market.symbol]
|
||||
val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
|
||||
val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
|
||||
val action = normalizedAction(signal?.action)
|
||||
val decision = when (action) {
|
||||
"BUY" -> "Покупка разрешена"
|
||||
"SELL" -> "Выход из позиции"
|
||||
else -> "Ожидание"
|
||||
}
|
||||
val reason = signal?.reason?.ifBlank { market.forecast?.reason.orEmpty() } ?: market.forecast?.reason.orEmpty()
|
||||
|
||||
setText(binding.price, price(latestPrice(market)), colorForSigned(edge))
|
||||
setText(binding.edgeLine, "Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h")
|
||||
setText(binding.equity, money(data.account.equity))
|
||||
setText(binding.status, if (data.running) "РАБОТАЕТ" else "СТОП", if (data.running) palette.green else palette.amber)
|
||||
setText(binding.decision, decision, actionColor(action))
|
||||
setText(binding.kelly, money(signal?.positionNotionalUsdt ?: 0.0))
|
||||
setText(binding.reason, reason.ifBlank { "Нет объяснения от модели" })
|
||||
binding.chart?.candles = market.candles
|
||||
setText(binding.chartTime, lastCandleTime(market))
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateSettingsScreen(data: BotSnapshot): Boolean {
|
||||
if (contentHost.findFocus() is EditText) {
|
||||
return true
|
||||
}
|
||||
val binding = settingsBindings ?: return false
|
||||
val retrainSignature = retrainBlockSignature(data)
|
||||
if (binding.retrainSignature != retrainSignature) {
|
||||
binding.retrainContainer?.let { container ->
|
||||
container.removeAllViews()
|
||||
container.addView(section("Переобучение Torch", retrainSettingsBlock(data.retrain, data.backtest)))
|
||||
binding.retrainSignature = retrainSignature
|
||||
} ?: return false
|
||||
}
|
||||
val liveSignature = liveBlockSignature(data)
|
||||
if (binding.liveSignature != liveSignature) {
|
||||
binding.liveContainer?.let { container ->
|
||||
container.removeAllViews()
|
||||
container.addView(section("Переход к live-торговле", liveSettingsBlock(data)))
|
||||
binding.liveSignature = liveSignature
|
||||
} ?: return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun saveCurrentScroll(tabId: String = activeTab) {
|
||||
@@ -199,9 +362,9 @@ class MainActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun aiScreen(): View {
|
||||
private fun tradingScreen(): View {
|
||||
val box = screenContent()
|
||||
box.addView(statusLine("AI Торговля", modeBadgeText()))
|
||||
box.addView(statusLine("Торговля", modeBadgeText()))
|
||||
val data = snapshot
|
||||
if (data == null) {
|
||||
box.addView(connectionStatePanel().top(dp(18)))
|
||||
@@ -215,11 +378,12 @@ class MainActivity : Activity() {
|
||||
}
|
||||
val signal = data.signalsBySymbol[market.symbol]
|
||||
|
||||
tradingBindings = TradingBindings(market.symbol)
|
||||
box.addView(pairSelector(data).top(dp(14)))
|
||||
box.addView(aiHero(data, market, signal).top(dp(14)))
|
||||
box.addView(chartTabs().top(dp(18)))
|
||||
box.addView(chartPanel(market).top(dp(10)))
|
||||
box.addView(decisionPanel(market, signal).top(dp(14)))
|
||||
box.addView(torchFeaturePanel(market.forecast).top(dp(14)))
|
||||
return wrapScroll(box)
|
||||
}
|
||||
|
||||
@@ -278,36 +442,43 @@ class MainActivity : Activity() {
|
||||
|
||||
val openPnl = data.positions.sumOf { it.unrealizedPnl }
|
||||
val realizedPnl = data.closedTradesSummary.netPnl
|
||||
assetsBindings = AssetsBindings(
|
||||
positionSignature = positionsSignature(data.positions),
|
||||
closedTradesSignature = closedTradesSignature(data.closedTrades, data.closedTradesSummary),
|
||||
)
|
||||
box.addView(assetHero(data, openPnl, realizedPnl).top(dp(14)))
|
||||
box.addView(twoColumnMetrics(
|
||||
"Доступно" to money(data.account.cash),
|
||||
"В позициях" to money(data.account.exposure),
|
||||
).top(dp(14)))
|
||||
box.addView(twoColumnMetrics(
|
||||
"Закрытые сделки" to signedMoney(realizedPnl),
|
||||
"Открытый P&L" to signedMoney(openPnl),
|
||||
).top(dp(14)))
|
||||
box.addView(accountMetrics(data).top(dp(14)))
|
||||
box.addView(positionsPanel(data.positions).top(dp(16)))
|
||||
box.addView(closedTradesPanel(data.closedTrades, data.closedTradesSummary).top(dp(16)))
|
||||
val closedTradesHost = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
assetsBindings?.closedTradesContainer = closedTradesHost
|
||||
closedTradesHost.addView(closedTradesPanel(data.closedTrades, data.closedTradesSummary))
|
||||
box.addView(closedTradesHost.top(dp(16)))
|
||||
return wrapScroll(box)
|
||||
}
|
||||
|
||||
private fun settingsScreen(): View {
|
||||
val box = screenContent()
|
||||
box.addView(statusLine("Настройки", prefs.apiBaseUrl))
|
||||
settingsBindings = SettingsBindings(
|
||||
retrainSignature = retrainBlockSignature(snapshot),
|
||||
liveSignature = liveBlockSignature(snapshot),
|
||||
)
|
||||
|
||||
val (savedLogin, savedPassword) = authParts(prefs.commandToken)
|
||||
val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
|
||||
val tokenInput = input("API auth: token, Bearer, Basic или login:password", prefs.commandToken).apply {
|
||||
val loginInput = input("Логин API", savedLogin)
|
||||
val passwordInput = input("Пароль API", savedPassword).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
box.addView(section("Подключение", LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(apiInput)
|
||||
addView(tokenInput.top(dp(8)))
|
||||
addView(loginInput.top(dp(8)))
|
||||
addView(passwordInput.top(dp(8)))
|
||||
addView(actionRow(
|
||||
"Сохранить" to {
|
||||
prefs.apiBaseUrl = apiInput.text.toString()
|
||||
prefs.commandToken = tokenInput.text.toString()
|
||||
prefs.commandToken = authToken(loginInput.text.toString(), passwordInput.text.toString())
|
||||
toast("Подключение сохранено")
|
||||
refreshData(silent = false)
|
||||
},
|
||||
@@ -330,8 +501,15 @@ class MainActivity : Activity() {
|
||||
},
|
||||
)).top(dp(14)))
|
||||
|
||||
box.addView(section("Переобучение Torch", retrainSettingsBlock(snapshot?.retrain ?: JSONObject(), snapshot?.backtest ?: JSONObject())).top(dp(14)))
|
||||
box.addView(section("Переход к live-торговле", liveSettingsBlock(snapshot)).top(dp(14)))
|
||||
val retrainHost = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
settingsBindings?.retrainContainer = retrainHost
|
||||
retrainHost.addView(section("Переобучение Torch", retrainSettingsBlock(snapshot?.retrain ?: JSONObject(), snapshot?.backtest ?: JSONObject())))
|
||||
box.addView(retrainHost.top(dp(14)))
|
||||
|
||||
val liveHost = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
settingsBindings?.liveContainer = liveHost
|
||||
liveHost.addView(section("Переход к live-торговле", liveSettingsBlock(snapshot)))
|
||||
box.addView(liveHost.top(dp(14)))
|
||||
return wrapScroll(box)
|
||||
}
|
||||
|
||||
@@ -347,18 +525,26 @@ class MainActivity : Activity() {
|
||||
top.addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text(market.symbol, 20f, Typeface.BOLD))
|
||||
addView(text(price(latestPrice(market)), 32f, Typeface.BOLD, colorForSigned(signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0)).top(dp(6)))
|
||||
val priceText = text(price(latestPrice(market)), 32f, Typeface.BOLD, colorForSigned(signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0))
|
||||
tradingBindings?.price = priceText
|
||||
addView(priceText.top(dp(6)))
|
||||
val edge = signal?.expectedReturnPercent ?: market.forecast?.expectedReturnPercent ?: 0.0
|
||||
val probability = signal?.probabilityUp ?: market.forecast?.probabilityUp ?: 0.0
|
||||
addView(text("Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h", 13f, Typeface.NORMAL, palette.muted).top(dp(2)))
|
||||
val edgeLine = text("Edge ${signedPercent(edge, 2)} · P(up) ${probability(probability)} · 1h", 13f, Typeface.NORMAL, palette.muted)
|
||||
tradingBindings?.edgeLine = edgeLine
|
||||
addView(edgeLine.top(dp(2)))
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
|
||||
top.addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.END
|
||||
addView(text("Equity", 12f, Typeface.NORMAL, palette.muted))
|
||||
addView(text(money(data.account.equity), 18f, Typeface.BOLD).top(dp(5)))
|
||||
addView(statusBadge(if (data.running) "Работает" else "Стоп", if (data.running) "ok" else "warn").top(dp(8)))
|
||||
val equityText = text(money(data.account.equity), 18f, Typeface.BOLD)
|
||||
tradingBindings?.equity = equityText
|
||||
addView(equityText.top(dp(5)))
|
||||
val statusText = statusBadge(if (data.running) "Работает" else "Стоп", if (data.running) "ok" else "warn")
|
||||
tradingBindings?.status = statusText
|
||||
addView(statusText.top(dp(8)))
|
||||
}, LinearLayout.LayoutParams(dp(132), ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
|
||||
addView(top)
|
||||
@@ -374,7 +560,6 @@ class MainActivity : Activity() {
|
||||
setPadding(0, dp(12), 0, dp(6))
|
||||
addView(tabText("График", true))
|
||||
addView(tabText("Torch", false))
|
||||
addView(tabText("Риск", false))
|
||||
addView(tabText("История", false))
|
||||
})
|
||||
}
|
||||
@@ -387,18 +572,67 @@ class MainActivity : Activity() {
|
||||
palette = this@MainActivity.palette
|
||||
candles = market.candles
|
||||
}
|
||||
tradingBindings?.chart = chart
|
||||
addView(chart, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(278)))
|
||||
addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
setPadding(0, dp(4), 0, 0)
|
||||
addView(text("EMA50", 11f, Typeface.NORMAL, palette.muted))
|
||||
addView(text("EMA200", 11f, Typeface.NORMAL, palette.amber).left(dp(16)))
|
||||
addView(text(lastCandleTime(market), 11f, Typeface.NORMAL, palette.muted).apply {
|
||||
val timeText = text(lastCandleTime(market), 11f, Typeface.NORMAL, palette.muted).apply {
|
||||
gravity = Gravity.END
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
}
|
||||
tradingBindings?.chartTime = timeText
|
||||
addView(timeText, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
})
|
||||
}
|
||||
|
||||
private fun pairSelector(data: BotSnapshot): View {
|
||||
val symbols = orderedMarkets(data.markets).map { it.symbol }
|
||||
val selectedIndex = symbols.indexOf(prefs.selectedSymbol).coerceAtLeast(0)
|
||||
val adapter = object : ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, symbols) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getView(position, convertView, parent) as TextView
|
||||
view.setTextColor(palette.text)
|
||||
view.textSize = 15f
|
||||
view.typeface = Typeface.DEFAULT_BOLD
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getDropDownView(position, convertView, parent) as TextView
|
||||
view.setTextColor(palette.text)
|
||||
view.setBackgroundColor(palette.panel)
|
||||
view.textSize = 15f
|
||||
view.setPadding(dp(14), dp(12), dp(14), dp(12))
|
||||
return view
|
||||
}
|
||||
}.apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Торговая пара", 12f, Typeface.NORMAL, palette.muted))
|
||||
addView(Spinner(this@MainActivity).apply {
|
||||
this.adapter = adapter
|
||||
setSelection(selectedIndex, false)
|
||||
background = boxDrawable(palette.panel, palette.line, 8)
|
||||
setPadding(dp(10), 0, dp(10), 0)
|
||||
onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
val symbol = symbols.getOrNull(position) ?: return
|
||||
if (symbol != prefs.selectedSymbol) {
|
||||
prefs.selectedSymbol = symbol
|
||||
render(saveScroll = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
}
|
||||
}, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(48)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun decisionPanel(market: MarketItem, signal: SignalData?): View {
|
||||
val action = normalizedAction(signal?.action)
|
||||
val color = actionColor(action)
|
||||
@@ -416,17 +650,21 @@ class MainActivity : Activity() {
|
||||
addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Решение Torch", 12f, Typeface.NORMAL, palette.muted))
|
||||
addView(text(decision, 20f, Typeface.BOLD, color).top(dp(8)))
|
||||
val decisionText = text(decision, 20f, Typeface.BOLD, color)
|
||||
tradingBindings?.decision = decisionText
|
||||
addView(decisionText.top(dp(8)))
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.END
|
||||
addView(text("Kelly", 12f, Typeface.NORMAL, palette.muted))
|
||||
addView(text(money(signal?.positionNotionalUsdt ?: 0.0), 20f, Typeface.BOLD).top(dp(8)))
|
||||
val kellyText = text(money(signal?.positionNotionalUsdt ?: 0.0), 20f, Typeface.BOLD)
|
||||
tradingBindings?.kelly = kellyText
|
||||
addView(kellyText.top(dp(8)))
|
||||
}, LinearLayout.LayoutParams(dp(120), ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
})
|
||||
addView(thinDivider().top(dp(12)))
|
||||
addView(keyValueLine("Причина", reason.ifBlank { "Нет объяснения от модели" }).top(dp(8)))
|
||||
addView(boundKeyValueLine("Причина", reason.ifBlank { "Нет объяснения от модели" }) { tradingBindings?.reason = it }.top(dp(8)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -457,7 +695,7 @@ class MainActivity : Activity() {
|
||||
background = if (selected) boxDrawable(palette.panelAlt, Color.TRANSPARENT, 2) else null
|
||||
setOnClickListener {
|
||||
prefs.selectedSymbol = market.symbol
|
||||
activeTab = "ai"
|
||||
activeTab = "trading"
|
||||
renderBottomNav()
|
||||
render(saveScroll = false)
|
||||
}
|
||||
@@ -556,33 +794,116 @@ class MainActivity : Activity() {
|
||||
LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Общие активы", 13f, Typeface.NORMAL, palette.muted))
|
||||
addView(text(money(data.account.equity), 34f, Typeface.BOLD).top(dp(8)))
|
||||
addView(text("P&L открытых позиций ${signedMoney(pnl)}", 15f, Typeface.NORMAL, colorForSigned(pnl)).top(dp(8)))
|
||||
addView(text("Прибыль закрытых сделок ${signedMoney(realizedPnl)}", 15f, Typeface.NORMAL, colorForSigned(realizedPnl)).top(dp(5)))
|
||||
addView(text("Режим: ${modeLabel(data.mode)} · ${if (data.running) "цикл работает" else "цикл остановлен"}", 12f, Typeface.NORMAL, palette.muted).top(dp(8)))
|
||||
val equityText = text(money(data.account.equity), 34f, Typeface.BOLD)
|
||||
assetsBindings?.equity = equityText
|
||||
addView(equityText.top(dp(8)))
|
||||
val openPnlText = text("P&L открытых позиций ${signedMoney(pnl)}", 15f, Typeface.NORMAL, colorForSigned(pnl))
|
||||
assetsBindings?.openPnl = openPnlText
|
||||
addView(openPnlText.top(dp(8)))
|
||||
val realizedPnlText = text("Прибыль закрытых сделок ${signedMoney(realizedPnl)}", 15f, Typeface.NORMAL, colorForSigned(realizedPnl))
|
||||
assetsBindings?.realizedPnl = realizedPnlText
|
||||
addView(realizedPnlText.top(dp(5)))
|
||||
val modeText = text("Режим: ${modeLabel(data.mode)} · ${if (data.running) "цикл работает" else "цикл остановлен"}", 12f, Typeface.NORMAL, palette.muted)
|
||||
assetsBindings?.mode = modeText
|
||||
addView(modeText.top(dp(8)))
|
||||
}
|
||||
|
||||
private fun accountMetrics(data: BotSnapshot): View =
|
||||
LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
addView(metricCell("Доступно", money(data.account.cash)).also { cell ->
|
||||
assetsBindings?.cash = cell.findViewWithTag<TextView>("metric_value")
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { setMargins(0, 0, dp(6), 0) })
|
||||
addView(metricCell("В позициях", money(data.account.exposure)).also { cell ->
|
||||
assetsBindings?.exposure = cell.findViewWithTag<TextView>("metric_value")
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { setMargins(dp(6), 0, 0, 0) })
|
||||
}
|
||||
|
||||
private fun positionsPanel(positions: List<PositionData>): View {
|
||||
if (positions.isEmpty()) {
|
||||
return section("Открытые позиции", text("Открытых позиций нет", 13f, Typeface.NORMAL, palette.muted))
|
||||
val body = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
val title = text("Открытые позиции: ${positions.size}", 18f, Typeface.BOLD)
|
||||
assetsBindings?.positionCount = title
|
||||
addView(title)
|
||||
val container = LinearLayout(this@MainActivity).apply { orientation = LinearLayout.VERTICAL }
|
||||
assetsBindings?.positionsContainer = container
|
||||
rebuildPositionsContent(container, positions)
|
||||
addView(container.top(dp(12)))
|
||||
}
|
||||
val box = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
positions.forEachIndexed { index, position ->
|
||||
if (index > 0) box.addView(thinDivider().top(dp(8)))
|
||||
box.addView(LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(keyValueLine(position.symbol, "${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", colorForSigned(position.unrealizedPnlPercent)))
|
||||
addView(keyValueLine("Вход", price(position.entryPrice)).top(dp(4)))
|
||||
addView(keyValueLine("Текущая", price(position.markPrice)).top(dp(4)))
|
||||
addView(keyValueLine("PnL", signedMoney(position.unrealizedPnl), colorForSigned(position.unrealizedPnl)).top(dp(4)))
|
||||
addView(keyValueLine("Take-profit", priceOrNone(position.takeProfit)).top(dp(4)))
|
||||
addView(keyValueLine("Защитный выход", protectiveExitText(position), protectiveExitColor(position)).top(dp(4)))
|
||||
addView(keyValueLine("Сигнал выхода", exitSignalText(position), actionColor(normalizedAction(position.exitAction))).top(dp(4)))
|
||||
}.top(dp(8)))
|
||||
}
|
||||
return section("Открытые позиции", box)
|
||||
return section(null, body)
|
||||
}
|
||||
|
||||
private fun rebuildPositionsContent(container: LinearLayout, positions: List<PositionData>) {
|
||||
container.removeAllViews()
|
||||
assetsBindings?.positions?.clear()
|
||||
assetsBindings?.positionsList = null
|
||||
if (positions.isEmpty()) {
|
||||
container.addView(text("Открытых позиций нет", 13f, Typeface.NORMAL, palette.muted))
|
||||
return
|
||||
}
|
||||
val cardWidth = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260))
|
||||
val list = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
setPadding(0, 0, dp(10), 0)
|
||||
}
|
||||
assetsBindings?.positionsList = list
|
||||
rebuildPositionsList(list, positions, cardWidth)
|
||||
val scroller = HorizontalScrollView(this).apply {
|
||||
isHorizontalScrollBarEnabled = false
|
||||
overScrollMode = View.OVER_SCROLL_NEVER
|
||||
addView(list, ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
container.addView(scroller)
|
||||
}
|
||||
|
||||
private fun rebuildPositionsList(
|
||||
list: LinearLayout,
|
||||
positions: List<PositionData>,
|
||||
cardWidth: Int = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260)),
|
||||
) {
|
||||
list.removeAllViews()
|
||||
positions.forEachIndexed { index, position ->
|
||||
list.addView(positionCard(position, index + 1, positions.size), LinearLayout.LayoutParams(cardWidth, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
if (index < positions.lastIndex) setMargins(0, 0, dp(12), 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun positionCard(position: PositionData, index: Int, total: Int): View =
|
||||
LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(dp(14), dp(13), dp(14), dp(13))
|
||||
background = boxDrawable(palette.panelAlt, palette.line, 8)
|
||||
addView(LinearLayout(this@MainActivity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
addView(text(position.symbol, 20f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
addView(text("$index/$total", 12f, Typeface.BOLD, palette.muted))
|
||||
})
|
||||
val valueText = text("${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", 15f, Typeface.BOLD, colorForSigned(position.unrealizedPnlPercent))
|
||||
addView(valueText.top(dp(8)))
|
||||
addView(thinDivider().top(dp(12)))
|
||||
addView(boundKeyValueLine("Количество", number(position.qty, 8)) { textView ->
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.quantity = textView
|
||||
}.top(dp(10)))
|
||||
addView(boundKeyValueLine("Вход", price(position.entryPrice)) { textView ->
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.entryPrice = textView
|
||||
}.top(dp(5)))
|
||||
addView(boundKeyValueLine("Текущая", price(position.markPrice)) { textView ->
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.markPrice = textView
|
||||
}.top(dp(5)))
|
||||
addView(boundKeyValueLine("PnL", signedMoney(position.unrealizedPnl), colorForSigned(position.unrealizedPnl)) { textView ->
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.pnl = textView
|
||||
}.top(dp(5)))
|
||||
addView(boundKeyValueLine("Take-profit", priceOrNone(position.takeProfit)) { textView ->
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.takeProfit = textView
|
||||
}.top(dp(5)))
|
||||
addView(boundKeyValueLine("Максимум", priceOrNone(position.highestPrice)) { textView ->
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.highestPrice = textView
|
||||
}.top(dp(5)))
|
||||
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.value = valueText
|
||||
}
|
||||
|
||||
private fun closedTradesPanel(trades: List<ClosedTradeData>, summary: ClosedTradesSummary): View {
|
||||
val box = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
box.addView(keyValueLine("Всего закрыто", summary.trades.toString()))
|
||||
@@ -717,13 +1038,17 @@ class MainActivity : Activity() {
|
||||
prefs.retrainScheduleEnabled = true
|
||||
RetrainScheduler.schedule(this@MainActivity, prefs.retrainIntervalHours)
|
||||
toast("Расписание retrain включено")
|
||||
render()
|
||||
if (activeTab != "settings" || snapshot == null || !updateSettingsScreen(snapshot!!)) {
|
||||
render()
|
||||
}
|
||||
},
|
||||
"Отключить" to {
|
||||
prefs.retrainScheduleEnabled = false
|
||||
RetrainScheduler.cancel(this@MainActivity)
|
||||
toast("Расписание retrain отключено")
|
||||
render()
|
||||
if (activeTab != "settings" || snapshot == null || !updateSettingsScreen(snapshot!!)) {
|
||||
render()
|
||||
}
|
||||
},
|
||||
).top(dp(10)))
|
||||
}
|
||||
@@ -785,7 +1110,9 @@ class MainActivity : Activity() {
|
||||
setPadding(dp(14), dp(12), dp(14), dp(12))
|
||||
background = boxDrawable(palette.panel, palette.line, 8)
|
||||
addView(text(title, 12f, Typeface.NORMAL, palette.muted))
|
||||
addView(text(value, 17f, Typeface.BOLD, colorForSigned(value)).top(dp(6)))
|
||||
addView(text(value, 17f, Typeface.BOLD, colorForSigned(value)).apply {
|
||||
tag = "metric_value"
|
||||
}.top(dp(6)))
|
||||
}
|
||||
|
||||
private fun tableHeader(columns: List<Pair<String, Float>>): View =
|
||||
@@ -833,19 +1160,22 @@ class MainActivity : Activity() {
|
||||
if (!isAuthError()) {
|
||||
return emptyState("Нет данных от API. ${lastError.ifBlank { "Проверь подключение." }}")
|
||||
}
|
||||
val (savedLogin, savedPassword) = authParts(prefs.commandToken)
|
||||
val apiInput = input("Адрес API бота", prefs.apiBaseUrl)
|
||||
val authInput = input("Логин и пароль: login:password", prefs.commandToken).apply {
|
||||
val loginInput = input("Логин API", savedLogin)
|
||||
val passwordInput = input("Пароль API", savedPassword).apply {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
return section("Нужен вход в API", LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(text("Сервер tb.kusoft.xyz отвечает 401, значит он доступен, но требует авторизацию. Введите доступ в формате login:password, приложение само отправит Basic Auth.", 13f, Typeface.NORMAL, palette.muted))
|
||||
addView(text("Сервер tb.kusoft.xyz отвечает 401, значит он доступен, но требует авторизацию. Введите логин и пароль, приложение само отправит Basic Auth.", 13f, Typeface.NORMAL, palette.muted))
|
||||
addView(apiInput.top(dp(12)))
|
||||
addView(authInput.top(dp(8)))
|
||||
addView(loginInput.top(dp(8)))
|
||||
addView(passwordInput.top(dp(8)))
|
||||
addView(actionRow(
|
||||
"Подключиться" to {
|
||||
prefs.apiBaseUrl = apiInput.text.toString()
|
||||
prefs.commandToken = authInput.text.toString()
|
||||
prefs.commandToken = authToken(loginInput.text.toString(), passwordInput.text.toString())
|
||||
toast("Доступ сохранен, проверяю API")
|
||||
refreshData(silent = false)
|
||||
},
|
||||
@@ -932,6 +1262,28 @@ class MainActivity : Activity() {
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.05f))
|
||||
}
|
||||
|
||||
private fun boundKeyValueLine(key: String, value: String, valueColor: Int = colorForSigned(value), bindValue: (TextView) -> Unit): View =
|
||||
LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
addView(text(key, 13f, Typeface.NORMAL, palette.muted), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
val valueText = text(value.ifBlank { "нет данных" }, 13f, Typeface.BOLD, valueColor).apply {
|
||||
gravity = Gravity.END
|
||||
}
|
||||
bindValue(valueText)
|
||||
addView(valueText, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.05f))
|
||||
}
|
||||
|
||||
private fun setText(view: TextView?, value: String, color: Int? = null) {
|
||||
if (view == null) return
|
||||
if (view.text.toString() != value) {
|
||||
view.text = value
|
||||
}
|
||||
if (color != null) {
|
||||
view.setTextColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
private fun allocationBar(fractionRaw: Double): View {
|
||||
val fraction = fractionRaw.coerceIn(0.0, 1.0).toFloat()
|
||||
return LinearLayout(this).apply {
|
||||
@@ -1107,13 +1459,18 @@ class MainActivity : Activity() {
|
||||
RetrainScheduler.schedule(this, hours)
|
||||
}
|
||||
toast("Интервал retrain: $hours ч")
|
||||
if (activeTab == "settings" && snapshot != null && updateSettingsScreen(snapshot!!)) {
|
||||
return
|
||||
}
|
||||
render()
|
||||
}
|
||||
|
||||
private fun triggerRetrainNow() {
|
||||
if (retrainRequestInFlight) return
|
||||
retrainRequestInFlight = true
|
||||
render()
|
||||
if (activeTab != "settings" || snapshot == null || !updateSettingsScreen(snapshot!!)) {
|
||||
render()
|
||||
}
|
||||
executor.execute {
|
||||
try {
|
||||
val message = TradeBotApi(prefs.apiBaseUrl, prefs.commandToken).requestRetrain()
|
||||
@@ -1155,6 +1512,53 @@ class MainActivity : Activity() {
|
||||
private fun trainingStatusSignature(retrain: JSONObject): String =
|
||||
(retrain.optJSONObject("coordination") ?: JSONObject()).toString()
|
||||
|
||||
private fun retrainBlockSignature(data: BotSnapshot?): String =
|
||||
listOf(
|
||||
retrainRequestInFlight.toString(),
|
||||
prefs.retrainScheduleEnabled.toString(),
|
||||
prefs.retrainIntervalHours.toString(),
|
||||
data?.retrain?.toString().orEmpty(),
|
||||
data?.backtest?.optJSONObject("full_replay")?.toString().orEmpty(),
|
||||
).joinToString("|")
|
||||
|
||||
private fun liveBlockSignature(data: BotSnapshot?): String {
|
||||
val config = data?.config ?: JSONObject()
|
||||
return listOf(
|
||||
data?.mode.orEmpty(),
|
||||
data?.running?.toString().orEmpty(),
|
||||
config.optBoolean("live_ready", false).toString(),
|
||||
config.optDouble("live_order_max_usdt", 0.0).toString(),
|
||||
config.optDouble("risk_per_trade_percent", 0.0).toString(),
|
||||
).joinToString("|")
|
||||
}
|
||||
|
||||
private fun positionsSignature(positions: List<PositionData>): String =
|
||||
positions.joinToString("|") { positionKey(it) }
|
||||
|
||||
private fun positionKey(position: PositionData): String =
|
||||
when {
|
||||
position.id != null && position.id > 0L -> "id:${position.id}"
|
||||
position.openedAt.isNotBlank() -> "${position.symbol}:${position.openedAt}"
|
||||
else -> position.symbol
|
||||
}
|
||||
|
||||
private fun closedTradesSignature(trades: List<ClosedTradeData>, summary: ClosedTradesSummary): String =
|
||||
listOf(
|
||||
summary.trades.toString(),
|
||||
number(summary.netPnl, 8),
|
||||
number(summary.feeUsdt, 8),
|
||||
number(summary.winRate, 8),
|
||||
trades.take(10).joinToString(";") { trade ->
|
||||
listOf(
|
||||
trade.closedAt,
|
||||
trade.symbol,
|
||||
number(trade.qty, 8),
|
||||
priceOrNone(trade.exitPrice),
|
||||
number(trade.netPnl, 8),
|
||||
).joinToString(",")
|
||||
},
|
||||
).joinToString("|")
|
||||
|
||||
private fun trainingJobLabel(status: String, phase: String): String =
|
||||
when (status) {
|
||||
"pending" -> "ждет Windows-agent"
|
||||
@@ -1213,6 +1617,27 @@ class MainActivity : Activity() {
|
||||
else -> value.ifBlank { "нет данных" }
|
||||
}
|
||||
|
||||
private fun authParts(value: String): Pair<String, String> {
|
||||
val trimmed = value.trim()
|
||||
if (trimmed.isBlank() || trimmed.startsWith("Bearer ", ignoreCase = true)) {
|
||||
return "" to ""
|
||||
}
|
||||
val basicPrefix = "Basic "
|
||||
val raw = if (trimmed.startsWith(basicPrefix, ignoreCase = true)) "" else trimmed
|
||||
val separator = raw.indexOf(':')
|
||||
return if (separator >= 0) {
|
||||
raw.substring(0, separator) to raw.substring(separator + 1)
|
||||
} else {
|
||||
raw to ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun authToken(login: String, password: String): String {
|
||||
val cleanLogin = login.trim()
|
||||
val cleanPassword = password.trim()
|
||||
return if (cleanLogin.isBlank() && cleanPassword.isBlank()) "" else "$cleanLogin:$cleanPassword"
|
||||
}
|
||||
|
||||
private fun modelLabel(value: String): String =
|
||||
when {
|
||||
value.contains("gru", ignoreCase = true) -> "PyTorch GRU"
|
||||
|
||||
@@ -84,6 +84,7 @@ data class SignalData(
|
||||
}
|
||||
|
||||
data class PositionData(
|
||||
val id: Long?,
|
||||
val symbol: String,
|
||||
val qty: Double,
|
||||
val entryPrice: Double,
|
||||
@@ -97,6 +98,7 @@ data class PositionData(
|
||||
val highestPrice: Double?,
|
||||
val trailingStop: Double?,
|
||||
val atrTrailingStop: Double?,
|
||||
val openedAt: String,
|
||||
val exitAction: String,
|
||||
val exitReason: String,
|
||||
val stopLossExitEnabled: Boolean,
|
||||
@@ -153,5 +155,8 @@ fun JSONObject.optStringClean(name: String): String =
|
||||
fun JSONObject.optDoubleOrNull(name: String): Double? =
|
||||
if (has(name) && !isNull(name)) optDouble(name) else null
|
||||
|
||||
fun JSONObject.optLongOrNull(name: String): Long? =
|
||||
if (has(name) && !isNull(name)) optLong(name) else null
|
||||
|
||||
fun JSONObject.optBooleanOrNull(name: String): Boolean? =
|
||||
if (has(name) && !isNull(name)) optBoolean(name) else null
|
||||
|
||||
@@ -104,6 +104,7 @@ class TradeBotApi(
|
||||
val row = items.optJSONObject(index) ?: continue
|
||||
val exitPlan = row.optJSONObject("exit_plan") ?: JSONObject()
|
||||
output += PositionData(
|
||||
id = row.optLongOrNull("id"),
|
||||
symbol = row.optStringClean("symbol"),
|
||||
qty = row.optDouble("qty", 0.0),
|
||||
entryPrice = row.optDouble("entry_price", 0.0),
|
||||
@@ -117,6 +118,7 @@ class TradeBotApi(
|
||||
highestPrice = exitPlan.optDoubleOrNull("highest_price") ?: row.optDoubleOrNull("highest_price"),
|
||||
trailingStop = exitPlan.optDoubleOrNull("trailing_stop"),
|
||||
atrTrailingStop = exitPlan.optDoubleOrNull("atr_trailing_stop"),
|
||||
openedAt = row.optStringClean("opened_at"),
|
||||
exitAction = exitPlan.optStringClean("action"),
|
||||
exitReason = exitPlan.optStringClean("reason"),
|
||||
stopLossExitEnabled = exitPlan.optBooleanOrNull("stop_loss_exit_enabled") ?: true,
|
||||
|
||||
Reference in New Issue
Block a user