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 {
|
||||
order_id: data["orderId"].as_u64().unwrap_or(0),
|
||||
symbol: data["symbol"].as_str().unwrap_or("").to_string(),
|
||||
|
|
@ -569,5 +584,6 @@ fn parse_filled_order(data: &Value, side: OrderSide) -> Result<FilledOrder> {
|
|||
price,
|
||||
quantity: executed_qty,
|
||||
timestamp: data["transactTime"].as_i64().unwrap_or(0),
|
||||
commission_usdt,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ pub struct FilledOrder {
|
|||
pub price: f64,
|
||||
pub quantity: f64,
|
||||
pub timestamp: i64,
|
||||
pub commission_usdt: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -142,15 +142,16 @@ impl BotRunner {
|
|||
};
|
||||
|
||||
let position = OpenPosition {
|
||||
bot_id: config.id.clone(),
|
||||
bot_name: config.name.clone(),
|
||||
symbol: config.symbol.clone(),
|
||||
order_id: result.sell_order.order_id,
|
||||
buy_price: result.buy_order.price,
|
||||
sell_target: result.sell_order.price,
|
||||
quantity: result.buy_order.quantity,
|
||||
profit_percent: config.profit_percent,
|
||||
opened_at: result.timestamp,
|
||||
bot_id: config.id.clone(),
|
||||
bot_name: config.name.clone(),
|
||||
symbol: config.symbol.clone(),
|
||||
order_id: result.sell_order.order_id,
|
||||
buy_price: result.buy_order.price,
|
||||
sell_target: result.sell_order.price,
|
||||
quantity: result.buy_order.quantity,
|
||||
profit_percent: config.profit_percent,
|
||||
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);",
|
||||
)?;
|
||||
// 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 })
|
||||
}
|
||||
|
||||
|
|
@ -188,12 +192,12 @@ impl Database {
|
|||
pub fn insert_position(&self, p: &OpenPosition) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO open_positions
|
||||
(order_id, bot_id, bot_name, symbol, buy_price, sell_target, quantity, profit_percent, opened_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
(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, ?10)",
|
||||
params![
|
||||
p.order_id as i64, p.bot_id, p.bot_name, p.symbol,
|
||||
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(())
|
||||
|
|
@ -201,21 +205,22 @@ impl Database {
|
|||
|
||||
pub fn get_positions(&self) -> SqlResult<Vec<OpenPosition>> {
|
||||
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",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
let order_id: i64 = row.get(0)?;
|
||||
Ok(OpenPosition {
|
||||
order_id: order_id as u64,
|
||||
bot_id: row.get(1)?,
|
||||
bot_name: row.get(2)?,
|
||||
symbol: row.get(3)?,
|
||||
buy_price: row.get(4)?,
|
||||
sell_target: row.get(5)?,
|
||||
quantity: row.get(6)?,
|
||||
profit_percent: row.get(7)?,
|
||||
opened_at: row.get(8)?,
|
||||
order_id: order_id as u64,
|
||||
bot_id: row.get(1)?,
|
||||
bot_name: row.get(2)?,
|
||||
symbol: row.get(3)?,
|
||||
buy_price: row.get(4)?,
|
||||
sell_target: row.get(5)?,
|
||||
quantity: row.get(6)?,
|
||||
profit_percent: row.get(7)?,
|
||||
opened_at: row.get(8)?,
|
||||
buy_commission_usdt: row.get(9)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
|
|
@ -338,21 +343,22 @@ impl Database {
|
|||
|
||||
pub fn get_positions_by_bot(&self, bot_id: &str) -> SqlResult<Vec<crate::storage::positions::OpenPosition>> {
|
||||
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",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![bot_id], |row| {
|
||||
let order_id: i64 = row.get(0)?;
|
||||
Ok(crate::storage::positions::OpenPosition {
|
||||
order_id: order_id as u64,
|
||||
bot_id: row.get(1)?,
|
||||
bot_name: row.get(2)?,
|
||||
symbol: row.get(3)?,
|
||||
buy_price: row.get(4)?,
|
||||
sell_target: row.get(5)?,
|
||||
quantity: row.get(6)?,
|
||||
profit_percent: row.get(7)?,
|
||||
opened_at: row.get(8)?,
|
||||
order_id: order_id as u64,
|
||||
bot_id: row.get(1)?,
|
||||
bot_name: row.get(2)?,
|
||||
symbol: row.get(3)?,
|
||||
buy_price: row.get(4)?,
|
||||
sell_target: row.get(5)?,
|
||||
quantity: row.get(6)?,
|
||||
profit_percent: row.get(7)?,
|
||||
opened_at: row.get(8)?,
|
||||
buy_commission_usdt: row.get(9)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
|
|
|
|||
|
|
@ -12,4 +12,5 @@ pub struct OpenPosition {
|
|||
pub quantity: f64,
|
||||
pub profit_percent: f64,
|
||||
pub opened_at: i64, // ms
|
||||
pub buy_commission_usdt: f64,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -621,9 +621,17 @@ function renderOpenTable() {
|
|||
return;
|
||||
}
|
||||
tbody.innerHTML = openPositionsCache.map(p => {
|
||||
const pnl = currentPrice > 0
|
||||
? ((currentPrice - p.buy_price) / p.buy_price * 100)
|
||||
: null;
|
||||
let pnl = null;
|
||||
if (currentPrice > 0) {
|
||||
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
|
||||
? `<span class="${pnl >= 0 ? 'c-green' : 'c-red'}">${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}%</span>`
|
||||
: '<span class="c-muted">—</span>';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue