decouple display from trading: snapMu + RefreshSnapshot/ReadSnapshot

- Add snapMu RWMutex + positionsSnapshot to Trader
- RefreshSnapshot() called from main loop after Tick() — acquires
  t.mu briefly, stores deep copy under snapMu
- ReadSnapshot() returns snapshot copy under snapMu.RLock — never
  touches t.mu, zero contention with trading path
- Dashboard + handleStatus + hourly summary + status log all
  use ReadSnapshot() instead of GetPositionsCopy()
- Trading path (Tick/TryEntry/executeEntry/checkExit/checkScaleIn)
  never blocked by display reads
- Snapshot is at most 1 tick behind live state — acceptable delay
This commit is contained in:
jackyu66git
2026-05-03 18:35:47 +08:00
parent b08d8490fc
commit 02b74f1ec0
3 changed files with 31 additions and 6 deletions
+3 -3
View File
@@ -290,8 +290,8 @@ func (d *Dashboard) broadcastLoop() {
}
d.hub.Broadcast("prices", prices)
// 2. Open positions with live PnL (P3-3) — use safe copy for concurrent read
positions := d.trader.GetPositionsCopy()
// 2. Open positions with live PnL (P3-3) — read from decoupled snapshot, never blocks trader
positions := d.trader.ReadSnapshot()
posList := make([]map[string]interface{}, 0, len(positions))
for _, pos := range positions {
posEntry := map[string]interface{}{
@@ -428,7 +428,7 @@ func (d *Dashboard) handleIndex(w http.ResponseWriter, r *http.Request) {
func (d *Dashboard) handleStatus(w http.ResponseWriter, r *http.Request) {
snap := d.store.GetAll()
positions := d.trader.GetPositionsCopy()
positions := d.trader.ReadSnapshot()
converged, diverged, flat, total := d.trader.GetClosedStats()
resp := map[string]interface{}{