From 6d76c60fc19c5495d7260baaf0db02953e7482c7 Mon Sep 17 00:00:00 2001 From: caspar Date: Fri, 20 Mar 2026 12:38:18 -0400 Subject: [PATCH] Refactored handler for alerts. --- Backend.cs | 5 +- DatabaseHandler/HistoricalRequestParser.cs | 4 + MarketDataRequest/HistoricalEquityRequest.cs | 8 ++ ServiceHandler/PercentChangeEvaluator.cs | 44 +++++++ ServiceHandler/PriceSpikeEvaluator.cs | 71 ++++++++++ ServiceHandler/TrailingPriceEvaluator.cs | 73 +++++++++++ docs/alerts-api.md | 131 +++++++++++++++++++ 7 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 ServiceHandler/PercentChangeEvaluator.cs create mode 100644 ServiceHandler/PriceSpikeEvaluator.cs create mode 100644 ServiceHandler/TrailingPriceEvaluator.cs create mode 100644 docs/alerts-api.md diff --git a/Backend.cs b/Backend.cs index 69aefc2..b9e9a4a 100644 --- a/Backend.cs +++ b/Backend.cs @@ -15,7 +15,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +//builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddSingleton(); @@ -25,6 +25,9 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddCors(options => diff --git a/DatabaseHandler/HistoricalRequestParser.cs b/DatabaseHandler/HistoricalRequestParser.cs index c16653d..73a9ed8 100644 --- a/DatabaseHandler/HistoricalRequestParser.cs +++ b/DatabaseHandler/HistoricalRequestParser.cs @@ -84,6 +84,10 @@ Console.WriteLine(raw); HistoricalBarSize.FiveMins => 300, HistoricalBarSize.FifteenMins => 900, HistoricalBarSize.OneHour => 3600, + HistoricalBarSize.TwoHours => 7200, + HistoricalBarSize.ThreeHours => 10800, + HistoricalBarSize.FourHours => 14400, + HistoricalBarSize.EightHours => 28800, HistoricalBarSize.OneDay => 86400, HistoricalBarSize.FiveDays => 432000, HistoricalBarSize.OneWeek => 604800, diff --git a/MarketDataRequest/HistoricalEquityRequest.cs b/MarketDataRequest/HistoricalEquityRequest.cs index 90a3a31..9584cf0 100644 --- a/MarketDataRequest/HistoricalEquityRequest.cs +++ b/MarketDataRequest/HistoricalEquityRequest.cs @@ -6,6 +6,10 @@ namespace Backend.MarketDataRequest FiveMins, FifteenMins, OneHour, + TwoHours, + ThreeHours, + FourHours, + EightHours, OneDay, FiveDays, OneWeek, @@ -70,6 +74,10 @@ namespace Backend.MarketDataRequest HistoricalBarSize.FiveMins => "5 mins", HistoricalBarSize.FifteenMins => "15 mins", HistoricalBarSize.OneHour => "1 hour", + HistoricalBarSize.TwoHours => "2 hours", + HistoricalBarSize.ThreeHours => "3 hours", + HistoricalBarSize.FourHours => "4 hours", + HistoricalBarSize.EightHours => "8 hours", HistoricalBarSize.OneDay => "1 day", HistoricalBarSize.FiveDays => "5 days", HistoricalBarSize.OneWeek => "1 week", diff --git a/ServiceHandler/PercentChangeEvaluator.cs b/ServiceHandler/PercentChangeEvaluator.cs new file mode 100644 index 0000000..2ced43f --- /dev/null +++ b/ServiceHandler/PercentChangeEvaluator.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using Backend.DatabaseHandler; +using Backend.Interface; + +namespace Backend.ServiceHander; + +public sealed class PercentChangeEvaluator : IAlertEvaluator +{ + public string AlertType => "percent_change"; + + public Task EvaluateAsync(AlertRecord alert, MarketDataService marketData) + { + var parameters = JsonSerializer.Deserialize(alert.Parameters) + ?? throw new InvalidOperationException($"Invalid parameters for alert {alert.Id}"); + + var snapshot = marketData.GetLiveQuote(alert.Symbol); + + if (!snapshot.LastPrice.HasValue || !snapshot.PreviousClose.HasValue || snapshot.PreviousClose.Value == 0) + { + return Task.FromResult(new AlertEvaluation { Triggered = false }); + } + + double price = snapshot.LastPrice.Value; + double prevClose = snapshot.PreviousClose.Value; + double percentChange = (price - prevClose) / prevClose * 100.0; + + bool triggered = parameters.PercentThreshold >= 0 + ? percentChange >= parameters.PercentThreshold + : percentChange <= parameters.PercentThreshold; + + string updatedState = JsonSerializer.Serialize(new { lastPrice = price, percentChange }); + + return Task.FromResult(new AlertEvaluation + { + Triggered = triggered, + UpdatedLastState = updatedState + }); + } + + private sealed class PercentChangeParameters + { + public double PercentThreshold { get; set; } + } +} diff --git a/ServiceHandler/PriceSpikeEvaluator.cs b/ServiceHandler/PriceSpikeEvaluator.cs new file mode 100644 index 0000000..0e38714 --- /dev/null +++ b/ServiceHandler/PriceSpikeEvaluator.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Backend.DatabaseHandler; +using Backend.Interface; + +namespace Backend.ServiceHander; + +public sealed class PriceSpikeEvaluator : IAlertEvaluator +{ + public string AlertType => "price_spike"; + + public Task EvaluateAsync(AlertRecord alert, MarketDataService marketData) + { + var parameters = JsonSerializer.Deserialize(alert.Parameters) + ?? throw new InvalidOperationException($"Invalid parameters for alert {alert.Id}"); + + var snapshot = marketData.GetLiveQuote(alert.Symbol); + + if (!snapshot.LastPrice.HasValue) + { + return Task.FromResult(new AlertEvaluation { Triggered = false }); + } + + double price = snapshot.LastPrice.Value; + var now = DateTime.UtcNow; + + var state = JsonSerializer.Deserialize(alert.LastState) + ?? new PriceSpikeState(); + + state.Prices ??= new List(); + state.Prices.Add(new PricePoint { Price = price, Timestamp = now }); + + var cutoff = now.AddMinutes(-parameters.WindowMinutes); + state.Prices.RemoveAll(p => p.Timestamp < cutoff); + + bool triggered = false; + if (state.Prices.Count >= 2) + { + double oldest = state.Prices[0].Price; + if (oldest != 0) + { + double change = Math.Abs((price - oldest) / oldest * 100.0); + triggered = change >= parameters.PercentChange; + } + } + + string updatedState = JsonSerializer.Serialize(state); + + return Task.FromResult(new AlertEvaluation + { + Triggered = triggered, + UpdatedLastState = updatedState + }); + } + + private sealed class PriceSpikeParameters + { + public double PercentChange { get; set; } + public int WindowMinutes { get; set; } = 15; + } + + private sealed class PriceSpikeState + { + public List? Prices { get; set; } + } + + private sealed class PricePoint + { + public double Price { get; set; } + public DateTime Timestamp { get; set; } + } +} diff --git a/ServiceHandler/TrailingPriceEvaluator.cs b/ServiceHandler/TrailingPriceEvaluator.cs new file mode 100644 index 0000000..fd07c4d --- /dev/null +++ b/ServiceHandler/TrailingPriceEvaluator.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Backend.DatabaseHandler; +using Backend.Interface; + +namespace Backend.ServiceHander; + +public sealed class TrailingPriceEvaluator : IAlertEvaluator +{ + public string AlertType => "trailing_price"; + + public Task EvaluateAsync(AlertRecord alert, MarketDataService marketData) + { + var parameters = JsonSerializer.Deserialize(alert.Parameters) + ?? throw new InvalidOperationException($"Invalid parameters for alert {alert.Id}"); + + var snapshot = marketData.GetLiveQuote(alert.Symbol); + + if (!snapshot.LastPrice.HasValue) + { + return Task.FromResult(new AlertEvaluation { Triggered = false }); + } + + double price = snapshot.LastPrice.Value; + var state = JsonSerializer.Deserialize(alert.LastState) + ?? new TrailingPriceState(); + + bool triggered; + + if (parameters.Direction.Equals("bullish", StringComparison.OrdinalIgnoreCase)) + { + if (!state.PeakPrice.HasValue || price > state.PeakPrice.Value) + state.PeakPrice = price; + + double dropPercent = (state.PeakPrice.Value - price) / state.PeakPrice.Value * 100.0; + triggered = dropPercent >= parameters.TrailPercent; + } + else + { + if (!state.TroughPrice.HasValue || price < state.TroughPrice.Value) + state.TroughPrice = price; + + double risePercent = state.TroughPrice.Value == 0 + ? 0 + : (price - state.TroughPrice.Value) / state.TroughPrice.Value * 100.0; + triggered = risePercent >= parameters.TrailPercent; + } + + string updatedState = JsonSerializer.Serialize(new + { + peakPrice = state.PeakPrice, + troughPrice = state.TroughPrice, + lastPrice = price + }); + + return Task.FromResult(new AlertEvaluation + { + Triggered = triggered, + UpdatedLastState = updatedState + }); + } + + private sealed class TrailingPriceParameters + { + public string Direction { get; set; } = string.Empty; + public double TrailPercent { get; set; } + } + + private sealed class TrailingPriceState + { + public double? PeakPrice { get; set; } + public double? TroughPrice { get; set; } + } +} diff --git a/docs/alerts-api.md b/docs/alerts-api.md new file mode 100644 index 0000000..d259751 --- /dev/null +++ b/docs/alerts-api.md @@ -0,0 +1,131 @@ +# Alerts API + +Base URL: `https://api.dongfeng-systems.org` + +--- + +## Create Alert + +**POST** `/api/alerts/create` + +| Field | Type | Required | Description | +|---|---|---|---| +| `username` | string | yes | User identifier | +| `symbol` | string | yes | Ticker symbol (e.g. AAPL) | +| `alertType` | string | yes | One of the alert types below | +| `parameters` | string (JSON) | yes | JSON string with type-specific parameters | + +### Alert Types & Parameters + +**`price_threshold`** — Triggers when price crosses a target value. + +```json +{ "direction": "above", "target": 200.0 } +``` + +| Param | Type | Description | +|---|---|---| +| `direction` | string | `"above"` or `"below"` | +| `target` | number | Target price | + +**`percent_change`** — Triggers when daily percent change from previous close crosses a threshold. + +```json +{ "percentThreshold": 5.0 } +``` + +| Param | Type | Description | +|---|---|---| +| `percentThreshold` | number | Positive triggers on gains, negative on losses | + +**`price_spike`** — Triggers when price moves sharply within a rolling time window. + +```json +{ "percentChange": 3.0, "windowMinutes": 10 } +``` + +| Param | Type | Description | +|---|---|---| +| `percentChange` | number | Minimum percent move to trigger | +| `windowMinutes` | number | Rolling window size in minutes (default: 15) | + +**`trailing_price`** — Tracks a price extreme and triggers on reversal. + +```json +{ "direction": "bullish", "trailPercent": 5.0 } +``` + +| Param | Type | Description | +|---|---|---| +| `direction` | string | `"bullish"` (triggers on drop from peak) or `"bearish"` (triggers on rise from trough) | +| `trailPercent` | number | Percent reversal from extreme to trigger | + +### Example + +```bash +curl -X POST https://api.dongfeng-systems.org/api/alerts/create \ + -H "Content-Type: application/json" \ + -d '{ + "username": "joe", + "symbol": "AAPL", + "alertType": "price_threshold", + "parameters": "{\"direction\": \"above\", \"target\": 200.0}" + }' +``` + +**Response:** + +```json +{ + "id": 1, + "username": "joe", + "symbol": "AAPL", + "alertType": "price_threshold", + "parameters": "{\"direction\": \"above\", \"target\": 200.0}", + "lastState": "{}", + "isTriggered": false, + "createdAt": "2026-03-20T12:00:00Z" +} +``` + +--- + +## List Alerts + +**GET** `/api/alerts/list?username={username}` + +Returns all alerts for a user (both active and triggered). + +```bash +curl "https://api.dongfeng-systems.org/api/alerts/list?username=joe" +``` + +--- + +## List Triggered Alerts + +**GET** `/api/alerts/triggered?username={username}` + +Returns only triggered alerts for a user. + +```bash +curl "https://api.dongfeng-systems.org/api/alerts/triggered?username=joe" +``` + +--- + +## Delete Alert + +**DELETE** `/api/alerts/delete?username={username}&id={alertId}` + +Deletes an alert. Only the owning user can delete it. + +```bash +curl -X DELETE "https://api.dongfeng-systems.org/api/alerts/delete?username=joe&id=1" +``` + +**Response:** + +```json +{ "success": true } +```