Refactored handler for alerts.
This commit is contained in:
parent
0ce5225434
commit
6d76c60fc1
@ -15,7 +15,7 @@ builder.Services.AddSingleton<IUserInterestStore, PostgresUserInterestStore>();
|
|||||||
builder.Services.AddSingleton<FinBertScoringService>();
|
builder.Services.AddSingleton<FinBertScoringService>();
|
||||||
builder.Services.AddSingleton<INewsSentimentStore, PostgresNewsSentimentStore>();
|
builder.Services.AddSingleton<INewsSentimentStore, PostgresNewsSentimentStore>();
|
||||||
builder.Services.AddSingleton<NewsSentimentService>();
|
builder.Services.AddSingleton<NewsSentimentService>();
|
||||||
builder.Services.AddSingleton<EdgarFilingService>();
|
//builder.Services.AddSingleton<EdgarFilingService>();
|
||||||
builder.Services.AddHostedService<NewsSentimentRefreshService>();
|
builder.Services.AddHostedService<NewsSentimentRefreshService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IUserLocationStore, PostgresUserLocationStore>();
|
builder.Services.AddSingleton<IUserLocationStore, PostgresUserLocationStore>();
|
||||||
@ -25,6 +25,9 @@ builder.Services.AddSingleton<IAnnotationStore, PostgresAnnotationStore>();
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IAlertStore, PostgresAlertStore>();
|
builder.Services.AddSingleton<IAlertStore, PostgresAlertStore>();
|
||||||
builder.Services.AddSingleton<IAlertEvaluator, PriceThresholdEvaluator>();
|
builder.Services.AddSingleton<IAlertEvaluator, PriceThresholdEvaluator>();
|
||||||
|
builder.Services.AddSingleton<IAlertEvaluator, PercentChangeEvaluator>();
|
||||||
|
builder.Services.AddSingleton<IAlertEvaluator, PriceSpikeEvaluator>();
|
||||||
|
builder.Services.AddSingleton<IAlertEvaluator, TrailingPriceEvaluator>();
|
||||||
builder.Services.AddHostedService<AlertEvaluationService>();
|
builder.Services.AddHostedService<AlertEvaluationService>();
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
|
|||||||
@ -84,6 +84,10 @@ Console.WriteLine(raw);
|
|||||||
HistoricalBarSize.FiveMins => 300,
|
HistoricalBarSize.FiveMins => 300,
|
||||||
HistoricalBarSize.FifteenMins => 900,
|
HistoricalBarSize.FifteenMins => 900,
|
||||||
HistoricalBarSize.OneHour => 3600,
|
HistoricalBarSize.OneHour => 3600,
|
||||||
|
HistoricalBarSize.TwoHours => 7200,
|
||||||
|
HistoricalBarSize.ThreeHours => 10800,
|
||||||
|
HistoricalBarSize.FourHours => 14400,
|
||||||
|
HistoricalBarSize.EightHours => 28800,
|
||||||
HistoricalBarSize.OneDay => 86400,
|
HistoricalBarSize.OneDay => 86400,
|
||||||
HistoricalBarSize.FiveDays => 432000,
|
HistoricalBarSize.FiveDays => 432000,
|
||||||
HistoricalBarSize.OneWeek => 604800,
|
HistoricalBarSize.OneWeek => 604800,
|
||||||
|
|||||||
@ -6,6 +6,10 @@ namespace Backend.MarketDataRequest
|
|||||||
FiveMins,
|
FiveMins,
|
||||||
FifteenMins,
|
FifteenMins,
|
||||||
OneHour,
|
OneHour,
|
||||||
|
TwoHours,
|
||||||
|
ThreeHours,
|
||||||
|
FourHours,
|
||||||
|
EightHours,
|
||||||
OneDay,
|
OneDay,
|
||||||
FiveDays,
|
FiveDays,
|
||||||
OneWeek,
|
OneWeek,
|
||||||
@ -70,6 +74,10 @@ namespace Backend.MarketDataRequest
|
|||||||
HistoricalBarSize.FiveMins => "5 mins",
|
HistoricalBarSize.FiveMins => "5 mins",
|
||||||
HistoricalBarSize.FifteenMins => "15 mins",
|
HistoricalBarSize.FifteenMins => "15 mins",
|
||||||
HistoricalBarSize.OneHour => "1 hour",
|
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.OneDay => "1 day",
|
||||||
HistoricalBarSize.FiveDays => "5 days",
|
HistoricalBarSize.FiveDays => "5 days",
|
||||||
HistoricalBarSize.OneWeek => "1 week",
|
HistoricalBarSize.OneWeek => "1 week",
|
||||||
|
|||||||
44
ServiceHandler/PercentChangeEvaluator.cs
Normal file
44
ServiceHandler/PercentChangeEvaluator.cs
Normal file
@ -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<AlertEvaluation> EvaluateAsync(AlertRecord alert, MarketDataService marketData)
|
||||||
|
{
|
||||||
|
var parameters = JsonSerializer.Deserialize<PercentChangeParameters>(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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
71
ServiceHandler/PriceSpikeEvaluator.cs
Normal file
71
ServiceHandler/PriceSpikeEvaluator.cs
Normal file
@ -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<AlertEvaluation> EvaluateAsync(AlertRecord alert, MarketDataService marketData)
|
||||||
|
{
|
||||||
|
var parameters = JsonSerializer.Deserialize<PriceSpikeParameters>(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<PriceSpikeState>(alert.LastState)
|
||||||
|
?? new PriceSpikeState();
|
||||||
|
|
||||||
|
state.Prices ??= new List<PricePoint>();
|
||||||
|
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<PricePoint>? Prices { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PricePoint
|
||||||
|
{
|
||||||
|
public double Price { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
73
ServiceHandler/TrailingPriceEvaluator.cs
Normal file
73
ServiceHandler/TrailingPriceEvaluator.cs
Normal file
@ -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<AlertEvaluation> EvaluateAsync(AlertRecord alert, MarketDataService marketData)
|
||||||
|
{
|
||||||
|
var parameters = JsonSerializer.Deserialize<TrailingPriceParameters>(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<TrailingPriceState>(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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
131
docs/alerts-api.md
Normal file
131
docs/alerts-api.md
Normal file
@ -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 }
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user