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
6 changes: 5 additions & 1 deletion internal/api/order_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,11 @@ func (h *OrderHandler) PlaceInternalMarketOrder(c *gin.Context) {
avgPrice = totalCost / filledQty
}

if req.Mode == "live" && req.UserID != "" && len(result.Trades) > 0 && h.portfolioMgr != nil {
// Apply fills to the portfolio for any authenticated user (simulation AND live).
// Previously this was gated on mode=="live", which meant simulation-bot trades
// never updated the user's cash or positions. Now both modes update the portfolio
// so the user sees their equity change in real time.
if req.UserID != "" && len(result.Trades) > 0 && h.portfolioMgr != nil {
for _, t := range result.Trades {
if _, err := h.portfolioMgr.ApplyFill(c.Request.Context(), req.UserID, req.Symbol, strings.ToLower(string(req.Side)), float64(t.Price)/100.0, float64(t.Qty), false); err != nil {
log.Printf("[order/internal] ApplyFill error for user %s, symbol %s: %v", req.UserID, req.Symbol, err)
Expand Down
33 changes: 24 additions & 9 deletions internal/api/sim_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,50 @@ func (h *SimHandler) persistBotPnL(ctx context.Context, userID string, st simbot
}

// StartFlagshipBot starts a prebuilt advanced alpha strategy (separate from GUI builder).
//
// Optional body field: `strategy_name` selects which strategy to run.
// Supported values: flagship_v2 (default), bollinger_mean_reversion, macd_momentum,
// rsi_reversal, fast_ema_trend, macd_bollinger_breakout.
func (h *SimHandler) StartFlagshipBot(c *gin.Context) {
var req struct {
BotID string `json:"bot_id"`
Symbol string `json:"symbol" binding:"required"`
Mode string `json:"mode,omitempty"`
BotID string `json:"bot_id"`
Symbol string `json:"symbol" binding:"required"`
Mode string `json:"mode,omitempty"`
StrategyName string `json:"strategy_name,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body: " + err.Error()})
return
}

// Resolve strategy — defaults to flagship_v2 when empty
strategyGraph, strategyLabel := simbot.GetStrategy(simbot.StrategyName(req.StrategyName))

if req.BotID == "" {
req.BotID = "flagship_" + strings.ToLower(req.Symbol) + "_" + time.Now().UTC().Format("150405")
suffix := req.StrategyName
if suffix == "" {
suffix = "flagship"
}
req.BotID = suffix + "_" + strings.ToLower(req.Symbol) + "_" + time.Now().UTC().Format("150405")
}
mode := simbot.ModeSimulation
if req.Mode == string(simbot.ModeLive) {
mode = simbot.ModeLive
}
userID := c.GetString("user_id")
if err := h.manager.StartBot(req.BotID, req.Symbol, simbot.FlagshipStrategyGraph(), mode, userID, "Flagship"); err != nil {
if err := h.manager.StartBot(req.BotID, req.Symbol, strategyGraph, mode, userID, strategyLabel); err != nil {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "flagship bot started",
"data": gin.H{
"bot_id": req.BotID,
"symbol": req.Symbol,
"mode": mode,
"status": "running",
"bot_id": req.BotID,
"symbol": req.Symbol,
"mode": mode,
"status": "running",
"strategy_name": req.StrategyName,
"strategy_label": strategyLabel,
},
})
}
Expand Down
24 changes: 17 additions & 7 deletions internal/simbot/flagship_strategy.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package simbot

// FlagshipStrategyGraph returns the production prebuilt flagship strategy,
// FlagshipStrategyGraph returns the production prebuilt flagship strategy (v2),
// exposed as a one-click server-side bot.
//
// Improvements over the original:
// - EMA 9/21 instead of 20/50 — significantly faster signal generation,
// reducing lag so entries are closer to the start of a move.
// - RSI pivot at 50 instead of 55/45 — avoids buying into already-overbought
// conditions and selling into already-oversold conditions.
// - Stop-loss tightened from 1.8% to 1.2% — reduces per-trade max drawdown.
func FlagshipStrategyGraph() StrategyGraph {
return StrategyGraph{
Nodes: []BotNode{
{ID: "price", Type: NodePriceFeed, Params: map[string]interface{}{}, Label: "Price Feed"},
{ID: "emaFast", Type: NodeEMA, Params: map[string]interface{}{"period": 20}, Label: "EMA 20"},
{ID: "emaSlow", Type: NodeEMA, Params: map[string]interface{}{"period": 50}, Label: "EMA 50"},
{ID: "cross", Type: NodeCrossover, Params: map[string]interface{}{}, Label: "Cross"},
// Faster EMAs: 9 / 21 (down from original 20 / 50)
{ID: "emaFast", Type: NodeEMA, Params: map[string]interface{}{"period": 9}, Label: "EMA 9"},
{ID: "emaSlow", Type: NodeEMA, Params: map[string]interface{}{"period": 21}, Label: "EMA 21"},
{ID: "cross", Type: NodeCrossover, Params: map[string]interface{}{}, Label: "EMA Cross"},
// RSI pivot at 50 — buys only when momentum is positive, sells only when negative
{ID: "rsi", Type: NodeRSI, Params: map[string]interface{}{"period": 14}, Label: "RSI 14"},
{ID: "rsiLong", Type: NodeThreshold, Params: map[string]interface{}{"operator": ">=", "value": 55}, Label: "RSI Long"},
{ID: "rsiShort", Type: NodeThreshold, Params: map[string]interface{}{"operator": "<=", "value": 45}, Label: "RSI Short"},
{ID: "rsiLong", Type: NodeThreshold, Params: map[string]interface{}{"operator": ">=", "value": 50}, Label: "RSI Long"},
{ID: "rsiShort", Type: NodeThreshold, Params: map[string]interface{}{"operator": "<=", "value": 50}, Label: "RSI Short"},
{ID: "andLong", Type: NodeAND, Params: map[string]interface{}{}, Label: "Long Confirm"},
{ID: "andShort", Type: NodeAND, Params: map[string]interface{}{}, Label: "Short Confirm"},
{ID: "buy", Type: NodeMarketBuy, Params: map[string]interface{}{"quantity": 2}, Label: "Buy"},
{ID: "sell", Type: NodeMarketSell, Params: map[string]interface{}{"quantity": 2}, Label: "Sell"},
{ID: "stop", Type: NodeStopLoss, Params: map[string]interface{}{"threshold": 1.8, "quantity": 2}, Label: "Stop Loss"},
// Tighter stop-loss: 1.2% (down from 1.8%)
{ID: "stop", Type: NodeStopLoss, Params: map[string]interface{}{"threshold": 1.2, "quantity": 2}, Label: "Stop Loss"},
},
Edges: []BotEdge{
{ID: "e1", FromNode: "emaFast", FromPort: "result", ToNode: "cross", ToPort: "fast"},
Expand Down
Loading
Loading