feat(mse): commission-aware PnL on open positions
- FilledOrder gains commission_usdt field - parse_filled_order: sums fills[] commission as rate × qty × price in USDT - OpenPosition gains buy_commission_usdt; DB migration via ALTER TABLE ADD COLUMN - runner.rs writes buy_commission_usdt from buy_order.commission_usdt - bot.html PnL = gross_pnl - buy_commission_pct - estimated_sell_commission_pct Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a8e80e0a15
commit
8804824199
6 changed files with 68 additions and 35 deletions
|
|
@ -562,6 +562,21 @@ fn parse_filled_order(data: &Value, side: OrderSide) -> Result<FilledOrder> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// fills'den komisyonu qty * fill_price * rate olarak USDT'ye çevir
|
||||||
|
let commission_usdt = data["fills"]
|
||||||
|
.as_array()
|
||||||
|
.map(|fills| {
|
||||||
|
fills.iter().fold(0.0f64, |acc, f| {
|
||||||
|
let qty: f64 = f["qty"].as_str().unwrap_or("0").parse().unwrap_or(0.0);
|
||||||
|
let fill_price: f64 = f["price"].as_str().unwrap_or("0").parse().unwrap_or(0.0);
|
||||||
|
let commission: f64 = f["commission"].as_str().unwrap_or("0").parse().unwrap_or(0.0);
|
||||||
|
// komisyon oranı = commission / qty, USDT karşılığı = oran × qty × fiyat
|
||||||
|
let rate = if qty > 0.0 { commission / qty } else { 0.001 };
|
||||||
|
acc + rate * qty * fill_price
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
Ok(FilledOrder {
|
Ok(FilledOrder {
|
||||||
order_id: data["orderId"].as_u64().unwrap_or(0),
|
order_id: data["orderId"].as_u64().unwrap_or(0),
|
||||||
symbol: data["symbol"].as_str().unwrap_or("").to_string(),
|
symbol: data["symbol"].as_str().unwrap_or("").to_string(),
|
||||||
|
|
@ -569,5 +584,6 @@ fn parse_filled_order(data: &Value, side: OrderSide) -> Result<FilledOrder> {
|
||||||
price,
|
price,
|
||||||
quantity: executed_qty,
|
quantity: executed_qty,
|
||||||
timestamp: data["transactTime"].as_i64().unwrap_or(0),
|
timestamp: data["transactTime"].as_i64().unwrap_or(0),
|
||||||
|
commission_usdt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ pub struct FilledOrder {
|
||||||
pub price: f64,
|
pub price: f64,
|
||||||
pub quantity: f64,
|
pub quantity: f64,
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
|
pub commission_usdt: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ impl BotRunner {
|
||||||
quantity: result.buy_order.quantity,
|
quantity: result.buy_order.quantity,
|
||||||
profit_percent: config.profit_percent,
|
profit_percent: config.profit_percent,
|
||||||
opened_at: result.timestamp,
|
opened_at: result.timestamp,
|
||||||
|
buy_commission_usdt: result.buy_order.commission_usdt,
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ impl Database {
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_bot_logs_bot_id ON bot_logs(bot_id, created_at DESC);",
|
CREATE INDEX IF NOT EXISTS idx_bot_logs_bot_id ON bot_logs(bot_id, created_at DESC);",
|
||||||
)?;
|
)?;
|
||||||
|
// migration: mevcut DB'ye buy_commission_usdt sütunu ekle
|
||||||
|
conn.execute_batch(
|
||||||
|
"ALTER TABLE open_positions ADD COLUMN buy_commission_usdt REAL NOT NULL DEFAULT 0;",
|
||||||
|
).ok();
|
||||||
Ok(Self { conn })
|
Ok(Self { conn })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,12 +192,12 @@ impl Database {
|
||||||
pub fn insert_position(&self, p: &OpenPosition) -> SqlResult<()> {
|
pub fn insert_position(&self, p: &OpenPosition) -> SqlResult<()> {
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT OR REPLACE INTO open_positions
|
"INSERT OR REPLACE INTO open_positions
|
||||||
(order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at)
|
(order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at, buy_commission_usdt)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||||
params![
|
params![
|
||||||
p.order_id as i64, p.bot_id, p.bot_name, p.symbol,
|
p.order_id as i64, p.bot_id, p.bot_name, p.symbol,
|
||||||
p.buy_price, p.sell_target, p.quantity,
|
p.buy_price, p.sell_target, p.quantity,
|
||||||
p.profit_percent, p.opened_at
|
p.profit_percent, p.opened_at, p.buy_commission_usdt
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -201,7 +205,7 @@ impl Database {
|
||||||
|
|
||||||
pub fn get_positions(&self) -> SqlResult<Vec<OpenPosition>> {
|
pub fn get_positions(&self) -> SqlResult<Vec<OpenPosition>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at
|
"SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at, buy_commission_usdt
|
||||||
FROM open_positions ORDER BY opened_at DESC",
|
FROM open_positions ORDER BY opened_at DESC",
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([], |row| {
|
||||||
|
|
@ -216,6 +220,7 @@ impl Database {
|
||||||
quantity: row.get(6)?,
|
quantity: row.get(6)?,
|
||||||
profit_percent: row.get(7)?,
|
profit_percent: row.get(7)?,
|
||||||
opened_at: row.get(8)?,
|
opened_at: row.get(8)?,
|
||||||
|
buy_commission_usdt: row.get(9)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect()
|
rows.collect()
|
||||||
|
|
@ -338,7 +343,7 @@ impl Database {
|
||||||
|
|
||||||
pub fn get_positions_by_bot(&self, bot_id: &str) -> SqlResult<Vec<crate::storage::positions::OpenPosition>> {
|
pub fn get_positions_by_bot(&self, bot_id: &str) -> SqlResult<Vec<crate::storage::positions::OpenPosition>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at
|
"SELECT order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at, buy_commission_usdt
|
||||||
FROM open_positions WHERE bot_id = ?1 ORDER BY opened_at DESC",
|
FROM open_positions WHERE bot_id = ?1 ORDER BY opened_at DESC",
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(params![bot_id], |row| {
|
let rows = stmt.query_map(params![bot_id], |row| {
|
||||||
|
|
@ -353,6 +358,7 @@ impl Database {
|
||||||
quantity: row.get(6)?,
|
quantity: row.get(6)?,
|
||||||
profit_percent: row.get(7)?,
|
profit_percent: row.get(7)?,
|
||||||
opened_at: row.get(8)?,
|
opened_at: row.get(8)?,
|
||||||
|
buy_commission_usdt: row.get(9)?,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
rows.collect()
|
rows.collect()
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,5 @@ pub struct OpenPosition {
|
||||||
pub quantity: f64,
|
pub quantity: f64,
|
||||||
pub profit_percent: f64,
|
pub profit_percent: f64,
|
||||||
pub opened_at: i64, // ms
|
pub opened_at: i64, // ms
|
||||||
|
pub buy_commission_usdt: f64,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -621,9 +621,17 @@ function renderOpenTable() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = openPositionsCache.map(p => {
|
tbody.innerHTML = openPositionsCache.map(p => {
|
||||||
const pnl = currentPrice > 0
|
let pnl = null;
|
||||||
? ((currentPrice - p.buy_price) / p.buy_price * 100)
|
if (currentPrice > 0) {
|
||||||
: null;
|
const grossPnl = (currentPrice - p.buy_price) / p.buy_price * 100;
|
||||||
|
// alış komisyonu % olarak: buy_commission_usdt / (buy_price * quantity) * 100
|
||||||
|
const buyCommissionPct = p.buy_commission_usdt > 0
|
||||||
|
? (p.buy_commission_usdt / (p.buy_price * p.quantity) * 100)
|
||||||
|
: 0.1;
|
||||||
|
// satış komisyonu tahmini: aynı oran üzerinden (currentPrice × quantity için)
|
||||||
|
const sellCommissionPct = buyCommissionPct;
|
||||||
|
pnl = grossPnl - buyCommissionPct - sellCommissionPct;
|
||||||
|
}
|
||||||
const pnlHtml = pnl !== null
|
const pnlHtml = pnl !== null
|
||||||
? `<span class="${pnl >= 0 ? 'c-green' : 'c-red'}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}%</span>`
|
? `<span class="${pnl >= 0 ? 'c-green' : 'c-red'}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}%</span>`
|
||||||
: '<span class="c-muted">—</span>';
|
: '<span class="c-muted">—</span>';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue