Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 125 additions & 38 deletions biz/odmgr_live.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,23 @@

type LiveOrderMgr struct {
OrderMgr
queue chan *OdQItem
doneKeys map[string]int64 // Completed Orders 已完成的订单:symbol+orderId
exgIdMap map[string]*ormo.InOutOrder // symbol+orderId: InOutOrder
doneTrades map[string]int64 // Processed trades 已处理的交易:symbol+tradeId
lockDoneKeys deadlock.Mutex
lockExgIdMap deadlock.Mutex
lockDoneTrades deadlock.Mutex
isWatchMyTrade int32 // atomic flag: Is the account transaction flow being monitored? 是否正在监听账户交易流
isTrialUnMatches int32 // atomic flag: Is monitoring unmatched transactions? 是否正在监听未匹配交易
isConsumeOrderQ int32 // atomic flag: Is it consuming from the order queue? 是否正在从订单队列消费
isWatchAccConfig int32 // atomic flag: Is the leverage ratio being monitored? 是否正在监听杠杆倍数变化
unMatchTrades map[string]*banexg.MyTrade // Transactions received from ws that have no matching orders 从ws收到的暂无匹配的订单的交易
lockUnMatches deadlock.Mutex // Prevent concurrent reading and writing of unMatchTrades 防止并发读写unMatchTrades
exitByMyOrder FuncHandleMyOrder // Try to use the transaction results of other end operations to update the current order status 尝试使用其他端操作的交易结果,更新当前订单状态
traceExgOrder FuncHandleMyOrder
queue chan *OdQItem
doneKeys map[string]int64 // Completed Orders 已完成的订单:symbol+orderId
exgIdMap map[string]*ormo.InOutOrder // symbol+orderId: InOutOrder
doneTrades map[string]int64 // Processed trades 已处理的交易:symbol+tradeId
lockDoneKeys deadlock.Mutex
lockExgIdMap deadlock.Mutex
lockDoneTrades deadlock.Mutex
isWatchMyTrade int32 // atomic flag: Is the account transaction flow being monitored? 是否正在监听账户交易流
watchMyTradesFn func(map[string]interface{}) (chan *banexg.MyTrade, *errs.Error)
myTradeWatchRetry time.Duration
isTrialUnMatches int32 // atomic flag: Is monitoring unmatched transactions? 是否正在监听未匹配交易
isConsumeOrderQ int32 // atomic flag: Is it consuming from the order queue? 是否正在从订单队列消费
isWatchAccConfig int32 // atomic flag: Is the leverage ratio being monitored? 是否正在监听杠杆倍数变化
unMatchTrades map[string]*banexg.MyTrade // Transactions received from ws that have no matching orders 从ws收到的暂无匹配的订单的交易
lockUnMatches deadlock.Mutex // Prevent concurrent reading and writing of unMatchTrades 防止并发读写unMatchTrades
exitByMyOrder FuncHandleMyOrder // Try to use the transaction results of other end operations to update the current order status 尝试使用其他端操作的交易结果,更新当前订单状态
traceExgOrder FuncHandleMyOrder
}

type OdQItem struct {
Expand All @@ -54,9 +56,11 @@
}

const (
AmtDust = 1e-8
odInfoLocalTrigger = "LocalTrigger"
restoreAmbiguousExgData = "ambiguous_exchange_entries"
AmtDust = 1e-8
odInfoLocalTrigger = "LocalTrigger"
restoreAmbiguousExgData = "ambiguous_exchange_entries"
pendingMarketEntryReconcileAfter = 20 * time.Second
defaultMyTradeWatchRetry = 5 * time.Second
)

var (
Expand Down Expand Up @@ -102,11 +106,12 @@
callBack: callBack,
Account: account,
},
queue: make(chan *OdQItem, 1000),
doneKeys: map[string]int64{},
exgIdMap: map[string]*ormo.InOutOrder{},
doneTrades: map[string]int64{},
unMatchTrades: map[string]*banexg.MyTrade{},
queue: make(chan *OdQItem, 1000),
doneKeys: map[string]int64{},
exgIdMap: map[string]*ormo.InOutOrder{},
doneTrades: map[string]int64{},
unMatchTrades: map[string]*banexg.MyTrade{},
myTradeWatchRetry: defaultMyTradeWatchRetry,
}
res.afterEnter = makeAfterEnter(res)
res.afterExit = makeAfterExit(res)
Expand Down Expand Up @@ -146,9 +151,20 @@
posMap[pos.Symbol][isShort] = pos
}

// 按symbol分组本地订单
// Reconcile only stale submitted market entries. A private WebSocket outage may
// drop their execution events; the targeted REST check repairs that gap without
// changing pending limit-order behavior.
openOds, lock := ormo.GetOpenODs(o.Account)
lock.Lock()
pendingEntries := make([]*ormo.InOutOrder, 0, len(openOds))
for _, od := range openOds {
pendingEntries = append(pendingEntries, od)
}
lock.Unlock()
o.reconcilePendingMarketEntries(pendingEntries)

// 按symbol分组本地订单
lock.Lock()
// 过滤重复订单
var duplicateOdNum = 0
var checkKeys = make(map[string]bool)
Expand Down Expand Up @@ -1272,30 +1288,101 @@
}

func (o *LiveOrderMgr) WatchMyTrades() {
out, err := exg.Default.WatchMyTrades(map[string]interface{}{
banexg.ParamAccount: o.Account,
})
if err != nil {
log.Error("WatchMyTrades fail", zap.String("acc", o.Account), zap.Error(err))
return
}
if !atomic.CompareAndSwapInt32(&o.isWatchMyTrade, 0, 1) {
return
}
watch := o.watchMyTradesFn
if watch == nil {
watch = exg.Default.WatchMyTrades
}
retryDelay := o.myTradeWatchRetry
if retryDelay <= 0 {
retryDelay = defaultMyTradeWatchRetry
}
go func() {
defer func() {
atomic.StoreInt32(&o.isWatchMyTrade, 0)
}()
for trade := range out {
orm.AddDumpRow(orm.DumpWsMyTrade, trade.Symbol+trade.ID, trade)
if trade.State == banexg.OdStatusOpen {
defer atomic.StoreInt32(&o.isWatchMyTrade, 0)
for {
out, err := watch(map[string]interface{}{
banexg.ParamAccount: o.Account,
})
if err != nil {
log.Error("WatchMyTrades fail, retrying", zap.String("acc", o.Account), zap.Error(err))
time.Sleep(retryDelay)
continue
}
o.handleMyTrade(trade)
for trade := range out {
orm.AddDumpRow(orm.DumpWsMyTrade, trade.Symbol+trade.ID, trade)
if trade.State == banexg.OdStatusOpen {
continue
}
o.handleMyTrade(trade)
}
log.Warn("WatchMyTrades stream closed, retrying", zap.String("acc", o.Account),
zap.Duration("after", retryDelay))
time.Sleep(retryDelay)
Comment on lines +1304 to +1322

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The watcher loop has no shutdown condition, so after the app context is canceled it can keep reconnecting and sleeping forever instead of exiting. Add a context/done check before retry sleeps and before starting a new subscription attempt so the goroutine terminates cleanly. [resource leak]

Severity Level: Major ⚠️
- ❌ Trade watcher goroutine never stops on global shutdown.
- ⚠️ Process holds WebSocket connections after StopAll cancel.
- ⚠️ Resource leak complicates graceful robot shutdown procedures.
Steps of Reproduction ✅
1. In `biz/biz.go:57-59`, the live bootstrap initializes `core.Ctx` and `core.StopAll` via
`ctx, cancel := context.WithCancel(context.Background()); core.Ctx = ctx; core.StopAll =
cancel`, establishing a global cancellation mechanism for goroutines.

2. A live trading run calls `biz.StartLiveOdMgr()` from `live/crypto_trader.go:377`, which
iterates configured accounts and starts per-account managers; in
`biz/odmgr_live.go:3293-3303` each manager invokes `odMgr.WatchMyTrades()` to monitor the
account trade stream.

3. `WatchMyTrades()` in `biz/odmgr_live.go:1290-1324` launches a goroutine that executes
an infinite `for` loop starting at line 1304, repeatedly calling `watch(...)`, handling
trades, logging, and sleeping on both subscription errors (`time.Sleep(retryDelay)` at
line 1310) and closed streams (`time.Sleep(retryDelay)` at line 1322) without ever
checking `core.Ctx.Done()` or any other shutdown signal.

4. When `core.StopAll()` is invoked (for example from `biz/odmgr_local.go:544` or other
shutdown paths), `core.Ctx` is canceled and other workers like
`WsDataLoader.downloadWorker` and `splitWorker` in `data/ws_loader.go:344-383` exit via
`select { case <-core.Ctx.Done(): return }`, but `WatchMyTrades` continues its
reconnect-and-sleep loop indefinitely, leaving the goroutine and its WebSocket resources
running past logical shutdown until process termination.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** biz/odmgr_live.go
**Line:** 1304:1322
**Comment:**
	*Resource Leak: The watcher loop has no shutdown condition, so after the app context is canceled it can keep reconnecting and sleeping forever instead of exiting. Add a context/done check before retry sleeps and before starting a new subscription attempt so the goroutine terminates cleanly.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
Comment on lines 1302 to 1323

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Watchmytrades ignores shutdown 🐞 Bug ☼ Reliability

The new WatchMyTrades retry loop never checks core.Ctx.Done() and uses uninterruptible
time.Sleep, so it can keep reconnecting and logging retries indefinitely after shutdown is
requested. Add context cancellation checks around the watch call, channel processing, and retry
sleep.
Agent Prompt
### Issue description
`WatchMyTrades` now runs an infinite reconnect loop, but it does not observe `core.Ctx.Done()` and sleeps via `time.Sleep`, which cannot be cancelled. This makes the goroutine continue retrying during shutdown.

### Issue Context
Other long-running goroutines in this file (e.g., `ConsumeOrderQueue`) exit when `core.Ctx.Done()` is closed.

### Fix Focus Areas
- biz/odmgr_live.go[1206-1224]
- biz/odmgr_live.go[1290-1324]

### Suggested fix
- Add `select { case <-core.Ctx.Done(): return ... }` checks in the outer `for` loop.
- Replace `time.Sleep(retryDelay)` with a cancellable wait:
  - `select { case <-core.Ctx.Done(): return; case <-time.After(retryDelay): }`
- Optionally, process trades with a `select` on `<-core.Ctx.Done()` and `<-out` so shutdown can stop even if the stream stays open.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}()
}
Comment on lines 1290 to 1325

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for shutdown/cleanup mechanisms in LiveOrderMgr.

# Look for CleanUp or similar methods to see how other goroutines are stopped.
ast-grep run --pattern $'func (o *LiveOrderMgr) CleanUp() {
  $$$
}' --lang go biz/odmgr_live.go

# Check if there is a context or a stopped flag on LiveOrderMgr
rg -A 5 -B 5 "type LiveOrderMgr struct" biz/odmgr_live.go

Repository: banbox/banbot

Length of output: 642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file structure.
ast-grep outline biz/odmgr_live.go --view expanded | sed -n '1,220p'

# Find shutdown/cleanup-related methods and flags in the file.
rg -n "func \\(o \\*LiveOrderMgr\\) (CleanUp|Close|Stop|Shutdown|Release)|isWatchMyTrade|done|closed|context\\.Context" biz/odmgr_live.go

# Show the LiveOrderMgr struct and nearby methods around WatchMyTrades.
sed -n '1,260p' biz/odmgr_live.go
echo '--- WATCH SECTION ---'
sed -n '1260,1365p' biz/odmgr_live.go

Repository: banbox/banbot

Length of output: 20908


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect shutdown path and any cleanup side effects around LiveOrderMgr.
sed -n '3260,3310p' biz/odmgr_live.go

echo '--- OnEnvEnd / shutdown references ---'
rg -n "OnEnvEnd|CleanUp\\(|StartLiveOdMgr|WatchMyTrades\\(|WatchLeverages\\(|TrialUnMatchesForever\\(" biz/odmgr_live.go

echo '--- CleanUp call sites in repo ---'
rg -n "\.CleanUp\\(" biz

echo '--- WatchMyTrades call sites in repo ---'
rg -n "WatchMyTrades\\(" biz

Repository: banbox/banbot

Length of output: 2108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,150p' biz/odmgr.go
echo '---'
sed -n '150,260p' biz/odmgr.go

Repository: banbox/banbot

Length of output: 7577


Add a stop signal to WatchMyTrades

WatchMyTrades() keeps reconnecting forever, and CleanUp() is currently a no-op, so CleanUpOdMgr() won’t stop this goroutine. Add a shutdown flag/context check in the loop and clear it from cleanup to avoid leaking the watcher after env end.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@biz/odmgr_live.go` around lines 1290 - 1325, Add a shutdown signal for the
goroutine started by LiveOrderMgr.WatchMyTrades, check it before reconnecting
and retry sleeping, and exit when shutdown is requested. Update the manager
cleanup path, including CleanUp, to trigger and clear the watcher’s stop state
so CleanUpOdMgr stops the loop and prevents the goroutine from leaking.


// reconcilePendingMarketEntries restores a market entry only when the exchange
// has an authoritative positive fill. It deliberately ignores limit
// entries and fresh orders, preserving normal entry and trigger behavior.
func (o *LiveOrderMgr) reconcilePendingMarketEntries(ods []*ormo.InOutOrder) {

Check failure on line 1330 in biz/odmgr_live.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=banbox_banbot&issues=AZ9t-XAQOO9Qr7xM0xhQ&open=AZ9t-XAQOO9Qr7xM0xhQ&pullRequest=147
cutoff := btime.UTCStamp() - pendingMarketEntryReconcileAfter.Milliseconds()
for _, od := range ods {
if od == nil {
continue
}
lock := od.Lock()
enter := od.Enter
if enter == nil || enter.OrderID == "" || enter.OrderType != banexg.OdTypeMarket ||
enter.Status == ormo.OdStatusClosed || enter.Filled > AmtDust || enter.UpdateAt > cutoff {
lock.Unlock()
continue
}
orderID, symbol := enter.OrderID, od.Symbol
lock.Unlock()

res, err := exg.Default.FetchOrder(symbol, orderID, map[string]interface{}{
banexg.ParamAccount: o.Account,
})
if err != nil {
log.Warn("reconcile pending market entry fetch fail", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID), zap.Error(err))
continue
}
if res == nil || res.ID == "" || res.ID != orderID {
log.Warn("reconcile pending market entry invalid response", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID))
continue
}
if res.Filled <= AmtDust {
continue
}

lock = od.Lock()
enter = od.Enter
if enter == nil || enter.OrderID != orderID || enter.OrderType != banexg.OdTypeMarket ||
enter.Status == ormo.OdStatusClosed || enter.Filled > AmtDust {
lock.Unlock()
continue
}
beforeFilled, beforeStatus := enter.Filled, od.Status
err = o.applyAuthoritativeEnterOrder(od, res)
changed := enter.Filled > beforeFilled+AmtDust || od.Status != beforeStatus
lock.Unlock()
if err != nil {
log.Error("reconcile pending market entry apply fail", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID), zap.Error(err))
continue
}
if changed {
log.Info("reconciled pending market entry", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID),
zap.Float64("filled", od.Enter.Filled), zap.Float64("average", od.Enter.Average))
Comment on lines +1372 to +1382

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: You unlock the order and then read od.Enter fields for logging, which races with other goroutines that mutate the same order under its lock. Capture the values you want to log before unlocking (or re-lock for read) to avoid concurrent unsynchronized access. [race condition]

Severity Level: Major ⚠️
- ❌ Data race possible when reconciling stale market entries.
- ⚠️ Logs may show inconsistent filled and average values.
- ⚠️ Race detector could flag goroutine at reconciliation logging.
Steps of Reproduction ✅
1. The live scheduler in `live/common.go:305-309` periodically obtains a `LiveOrderMgr`
via `biz.GetLiveOdMgr(account)` and calls `_, err := odMgr.SyncLocalOrders()`, driving the
reconciliation path on real trading accounts.

2. Inside `SyncLocalOrders()` (`biz/odmgr_live.go:131-166`), open orders are collected
into `pendingEntries` and `o.reconcilePendingMarketEntries(pendingEntries)` is invoked at
line 164 to repair stale market entry orders whose WebSocket fills were missed.

3. In `reconcilePendingMarketEntries()` (`biz/odmgr_live.go:131-185`), for each eligible
order, the function acquires `lock := od.Lock()`, inspects `od.Enter`, unlocks to call
`exg.Default.FetchOrder`, and then re-locks; while holding the lock, it calls `err =
o.applyAuthoritativeEnterOrder(od, res)` at line 1371 and computes `changed :=
enter.Filled > beforeFilled+AmtDust || od.Status != beforeStatus` at line 1372.

4. The code then executes `lock.Unlock()` at line 1373 and immediately performs logging at
line 1382: `log.Info("reconciled pending market entry", ..., zap.Float64("filled",
od.Enter.Filled), zap.Float64("average", od.Enter.Average))`, reading `od.Enter` fields
without any lock, which can race with other goroutines like `ConsumeOrderQueue()`
(`biz/odmgr_live.go:1206-1258`) and `handleMyTrade()` (`biz/odmgr_live.go:1386-1481`) that
concurrently call `od.Lock()` and mutate `od.Enter` / `od.Status`; running `go test ./biz
-race` while these paths operate on the same order ID would surface a data race at this
logging site.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** biz/odmgr_live.go
**Line:** 1372:1382
**Comment:**
	*Race Condition: You unlock the order and then read `od.Enter` fields for logging, which races with other goroutines that mutate the same order under its lock. Capture the values you want to log before unlocking (or re-lock for read) to avoid concurrent unsynchronized access.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
Comment on lines +1372 to +1383

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Data race on od.Enter fields during logging.

Reading od.Enter.Filled and od.Enter.Average for logging after releasing the lock can result in a data race if another goroutine modifies them concurrently. Capture these values into local variables before unlocking to ensure thread safety.

🐛 Proposed fix
 		beforeFilled, beforeStatus := enter.Filled, od.Status
 		err = o.applyAuthoritativeEnterOrder(od, res)
 		changed := enter.Filled > beforeFilled+AmtDust || od.Status != beforeStatus
+		newFilled := enter.Filled
+		newAverage := enter.Average
 		lock.Unlock()
 		if err != nil {
 			log.Error("reconcile pending market entry apply fail", zap.String("acc", o.Account),
 				zap.String("key", od.Key()), zap.String("orderId", orderID), zap.Error(err))
 			continue
 		}
 		if changed {
 			log.Info("reconciled pending market entry", zap.String("acc", o.Account),
 				zap.String("key", od.Key()), zap.String("orderId", orderID),
-				zap.Float64("filled", od.Enter.Filled), zap.Float64("average", od.Enter.Average))
+				zap.Float64("filled", newFilled), zap.Float64("average", newAverage))
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
changed := enter.Filled > beforeFilled+AmtDust || od.Status != beforeStatus
lock.Unlock()
if err != nil {
log.Error("reconcile pending market entry apply fail", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID), zap.Error(err))
continue
}
if changed {
log.Info("reconciled pending market entry", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID),
zap.Float64("filled", od.Enter.Filled), zap.Float64("average", od.Enter.Average))
}
changed := enter.Filled > beforeFilled+AmtDust || od.Status != beforeStatus
newFilled := enter.Filled
newAverage := enter.Average
lock.Unlock()
if err != nil {
log.Error("reconcile pending market entry apply fail", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID), zap.Error(err))
continue
}
if changed {
log.Info("reconciled pending market entry", zap.String("acc", o.Account),
zap.String("key", od.Key()), zap.String("orderId", orderID),
zap.Float64("filled", newFilled), zap.Float64("average", newAverage))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@biz/odmgr_live.go` around lines 1372 - 1383, In the reconciliation flow
around the changed calculation, capture od.Enter.Filled and od.Enter.Average
into local variables while lock is held, before lock.Unlock(), and use those
locals in the subsequent “reconciled pending market entry” log. Keep the
existing changed check and logging behavior unchanged.

Comment on lines +1363 to +1383

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Racy reconcile log reads 🐞 Bug ☼ Reliability

reconcilePendingMarketEntries logs od.Enter.Filled/Average after releasing od.Lock(), which
can race with concurrent order updates and trigger Go’s race detector / produce inconsistent logs.
Capture the values while holding the order lock (or re-lock for reading) before logging.
Agent Prompt
### Issue description
`reconcilePendingMarketEntries` unlocks the order and then reads `od.Enter.Filled` and `od.Enter.Average` for logging. Other goroutines mutate these fields under `od.Lock()` (e.g., trade handling / order queue), so the unlocked reads can race.

### Issue Context
Order mutation is synchronized via `InOutOrder.Lock()`; logging should not read mutable fields after releasing that lock.

### Fix Focus Areas
- biz/odmgr_live.go[1330-1384]

### Suggested fix
While holding `od.Lock()` (before `lock.Unlock()`), copy any values you need for logging into local variables (e.g., `filled := enter.Filled`, `avg := enter.Average`, maybe `status := od.Status`). After unlocking, log using the copied locals instead of reading `od.Enter.*`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
}
func (o *LiveOrderMgr) handleMyTrade(trade *banexg.MyTrade) {
if _, ok := core.PairsMap[trade.Symbol]; !ok {
// 忽略不处理的交易对
Expand Down