Skip to content
Merged
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ func main() {
capturedHub.UpdateCandlePrice(capturedSym, sample.Price, sample.Timestamp)
depth := capturedManaged.GetDepth()
capturedHub.BroadcastDepth(capturedSym, depth)
if capturedBus != nil {
_ = capturedBus.Pub.PublishGBMTick(ctx, eventbus.GBMTickEvent{
Symbol: capturedSym,
BasePrice: int64(sample.Price * 100),
Timestamp: time.UnixMilli(sample.Timestamp).UTC(),
})
}
})
}

Expand Down
60 changes: 59 additions & 1 deletion internal/api/bot_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,63 @@ func (h *BotHandler) SaveStrategy(c *gin.Context) {
return
}

c.JSON(http.StatusOK, gin.H{"message": "Strategy saved successfully"})
c.JSON(http.StatusOK, gin.H{
"message": "Strategy saved successfully",
"data": gin.H{
"name": req.Name,
},
})
}

// ListStrategies returns saved strategy names for the current user.
func (h *BotHandler) ListStrategies(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found in token"})
return
}
items, err := h.botManager.ListUserStrategies(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list strategies: " + err.Error()})
return
}
type item struct {
Name string `json:"name"`
UpdatedAt string `json:"updated_at"`
}
resp := make([]item, 0, len(items))
for _, s := range items {
resp = append(resp, item{Name: s.Name, UpdatedAt: s.UpdatedAt.Format("2006-01-02T15:04:05Z07:00")})
}
c.JSON(http.StatusOK, gin.H{"data": resp})
}

// GetStrategy returns one saved strategy by name for the current user.
func (h *BotHandler) GetStrategy(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found in token"})
return
}
name := c.Param("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "strategy name is required"})
return
}
strategyJSON, err := h.botManager.LoadUserStrategy(userID, name)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Strategy not found"})
return
}
var strategy interface{}
if err := json.Unmarshal(strategyJSON, &strategy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Stored strategy is invalid JSON"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"name": name,
"strategy": strategy,
},
})
}
78 changes: 78 additions & 0 deletions internal/api/order_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"log"
"net"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -242,6 +243,83 @@ func (h *OrderHandler) PlaceMarketOrder(c *gin.Context) {
})
}

// PlaceInternalMarketOrder handles POST /internal/simbot/order/market
// Loopback-only endpoint used by in-process simulation bots.
func (h *OrderHandler) PlaceInternalMarketOrder(c *gin.Context) {
host, _, err := net.SplitHostPort(c.Request.RemoteAddr)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid remote address"})
return
}
ip := net.ParseIP(host)
if ip == nil || !ip.IsLoopback() {
c.JSON(http.StatusUnauthorized, gin.H{"error": "internal endpoint is loopback-only"})
return
}

var req MarketOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request: " + err.Error()})
return
}
side, valid := parseSide(string(req.Side))
if !valid {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid side: must be 'buy' or 'sell'"})
return
}
if h.mktMgr.GetSymbol(req.Symbol) == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "unknown symbol"})
return
}

book := h.bookManager.GetOrCreate(req.Symbol)
orderID, status, result := book.MarketAutoID(side, int(req.Quantity))
if status != engine.StatusOK {
c.JSON(http.StatusInternalServerError, gin.H{"error": "engine error: " + status.Error()})
return
}

if h.bus != nil {
filledQty := int64(0)
var totalCost int64
for _, t := range result.Trades {
filledQty += int64(t.Qty)
totalCost += int64(t.Price) * int64(t.Qty)
}
orderStatus := models.StatusFilled
if filledQty < int64(req.Quantity) {
orderStatus = models.StatusPartiallyFilled
}
avgPrice := int64(0)
if filledQty > 0 {
avgPrice = totalCost / filledQty
}
_ = h.bus.PublishOrderUpdate(context.Background(), eventbus.OrderUpdateEvent{
OrderID: int64(orderID),
UserID: "internal-bot",
Symbol: req.Symbol,
Side: req.Side,
Type: models.TypeMarket,
Status: orderStatus,
Quantity: int64(req.Quantity),
FilledQty: filledQty,
RemainingQty: int64(req.Quantity) - filledQty,
AvgPrice: avgPrice,
UpdatedAt: time.Now().UTC(),
})
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "internal market order executed",
"order_id": orderID,
"symbol": req.Symbol,
"side": req.Side,
"quantity": req.Quantity,
"trades": result.Trades,
})
}

// ---------------------------------------------------------------------------
// Limit order lifecycle
// ---------------------------------------------------------------------------
Expand Down
Loading
Loading