70 lines
2.2 KiB
Go
70 lines
2.2 KiB
Go
package risk
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// DailyTradeCounter abstracts the trade-counting dependency so Guardian
|
|
// can be tested without a real database.
|
|
type DailyTradeCounter interface {
|
|
CountToday(ctx context.Context) (int, error)
|
|
}
|
|
|
|
// Limits holds the four risk thresholds from config.
|
|
// A zero value for any field means that check is disabled.
|
|
type Limits struct {
|
|
MaxDailyTrades int
|
|
MaxPositionSizeUSD float64
|
|
StopLossPercent float64
|
|
TakeProfitPercent float64
|
|
}
|
|
|
|
type Guardian struct {
|
|
limits Limits
|
|
counter DailyTradeCounter
|
|
}
|
|
|
|
func NewGuardian(limits Limits, counter DailyTradeCounter) *Guardian {
|
|
return &Guardian{limits: limits, counter: counter}
|
|
}
|
|
|
|
// PreTradeCheck validates per-trade constraints before execution.
|
|
// Returns nil if the trade is allowed, a descriptive error if blocked.
|
|
func (g *Guardian) PreTradeCheck(ctx context.Context, tradeUSDValue float64) error {
|
|
if g.limits.MaxPositionSizeUSD > 0 && tradeUSDValue > g.limits.MaxPositionSizeUSD {
|
|
return fmt.Errorf("trade blocked: position size $%.2f exceeds max $%.2f",
|
|
tradeUSDValue, g.limits.MaxPositionSizeUSD)
|
|
}
|
|
|
|
if g.limits.MaxDailyTrades > 0 && g.counter != nil {
|
|
count, err := g.counter.CountToday(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("trade blocked: unable to verify daily trade count: %w", err)
|
|
}
|
|
if count >= g.limits.MaxDailyTrades {
|
|
return fmt.Errorf("trade blocked: daily limit of %d trades reached (%d executed today)",
|
|
g.limits.MaxDailyTrades, count)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PortfolioCheck evaluates portfolio-level circuit breakers.
|
|
// pnlPercent is the unrealized P&L as a percentage (e.g. -8.5 means down 8.5%).
|
|
// Returns nil if trading should continue, a descriptive error if a breaker tripped.
|
|
func (g *Guardian) PortfolioCheck(pnlPercent float64) error {
|
|
if g.limits.StopLossPercent > 0 && pnlPercent <= -g.limits.StopLossPercent {
|
|
return fmt.Errorf("STOP-LOSS triggered: portfolio down %.2f%% (threshold: -%.2f%%)",
|
|
pnlPercent, g.limits.StopLossPercent)
|
|
}
|
|
|
|
if g.limits.TakeProfitPercent > 0 && pnlPercent >= g.limits.TakeProfitPercent {
|
|
return fmt.Errorf("TAKE-PROFIT triggered: portfolio up %.2f%% (threshold: +%.2f%%)",
|
|
pnlPercent, g.limits.TakeProfitPercent)
|
|
}
|
|
|
|
return nil
|
|
}
|