candle close alert
This commit is contained in:
parent
30a7f1b68c
commit
dd03cae0f3
10 changed files with 328 additions and 60 deletions
|
|
@ -133,32 +133,72 @@ func selectCandleInterval(gap time.Duration) provider.KlineInterval {
|
|||
}
|
||||
|
||||
// TODO: parallel checking for different instruments.
|
||||
// TODO: get one candle before interval
|
||||
|
||||
func (a *Alerter) checkAlerts(ctx context.Context) error {
|
||||
now := time.Now()
|
||||
gap := now.Sub(a.lastCheckedAt)
|
||||
candleInterval := selectCandleInterval(gap)
|
||||
|
||||
// Truncate to the selected interval boundary so we always re-check the candle
|
||||
// that was still forming at the previous tick — its High/Low may have changed
|
||||
// since.
|
||||
from := a.lastCheckedAt.Truncate(candleInterval.ToDuration())
|
||||
|
||||
a.log.Debug("checking alerts", "from", from, "to", now, "interval", candleInterval, "gap", gap.Round(time.Second))
|
||||
|
||||
instruments := a.cache.Instruments()
|
||||
|
||||
for _, instrument := range instruments {
|
||||
candles, err := a.priceProvider.Candles(ctx, instrument, from, now, candleInterval)
|
||||
if err != nil {
|
||||
a.log.Error("failed to get candles", "instrument", instrument.ID, "err", err)
|
||||
return fmt.Errorf("failed to get candles: %w", err)
|
||||
alerts := a.cache.AlertsByInstrument(instrument.ID)
|
||||
|
||||
// Separate crossing alerts (above/below) from candle-close alerts.
|
||||
var crossingAlerts []*entities.Alert
|
||||
closeByTimeframe := make(map[provider.KlineInterval][]*entities.Alert)
|
||||
|
||||
for _, alert := range alerts {
|
||||
switch alert.Condition {
|
||||
case entities.AlertConditionAbove, entities.AlertConditionBelow:
|
||||
crossingAlerts = append(crossingAlerts, alert)
|
||||
case entities.AlertConditionCloseAbove, entities.AlertConditionCloseBelow:
|
||||
tf := provider.KlineInterval(alert.Timeframe)
|
||||
closeByTimeframe[tf] = append(closeByTimeframe[tf], alert)
|
||||
}
|
||||
}
|
||||
|
||||
for _, alert := range a.cache.AlertsByInstrument(instrument.ID) {
|
||||
if triggered, price := alertTriggeredByCandles(alert, candles); triggered {
|
||||
a.triggerAlert(ctx, alert, price)
|
||||
// Check crossing alerts using an auto-selected interval for the gap.
|
||||
if len(crossingAlerts) > 0 {
|
||||
gap := now.Sub(a.lastCheckedAt)
|
||||
candleInterval := selectCandleInterval(gap)
|
||||
from := a.lastCheckedAt.Truncate(candleInterval.ToDuration())
|
||||
|
||||
a.log.Debug("checking crossing alerts", "instrument", instrument.ID,
|
||||
"from", from, "to", now, "interval", candleInterval)
|
||||
|
||||
candles, err := a.priceProvider.Candles(ctx, instrument, from, now, candleInterval)
|
||||
if err != nil {
|
||||
a.log.Error("failed to get candles for crossing alerts", "instrument", instrument.ID, "err", err)
|
||||
return fmt.Errorf("failed to get candles: %w", err)
|
||||
}
|
||||
|
||||
for _, alert := range crossingAlerts {
|
||||
if triggered, price := crossingTriggered(alert, candles); triggered {
|
||||
a.triggerAlert(ctx, alert, price)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check candle-close alerts per timeframe; only closed candles count.
|
||||
for tf, tfAlerts := range closeByTimeframe {
|
||||
from := a.lastCheckedAt.Truncate(tf.ToDuration())
|
||||
|
||||
a.log.Debug("checking close alerts", "instrument", instrument.ID,
|
||||
"from", from, "to", now, "timeframe", tf)
|
||||
|
||||
candles, err := a.priceProvider.Candles(ctx, instrument, from, now, tf)
|
||||
if err != nil {
|
||||
a.log.Error("failed to get candles for close alerts", "instrument", instrument.ID,
|
||||
"timeframe", tf, "err", err)
|
||||
return fmt.Errorf("failed to get candles: %w", err)
|
||||
}
|
||||
|
||||
// Only consider candles that have fully closed.
|
||||
closed := closedCandles(candles, tf, now)
|
||||
|
||||
for _, alert := range tfAlerts {
|
||||
if triggered, price := closeTriggered(alert, closed); triggered {
|
||||
a.triggerAlert(ctx, alert, price)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,9 +212,20 @@ func (a *Alerter) checkAlerts(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// alertTriggeredByCandles returns true and the triggering price if any candle
|
||||
// caused the alert condition to be met.
|
||||
func alertTriggeredByCandles(alert *entities.Alert, candles []entities.Candle) (bool, decimal.Decimal) {
|
||||
// closedCandles filters to candles whose close time (openTime + interval) is before now.
|
||||
func closedCandles(candles []entities.Candle, interval provider.KlineInterval, now time.Time) []entities.Candle {
|
||||
dur := interval.ToDuration()
|
||||
var result []entities.Candle
|
||||
for _, c := range candles {
|
||||
if !c.OpenTime.Add(dur).After(now) {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// crossingTriggered returns true if any candle's High/Low crosses the alert price.
|
||||
func crossingTriggered(alert *entities.Alert, candles []entities.Candle) (bool, decimal.Decimal) {
|
||||
for _, candle := range candles {
|
||||
switch alert.Condition {
|
||||
case entities.AlertConditionAbove:
|
||||
|
|
@ -190,6 +241,23 @@ func alertTriggeredByCandles(alert *entities.Alert, candles []entities.Candle) (
|
|||
return false, decimal.Zero
|
||||
}
|
||||
|
||||
// closeTriggered returns true if any closed candle's Close crosses the alert price.
|
||||
func closeTriggered(alert *entities.Alert, candles []entities.Candle) (bool, decimal.Decimal) {
|
||||
for _, candle := range candles {
|
||||
switch alert.Condition {
|
||||
case entities.AlertConditionCloseAbove:
|
||||
if candle.Close.GreaterThanOrEqual(alert.Price) {
|
||||
return true, candle.Close
|
||||
}
|
||||
case entities.AlertConditionCloseBelow:
|
||||
if candle.Close.LessThanOrEqual(alert.Price) {
|
||||
return true, candle.Close
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, decimal.Zero
|
||||
}
|
||||
|
||||
func (a *Alerter) triggerAlert(ctx context.Context, alert *entities.Alert, currentPrice decimal.Decimal) {
|
||||
if err := a.notifier.NotifyAlert(ctx, alert.UserID, alert, currentPrice); err != nil {
|
||||
a.log.Error("failed to notify alert", "alert_id", alert.ID, "err", err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue