Skip to content

Commit 2434084

Browse files
author
Ryan Mitchell
committed
feat: 單向淨持倉雙向網格(BOTH)與 Bot 創建衝突修正
- 合約 BOTH:槽位 PositionLeg、adjustOrdersBoth、PnL/全平/撤單適配;short_open_window_size - 現貨 BOTH 啟動降級 LONG;CloseAllPositions BOTH 走 LiquidateAll - POST /api/bots/create:已停止 Bot 不阻擋新建;運行中衝突不依賴快照內必存在 - 中英 directionHint;版本 3.105.0-rc2 Made-with: Cursor
1 parent ff5c36c commit 2434084

13 files changed

Lines changed: 927 additions & 95 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
所有重要的專案更新都會記錄在此檔案中。
44

5+
## [3.105.0-rc2] - 2026-04-15
6+
7+
### Fixed
8+
- **`POST /api/bots/create`**`1b` 對主配置快照中**任意**同腿 Bot 一律 `BotsConflict` 拒絕,導致「僅有已停止 Bot」時仍 **409****2** 僅在 `cfg.Bots` 內找到運行中 ID 才擋,運行中 Bot **未寫回快照**時漏擋。改為:**1b** 在運行時可確認該 Bot **未在跑**(或已停用)時不阻擋;**2****`ListBots` + `GetBot`/最小腿信息**`BotsConflict`
9+
- **Web i18n(en-US)**`botCreate.directionHint` 與中文語義對齊(單向淨持倉 BOTH、現貨降級說明)。
10+
11+
---
12+
13+
## [3.105.0-rc1] - 2026-04-15
14+
15+
### Added
16+
- **單向淨持倉雙向網格(`direction: BOTH`**:合約網格下方買開多、上方賣開空,槽位 **`PositionLeg`** 區分多/空腿;平倉仍 **`reduce_only`**。新增可選 **`short_open_window_size`**(未設時繼承 `sell_window_size` / `buy_window_size`)。現貨選 BOTH 時啟動時自動降級為 LONG。`SuperPositionManager` 新增 **`adjustOrdersBoth`**、PnL/全平/撤開倉單等路徑已適配。
17+
18+
---
19+
520
## [3.104.0-rc8] - 2026-04-15
621

722
### Fixed

bot_manager.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,10 +1015,16 @@ func (br *BotRuntime) CloseAllPositions(ctx context.Context, method string, time
10151015
return fmt.Errorf("unable to get current price")
10161016
}
10171017

1018+
if br.Config.GetDirection() == "BOTH" {
1019+
// 單向淨持倉雙向網格:按槽位腿別分別平多/平空
1020+
spm.LiquidateAll()
1021+
return nil
1022+
}
1023+
10181024
// 确定平仓方向
10191025
var side string
10201026
if br.Config.GetDirection() == "SHORT" {
1021-
side = "BUY" // 做空平仓是买入
1027+
side = "BUY" // 做空平仓是买入
10221028
} else {
10231029
side = "SELL" // 做多平仓是卖出
10241030
}

config/bot_config.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ type GridConfig struct {
7070
ProfitSpread float64 `yaml:"profit_spread,omitempty" json:"profit_spread,omitempty"`
7171
OrderQuantity float64 `yaml:"order_quantity" json:"order_quantity"`
7272
MinOrderValue float64 `yaml:"min_order_value" json:"min_order_value"`
73-
BuyWindowSize int `yaml:"buy_window_size" json:"buy_window_size"`
74-
SellWindowSize int `yaml:"sell_window_size" json:"sell_window_size"`
73+
BuyWindowSize int `yaml:"buy_window_size" json:"buy_window_size"`
74+
SellWindowSize int `yaml:"sell_window_size" json:"sell_window_size"`
75+
ShortOpenWindowSize int `yaml:"short_open_window_size,omitempty" json:"short_open_window_size,omitempty"`
7576

7677
Direction string `yaml:"direction,omitempty" json:"direction,omitempty"` // LONG/SHORT/BOTH
7778
PriceLow float64 `yaml:"price_low,omitempty" json:"price_low,omitempty"`
@@ -388,9 +389,10 @@ func ConvertFromBotConfig(bc BotConfig) *BotConfigFile {
388389
ProfitSpread: bc.ProfitSpread,
389390
OrderQuantity: bc.OrderQuantity,
390391
MinOrderValue: bc.MinOrderValue,
391-
BuyWindowSize: bc.BuyWindowSize,
392-
SellWindowSize: bc.SellWindowSize,
393-
Direction: bc.Direction,
392+
BuyWindowSize: bc.BuyWindowSize,
393+
SellWindowSize: bc.SellWindowSize,
394+
ShortOpenWindowSize: bc.ShortOpenWindowSize,
395+
Direction: bc.Direction,
394396
PriceLow: bc.PriceLow,
395397
PriceHigh: bc.PriceHigh,
396398
TriggerPrice: bc.TriggerPrice,
@@ -456,6 +458,7 @@ func ConvertToBotConfig(bcf *BotConfigFile) BotConfig {
456458
MinOrderValue: bcf.Grid.MinOrderValue,
457459
BuyWindowSize: bcf.Grid.BuyWindowSize,
458460
SellWindowSize: bcf.Grid.SellWindowSize,
461+
ShortOpenWindowSize: bcf.Grid.ShortOpenWindowSize,
459462
Direction: bcf.Grid.Direction,
460463
PriceLow: bcf.Grid.PriceLow,
461464
PriceHigh: bcf.Grid.PriceHigh,

config/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,8 @@ type Config struct {
486486
MinOrderValue float64 `yaml:"min_order_value"` // 最小訂單價值(USDT),預設 6U,小於此值不掛單
487487
BuyWindowSize int `yaml:"buy_window_size"`
488488
SellWindowSize int `yaml:"sell_window_size"` // 賣單視窗大小
489+
// ShortOpenWindowSize BOTH 模式:向上開空層數;0 時用 SellWindowSize,仍為 0 則用 BuyWindowSize
490+
ShortOpenWindowSize int `yaml:"short_open_window_size" json:"short_open_window_size"`
489491
ReconcileInterval int `yaml:"reconcile_interval"`
490492
OrderCleanupThreshold int `yaml:"order_cleanup_threshold"` // 訂單清理上限(預設 100)
491493
CleanupBatchSize int `yaml:"cleanup_batch_size"` // 清理批次大小(預設 10)
@@ -1276,6 +1278,7 @@ type SymbolConfig struct {
12761278
MinOrderValue float64 `yaml:"min_order_value" json:"min_order_value"` // 最小訂單價值
12771279
BuyWindowSize int `yaml:"buy_window_size" json:"buy_window_size"` // 買單窗口(主配置,未配置 profiles 时使用)
12781280
SellWindowSize int `yaml:"sell_window_size" json:"sell_window_size"` // 賣單視窗(主配置,未配置 profiles 时使用)
1281+
ShortOpenWindowSize int `yaml:"short_open_window_size,omitempty" json:"short_open_window_size,omitempty"` // BOTH:向上開空層數;0=繼承 sell/buy
12791282
ReconcileInterval int `yaml:"reconcile_interval" json:"reconcile_interval"` // 對账间隔(秒)
12801283
OrderCleanupThreshold int `yaml:"order_cleanup_threshold" json:"order_cleanup_threshold"` // 訂單清理上限
12811284
CleanupBatchSize int `yaml:"cleanup_batch_size" json:"cleanup_batch_size"` // 清理批次大小
@@ -1411,6 +1414,7 @@ type BotConfig struct {
14111414
MinOrderValue float64 `yaml:"min_order_value" json:"min_order_value"` // 最小訂單價值
14121415
BuyWindowSize int `yaml:"buy_window_size" json:"buy_window_size"` // 買單窗口
14131416
SellWindowSize int `yaml:"sell_window_size" json:"sell_window_size"` // 賣單視窗
1417+
ShortOpenWindowSize int `yaml:"short_open_window_size,omitempty" json:"short_open_window_size,omitempty"` // BOTH:向上開空層數,0 繼承 sell/buy 窗口
14141418
ReconcileInterval int `yaml:"reconcile_interval" json:"reconcile_interval"` // 對賬間隔(秒)
14151419
OrderCleanupThreshold int `yaml:"order_cleanup_threshold" json:"order_cleanup_threshold"` // 訂單清理上限
14161420
CleanupBatchSize int `yaml:"cleanup_batch_size" json:"cleanup_batch_size"` // 清理批次大小
@@ -1600,6 +1604,7 @@ func SymbolConfigToBotConfig(sc SymbolConfig, exchangeTestnet bool) BotConfig {
16001604
MinOrderValue: sc.MinOrderValue,
16011605
BuyWindowSize: sc.BuyWindowSize,
16021606
SellWindowSize: sc.SellWindowSize,
1607+
ShortOpenWindowSize: sc.ShortOpenWindowSize,
16031608
ReconcileInterval: sc.ReconcileInterval,
16041609
OrderCleanupThreshold: sc.OrderCleanupThreshold,
16051610
CleanupBatchSize: sc.CleanupBatchSize,
@@ -1681,6 +1686,7 @@ func BotConfigToSymbolConfig(bc BotConfig) SymbolConfig {
16811686
MinOrderValue: bc.MinOrderValue,
16821687
BuyWindowSize: bc.BuyWindowSize,
16831688
SellWindowSize: bc.SellWindowSize,
1689+
ShortOpenWindowSize: bc.ShortOpenWindowSize,
16841690
ReconcileInterval: bc.ReconcileInterval,
16851691
OrderCleanupThreshold: bc.OrderCleanupThreshold,
16861692
CleanupBatchSize: bc.CleanupBatchSize,
@@ -2512,6 +2518,7 @@ func (c *Config) Validate() error {
25122518
MinOrderValue: c.Trading.MinOrderValue,
25132519
BuyWindowSize: c.Trading.BuyWindowSize,
25142520
SellWindowSize: c.Trading.SellWindowSize,
2521+
ShortOpenWindowSize: c.Trading.ShortOpenWindowSize,
25152522
Direction: direction,
25162523
ReconcileInterval: c.Trading.ReconcileInterval,
25172524
OrderCleanupThreshold: c.Trading.OrderCleanupThreshold,
@@ -2543,6 +2550,7 @@ func (c *Config) Validate() error {
25432550
c.Trading.MinOrderValue = primary.MinOrderValue
25442551
c.Trading.BuyWindowSize = primary.BuyWindowSize
25452552
c.Trading.SellWindowSize = primary.SellWindowSize
2553+
c.Trading.ShortOpenWindowSize = primary.ShortOpenWindowSize
25462554
c.Trading.ReconcileInterval = primary.ReconcileInterval
25472555
c.Trading.OrderCleanupThreshold = primary.OrderCleanupThreshold
25482556
c.Trading.CleanupBatchSize = primary.CleanupBatchSize

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import (
4545
)
4646

4747
// Version 应用版本号
48-
var Version = "3.104.0-rc8"
48+
var Version = "3.105.0-rc2"
4949

5050
// capitalDataSourceAdapter 资金數據源适配器
5151
type capitalDataSourceAdapter struct {

position/both_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package position
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestBothSideIsOpen(t *testing.T) {
8+
empty := &InventorySlot{PositionStatus: PositionStatusEmpty, PositionQty: 0, PositionLeg: PositionLegNone}
9+
if !bothSideIsOpen("BUY", empty) || !bothSideIsOpen("SELL", empty) {
10+
t.Fatal("empty slot should allow both open sides")
11+
}
12+
long := &InventorySlot{PositionStatus: PositionStatusFilled, PositionQty: 1, PositionLeg: PositionLegLong}
13+
if !bothSideIsOpen("BUY", long) || bothSideIsOpen("SELL", long) {
14+
t.Fatal("long leg: only BUY is open")
15+
}
16+
sh := &InventorySlot{PositionStatus: PositionStatusFilled, PositionQty: 1, PositionLeg: PositionLegShort}
17+
if !bothSideIsOpen("SELL", sh) || bothSideIsOpen("BUY", sh) {
18+
t.Fatal("short leg: only SELL is open")
19+
}
20+
}

0 commit comments

Comments
 (0)