Show realized trade PnL in Android app
This commit is contained in:
@@ -10,7 +10,7 @@ android {
|
||||
applicationId = "xyz.kusoft.tradebotmonitor"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 9
|
||||
versionName = "0.2.6"
|
||||
versionCode = 10
|
||||
versionName = "0.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
+83
-3
@@ -275,13 +275,19 @@ class MainActivity : Activity() {
|
||||
return wrapScroll(box)
|
||||
}
|
||||
|
||||
val pnl = data.positions.sumOf { it.unrealizedPnl }
|
||||
box.addView(assetHero(data, pnl).top(dp(14)))
|
||||
val openPnl = data.positions.sumOf { it.unrealizedPnl }
|
||||
val realizedPnl = data.closedTradesSummary.netPnl
|
||||
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(positionsPanel(data.positions).top(dp(16)))
|
||||
box.addView(closedTradesPanel(data.closedTrades, data.closedTradesSummary).top(dp(16)))
|
||||
return wrapScroll(box)
|
||||
}
|
||||
|
||||
@@ -545,12 +551,13 @@ class MainActivity : Activity() {
|
||||
"Запустить цикл" to { sendControl(stop = false) },
|
||||
)
|
||||
|
||||
private fun assetHero(data: BotSnapshot, pnl: Double): View =
|
||||
private fun assetHero(data: BotSnapshot, pnl: Double, realizedPnl: Double): View =
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -567,11 +574,45 @@ class MainActivity : Activity() {
|
||||
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)
|
||||
}
|
||||
|
||||
private fun closedTradesPanel(trades: List<ClosedTradeData>, summary: ClosedTradesSummary): View {
|
||||
val box = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
box.addView(keyValueLine("Всего закрыто", summary.trades.toString()))
|
||||
box.addView(keyValueLine("Реализованный итог", signedMoney(summary.netPnl), colorForSigned(summary.netPnl)).top(dp(4)))
|
||||
box.addView(keyValueLine("Комиссии", money(summary.feeUsdt), palette.muted).top(dp(4)))
|
||||
box.addView(keyValueLine("Win-rate", percent(summary.winRate * 100.0, 1)).top(dp(4)))
|
||||
if (trades.isEmpty()) {
|
||||
box.addView(text("Закрытых сделок пока нет. Рост equity выше 100 USDT сейчас может быть только плавающей оценкой открытых позиций.", 13f, Typeface.NORMAL, palette.muted).top(dp(12)))
|
||||
return section("Последние 10 закрытых сделок", box)
|
||||
}
|
||||
box.addView(tableHeader(listOf("Время" to 1.05f, "Пара" to 0.9f, "Выход" to 0.9f, "Итог" to 0.9f)).top(dp(12)))
|
||||
trades.take(10).forEach { trade ->
|
||||
box.addView(closedTradeRow(trade).top(dp(7)))
|
||||
}
|
||||
return section("Последние 10 закрытых сделок", box)
|
||||
}
|
||||
|
||||
private fun closedTradeRow(trade: ClosedTradeData): View =
|
||||
LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
addView(text(shortIsoTime(trade.closedAt), 12f, Typeface.NORMAL, palette.muted), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.05f))
|
||||
addView(text(trade.symbol, 12f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
|
||||
addView(text(priceOrNone(trade.exitPrice), 12f, Typeface.NORMAL, palette.text).apply {
|
||||
gravity = Gravity.END
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
|
||||
addView(text(signedMoney(trade.netPnl), 12f, Typeface.BOLD, colorForSigned(trade.netPnl)).apply {
|
||||
gravity = Gravity.END
|
||||
}, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 0.9f))
|
||||
}
|
||||
|
||||
private fun trainingComputerPanel(retrain: JSONObject): View =
|
||||
LinearLayout(this).apply {
|
||||
val coordination = retrain.optJSONObject("coordination") ?: JSONObject()
|
||||
@@ -1039,6 +1080,17 @@ class MainActivity : Activity() {
|
||||
return SimpleDateFormat("dd.MM HH:mm", Locale("ru", "RU")).format(Date(millis))
|
||||
}
|
||||
|
||||
private fun shortIsoTime(value: String): String {
|
||||
if (value.isBlank()) return "-"
|
||||
return try {
|
||||
val normalized = value.replace("Z", "+00:00")
|
||||
val millis = java.time.OffsetDateTime.parse(normalized).toInstant().toEpochMilli()
|
||||
SimpleDateFormat("dd.MM HH:mm", Locale("ru", "RU")).format(Date(millis))
|
||||
} catch (_: Exception) {
|
||||
value.take(16).replace("T", " ")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRetrainInterval(hours: Int) {
|
||||
prefs.retrainIntervalHours = hours
|
||||
if (prefs.retrainScheduleEnabled) {
|
||||
@@ -1154,6 +1206,31 @@ class MainActivity : Activity() {
|
||||
|
||||
private fun yesNo(value: Boolean): String = if (value) "да" else "нет"
|
||||
|
||||
private fun exitSignalText(position: PositionData): String {
|
||||
val action = normalizedAction(position.exitAction)
|
||||
val reason = position.exitReason.ifBlank { "нет причины" }
|
||||
return if (action == "SELL") "SELL сейчас: ${reason.take(26)}" else "держим: ${reason.take(30)}"
|
||||
}
|
||||
|
||||
private fun protectiveExitText(position: PositionData): String {
|
||||
val trailing = position.atrTrailingStop ?: position.trailingStop
|
||||
if (trailing != null && trailing > 0.0) {
|
||||
return "trailing ${price(trailing)}"
|
||||
}
|
||||
val stop = position.stopLoss
|
||||
if (position.stopLossExitEnabled && stop != null && stop > 0.0) {
|
||||
return "stop-loss ${price(stop)}"
|
||||
}
|
||||
return "stop-loss выключен"
|
||||
}
|
||||
|
||||
private fun protectiveExitColor(position: PositionData): Int =
|
||||
when {
|
||||
position.atrTrailingStop != null || position.trailingStop != null -> palette.amber
|
||||
position.stopLossExitEnabled && position.stopLoss != null -> palette.red
|
||||
else -> palette.muted
|
||||
}
|
||||
|
||||
private fun colorForSigned(value: String): Int =
|
||||
when {
|
||||
value.trim().startsWith("-") -> palette.red
|
||||
@@ -1182,6 +1259,9 @@ class MainActivity : Activity() {
|
||||
else -> "0"
|
||||
}
|
||||
|
||||
private fun priceOrNone(value: Double?): String =
|
||||
if (value != null && value > 0.0) price(value) else "нет"
|
||||
|
||||
private fun number(value: Double, digits: Int = 2): String =
|
||||
"%,.${digits}f".format(Locale.US, value)
|
||||
|
||||
|
||||
@@ -92,6 +92,14 @@ data class PositionData(
|
||||
val marketValue: Double,
|
||||
val unrealizedPnl: Double,
|
||||
val unrealizedPnlPercent: Double,
|
||||
val stopLoss: Double?,
|
||||
val takeProfit: Double?,
|
||||
val highestPrice: Double?,
|
||||
val trailingStop: Double?,
|
||||
val atrTrailingStop: Double?,
|
||||
val exitAction: String,
|
||||
val exitReason: String,
|
||||
val stopLossExitEnabled: Boolean,
|
||||
)
|
||||
|
||||
data class AccountData(
|
||||
@@ -100,12 +108,37 @@ data class AccountData(
|
||||
val exposure: Double,
|
||||
)
|
||||
|
||||
data class ClosedTradeData(
|
||||
val symbol: String,
|
||||
val qty: Double,
|
||||
val entryPrice: Double?,
|
||||
val exitPrice: Double?,
|
||||
val grossPnl: Double,
|
||||
val feeUsdt: Double,
|
||||
val netPnl: Double,
|
||||
val reason: String,
|
||||
val openedAt: String,
|
||||
val closedAt: String,
|
||||
)
|
||||
|
||||
data class ClosedTradesSummary(
|
||||
val trades: Int,
|
||||
val netPnl: Double,
|
||||
val grossPnl: Double,
|
||||
val feeUsdt: Double,
|
||||
val wins: Int,
|
||||
val losses: Int,
|
||||
val winRate: Double,
|
||||
)
|
||||
|
||||
data class BotSnapshot(
|
||||
val ok: Boolean,
|
||||
val running: Boolean,
|
||||
val mode: String,
|
||||
val account: AccountData,
|
||||
val positions: List<PositionData>,
|
||||
val closedTrades: List<ClosedTradeData>,
|
||||
val closedTradesSummary: ClosedTradesSummary,
|
||||
val markets: List<MarketItem>,
|
||||
val signalsBySymbol: Map<String, SignalData>,
|
||||
val config: JSONObject,
|
||||
|
||||
@@ -19,6 +19,7 @@ class TradeBotApi(
|
||||
val markets = getJson("/api/markets")
|
||||
val signals = getJson("/api/signals?limit=220")
|
||||
val config = getJson("/api/config")
|
||||
val trades = getJson("/api/trades?limit=10")
|
||||
val retrain = getJson("/api/retrain")
|
||||
val backtest = getJson("/api/backtest")
|
||||
|
||||
@@ -35,6 +36,8 @@ class TradeBotApi(
|
||||
mode = health.optStringClean("mode"),
|
||||
account = account,
|
||||
positions = parsePositions(status.optJSONArray("positions") ?: JSONArray()),
|
||||
closedTrades = parseClosedTrades(trades.optJSONArray("closed_items") ?: JSONArray()),
|
||||
closedTradesSummary = parseClosedTradesSummary(trades.optJSONObject("closed_summary") ?: JSONObject()),
|
||||
markets = parseMarkets(markets.optJSONArray("markets") ?: JSONArray()),
|
||||
signalsBySymbol = parseLatestSignals(signals.optJSONArray("items") ?: JSONArray()),
|
||||
config = config,
|
||||
@@ -99,6 +102,7 @@ class TradeBotApi(
|
||||
val output = mutableListOf<PositionData>()
|
||||
for (index in 0 until items.length()) {
|
||||
val row = items.optJSONObject(index) ?: continue
|
||||
val exitPlan = row.optJSONObject("exit_plan") ?: JSONObject()
|
||||
output += PositionData(
|
||||
symbol = row.optStringClean("symbol"),
|
||||
qty = row.optDouble("qty", 0.0),
|
||||
@@ -108,11 +112,50 @@ class TradeBotApi(
|
||||
marketValue = row.optDouble("market_value", 0.0),
|
||||
unrealizedPnl = row.optDouble("unrealized_pnl", 0.0),
|
||||
unrealizedPnlPercent = row.optDouble("unrealized_pnl_percent", 0.0),
|
||||
stopLoss = exitPlan.optDoubleOrNull("stop_loss") ?: row.optDoubleOrNull("stop_loss"),
|
||||
takeProfit = exitPlan.optDoubleOrNull("take_profit") ?: row.optDoubleOrNull("take_profit"),
|
||||
highestPrice = exitPlan.optDoubleOrNull("highest_price") ?: row.optDoubleOrNull("highest_price"),
|
||||
trailingStop = exitPlan.optDoubleOrNull("trailing_stop"),
|
||||
atrTrailingStop = exitPlan.optDoubleOrNull("atr_trailing_stop"),
|
||||
exitAction = exitPlan.optStringClean("action"),
|
||||
exitReason = exitPlan.optStringClean("reason"),
|
||||
stopLossExitEnabled = exitPlan.optBooleanOrNull("stop_loss_exit_enabled") ?: true,
|
||||
)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
private fun parseClosedTrades(items: JSONArray): List<ClosedTradeData> {
|
||||
val output = mutableListOf<ClosedTradeData>()
|
||||
for (index in 0 until items.length()) {
|
||||
val row = items.optJSONObject(index) ?: continue
|
||||
output += ClosedTradeData(
|
||||
symbol = row.optStringClean("symbol"),
|
||||
qty = row.optDouble("qty", 0.0),
|
||||
entryPrice = row.optDoubleOrNull("entry_price"),
|
||||
exitPrice = row.optDoubleOrNull("exit_price"),
|
||||
grossPnl = row.optDouble("gross_pnl", 0.0),
|
||||
feeUsdt = row.optDouble("fee_usdt", 0.0),
|
||||
netPnl = row.optDouble("net_pnl", 0.0),
|
||||
reason = row.optStringClean("reason"),
|
||||
openedAt = row.optStringClean("opened_at"),
|
||||
closedAt = row.optStringClean("closed_at"),
|
||||
)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
private fun parseClosedTradesSummary(row: JSONObject): ClosedTradesSummary =
|
||||
ClosedTradesSummary(
|
||||
trades = row.optInt("trades", 0),
|
||||
netPnl = row.optDouble("net_pnl", 0.0),
|
||||
grossPnl = row.optDouble("gross_pnl", 0.0),
|
||||
feeUsdt = row.optDouble("fee_usdt", 0.0),
|
||||
wins = row.optInt("wins", 0),
|
||||
losses = row.optInt("losses", 0),
|
||||
winRate = row.optDouble("win_rate", 0.0),
|
||||
)
|
||||
|
||||
private fun parseMarkets(items: JSONArray): List<MarketItem> {
|
||||
val output = mutableListOf<MarketItem>()
|
||||
for (index in 0 until items.length()) {
|
||||
|
||||
Reference in New Issue
Block a user