fix: full arb table + net profit columns in price view

- ArbTable: removed slice(0,10) cap, shows all 42 opps (21x2)
- PriceTable: added net_bg_to_hl and net_hl_to_bg columns
- Backend SSE: sends net profit for both directions per coin
This commit is contained in:
jackyu66git
2026-05-04 18:26:49 +08:00
parent 9628100681
commit 0aa9923067
14 changed files with 45090 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
import sqlite3
conn = sqlite3.connect('data/trades.db')
rows = conn.execute('SELECT id, scale_count, amount_usd, fee_entry, fee_exit, net_pnl FROM trades').fetchall()
for r in rows:
print(f' #{r[0]}: scales={r[1]} amt=${r[2]} fee_entry={r[3]:.6f} fee_exit={r[4]:.6f} net={r[5]:.6f}')
conn.close()
+12
View File
@@ -356,6 +356,18 @@ func (d *Dashboard) broadcastLoop() {
spreadPct := (hlP - bgP) / bgP * 100 spreadPct := (hlP - bgP) / bgP * 100
entry["bg_hl_spread"] = spreadPct entry["bg_hl_spread"] = spreadPct
d.spreads.Record(coin.Name, spreadPct) d.spreads.Record(coin.Name, spreadPct)
// Both directions net profit after fees (4 taker fees: 2 entry + 2 exit)
cost := bgP * (1 + takerFees[ExBitget]/100)
revenue := hlP * (1 - takerFees[ExHyperLiquid]/100)
netBG := (revenue/cost-1)*100 - 2*(takerFees[ExBitget]+takerFees[ExHyperLiquid])
cost = hlP * (1 + takerFees[ExHyperLiquid]/100)
revenue = bgP * (1 - takerFees[ExBitget]/100)
netHL := (revenue/cost-1)*100 - 2*(takerFees[ExHyperLiquid]+takerFees[ExBitget])
entry["net_bg_to_hl"] = math.Round(netBG*10000) / 10000
entry["net_hl_to_bg"] = math.Round(netHL*10000) / 10000
} }
prices = append(prices, entry) prices = append(prices, entry)
File diff suppressed because it is too large Load Diff
+1
View File
@@ -42,6 +42,7 @@ func (b *BitgetTrade) PlaceMarketOrder(side, symbol, size, tradeSide string) (st
body := map[string]interface{}{ body := map[string]interface{}{
"marginCoin": "USDT", "marginCoin": "USDT",
"symbol": symbol, "symbol": symbol,
"productType": "USDT-FUTURES",
"side": side, "side": side,
"orderType": "market", "orderType": "market",
"timeInForce": "IOC", "timeInForce": "IOC",
+1 -1
View File
@@ -149,7 +149,7 @@ func GetHLSize(coin string, amountUSD, price float64) string {
switch coin { switch coin {
// integer sizes (szDecimals=0) // integer sizes (szDecimals=0)
case "DOGE", "ONDO", "WIF", "ALGO", "PYTH", "SAND", "ADA", "HBAR", case "DOGE", "ONDO", "WIF", "ALGO", "PYTH", "SAND", "ADA", "HBAR",
"IOTA", "MINA", "FET", "JUP", "MOVE": "IOTA", "FET", "JUP", "MOVE":
sz = math.Floor(sz) sz = math.Floor(sz)
if sz < 1 { if sz < 1 {
sz = 1 sz = 1
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exchange Monitor Dashboard</title> <title>Exchange Monitor Dashboard</title>
<script type="module" crossorigin src="/static/assets/index-DqqWimRT.js"></script> <script type="module" crossorigin src="/static/assets/index-D_JzXaOQ.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-Bh5bnFYE.css"> <link rel="stylesheet" crossorigin href="/static/assets/index-Bh5bnFYE.css">
</head> </head>
<body> <body>
+10 -4
View File
@@ -333,15 +333,15 @@ function PriceTable({ coins, prices, getPrevPrice, priceClass, pricesAge }) {
<div className="table-wrap"> <div className="table-wrap">
<table id="price-table"> <table id="price-table">
<thead> <thead>
<tr><th>币种</th><th>HyperLiquid</th><th>Bitget</th><th>BGHL价差</th></tr> <tr><th>币种</th><th>HyperLiquid</th><th>Bitget</th><th>毛价差</th><th>BGHL净利</th><th>HLBG净利</th></tr>
</thead> </thead>
<tbody id="price-body"> <tbody id="price-body">
{coins.length === 0 ? ( {coins.length === 0 ? (
<tr><td colSpan="4" className="loading">等待数据...</td></tr> <tr><td colSpan="6" className="loading">等待数据...</td></tr>
) : coins.map(coin => { ) : coins.map(coin => {
const row = prices.find(p => p.coin === coin) const row = prices.find(p => p.coin === coin)
if (!row) { if (!row) {
return <tr key={coin}><td>{coin}</td><td className="text-dim">-</td><td className="text-dim">-</td><td className="text-dim">-</td></tr> return <tr key={coin}><td>{coin}</td><td className="text-dim">-</td><td className="text-dim">-</td><td className="text-dim">-</td><td className="text-dim">-</td><td className="text-dim">-</td></tr>
} }
const cells = EXCHANGES.map(ex => { const cells = EXCHANGES.map(ex => {
const p = row[ex] const p = row[ex]
@@ -351,11 +351,17 @@ function PriceTable({ coins, prices, getPrevPrice, priceClass, pricesAge }) {
}) })
const spread = row['bg_hl_spread'] const spread = row['bg_hl_spread']
const spreadCls = spread > 0.2 ? 'text-green' : spread < -0.2 ? 'text-red' : '' const spreadCls = spread > 0.2 ? 'text-green' : spread < -0.2 ? 'text-red' : ''
const nb = row['net_bg_to_hl']
const nh = row['net_hl_to_bg']
const nbCls = nb != null ? pnlClass(nb) : ''
const nhCls = nh != null ? pnlClass(nh) : ''
return ( return (
<tr key={coin}> <tr key={coin}>
<td><strong>{coin}</strong></td> <td><strong>{coin}</strong></td>
{cells} {cells}
<td className={spreadCls}>{spread != null ? spread.toFixed(4) + '%' : '-'}</td> <td className={spreadCls}>{spread != null ? spread.toFixed(4) + '%' : '-'}</td>
<td className={nbCls}>{nb != null ? nb.toFixed(2) + '%' : '-'}</td>
<td className={nhCls}>{nh != null ? nh.toFixed(2) + '%' : '-'}</td>
</tr> </tr>
) )
})} })}
@@ -378,7 +384,7 @@ function ArbTable({ opps }) {
<tbody id="arb-body"> <tbody id="arb-body">
{!opps || opps.length === 0 ? ( {!opps || opps.length === 0 ? (
<tr><td colSpan="5" className="text-dim">暂无套利机会</td></tr> <tr><td colSpan="5" className="text-dim">暂无套利机会</td></tr>
) : opps.slice(0, 10).map((opp, i) => { ) : opps.map((opp, i) => {
const cls = opp.net_profit > 0.10 ? 'text-green' : opp.net_profit > 0.05 ? 'text-yellow' : '' const cls = opp.net_profit > 0.10 ? 'text-green' : opp.net_profit > 0.05 ? 'text-yellow' : ''
return ( return (
<tr key={i}> <tr key={i}>
Executable
BIN
View File
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Morning summary script — triggered by cron at 8 AM
PROJECT_DIR="/home/jack/Project/exchange-monitor-go"
STATS_FILE="$PROJECT_DIR/trade_stats.txt"
LOG_FILE="$PROJECT_DIR/morning_summary.txt"
# Find the PID of the exchange-monitor process
PID=$(pgrep -f "exchange-monitor" | head -1)
if [ -n "$PID" ]; then
# Send SIGUSR1 to dump stats
kill -USR1 "$PID" 2>/dev/null
sleep 2
# Read stats
if [ -f "$STATS_FILE" ]; then
cat "$STATS_FILE"
echo ""
echo "=== 程序状态 ==="
ps -p "$PID" -o pid,etime,cmd --no-headers 2>/dev/null || echo "进程已结束"
fi
# Kill the process
kill "$PID" 2>/dev/null
sleep 1
kill -0 "$PID" 2>/dev/null && kill -9 "$PID" 2>/dev/null
echo ""
echo "程序已停止 (PID: $PID)"
else
echo "未找到运行中的 exchange-monitor 进程"
fi
echo "=== 报告完成 ==="
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd /home/jack/Project/exchange-monitor-go
# Redirect stderr to stdout so background mode captures everything
./exchange-monitor 2>&1
-1
View File
@@ -31,7 +31,6 @@ var TrackedCoins = []TrackedCoin{
{Name: "ADA", BG: "ADAUSDT", HL: "ADA"}, {Name: "ADA", BG: "ADAUSDT", HL: "ADA"},
{Name: "HBAR", BG: "HBARUSDT", HL: "HBAR"}, {Name: "HBAR", BG: "HBARUSDT", HL: "HBAR"},
{Name: "IOTA", BG: "IOTAUSDT", HL: "IOTA"}, {Name: "IOTA", BG: "IOTAUSDT", HL: "IOTA"},
{Name: "MINA", BG: "MINAUSDT", HL: "MINA"},
{Name: "FET", BG: "FETUSDT", HL: "FET"}, {Name: "FET", BG: "FETUSDT", HL: "FET"},
{Name: "WLD", BG: "WLDUSDT", HL: "WLD"}, {Name: "WLD", BG: "WLDUSDT", HL: "WLD"},
{Name: "SUI", BG: "SUIUSDT", HL: "SUI"}, {Name: "SUI", BG: "SUIUSDT", HL: "SUI"},
+6
View File
@@ -0,0 +1,6 @@
=== 收敛统计 === 2026-05-02 20:01
总交易数: 310
价差收敛: 309
价差持平: 0
价差发散: 1
收敛率: 99.7%
+2
View File
@@ -555,11 +555,13 @@ func (t *Trader) executeEntry(opp *ArbOpportunity, store *PriceStore, notifier *
// Execute both legs // Execute both legs
if err := t.placeOrder(pos.LongLeg, "buy", store); err != "" { if err := t.placeOrder(pos.LongLeg, "buy", store); err != "" {
log.Printf("[Trader] %s: long leg placeOrder failed: %s", opp.Coin, err)
t.cleanup(pos.Coin) t.cleanup(pos.Coin)
return false return false
} }
time.Sleep(t.cfg.LegDelay) time.Sleep(t.cfg.LegDelay)
if err := t.placeOrder(pos.ShortLeg, "sell", store); err != "" { if err := t.placeOrder(pos.ShortLeg, "sell", store); err != "" {
log.Printf("[Trader] %s: short leg placeOrder failed: %s", opp.Coin, err)
// Leg1 placed successfully, leg2 failed — try to close leg1 // Leg1 placed successfully, leg2 failed — try to close leg1
pos.Status = "failed" pos.Status = "failed"
if closeErr := t.closeLeg(pos.LongLeg); closeErr != "" { if closeErr := t.closeLeg(pos.LongLeg); closeErr != "" {