Secured API with key.
This commit is contained in:
parent
6d76c60fc1
commit
a955ab3b32
13
Backend.cs
13
Backend.cs
@ -1,11 +1,17 @@
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.ServiceHander;
|
using Backend.ServiceHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
using Backend.Middleware;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
builder.Services.AddControllers(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add<Backend.Middleware.GlobalExceptionFilter>();
|
||||||
|
});
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
@ -23,6 +29,8 @@ builder.Services.AddSingleton<WeatherService>();
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IAnnotationStore, PostgresAnnotationStore>();
|
builder.Services.AddSingleton<IAnnotationStore, PostgresAnnotationStore>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IApiKeyStore, PostgresApiKeyStore>();
|
||||||
|
|
||||||
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, PercentChangeEvaluator>();
|
||||||
@ -59,6 +67,7 @@ app.UseForwardedHeaders();
|
|||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseCors("Frontend");
|
app.UseCors("Frontend");
|
||||||
|
app.UseMiddleware<ApiKeyMiddleware>();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Backend.Controller
|
namespace Backend.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/annotations")]
|
[Route("api/annotations")]
|
||||||
@ -19,8 +19,6 @@ namespace Backend.Controller
|
|||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string symbol,
|
[FromQuery] string symbol,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
@ -35,19 +33,12 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
return Content(json, "application/json");
|
return Content(json, "application/json");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("save")]
|
[HttpPost("save")]
|
||||||
public async Task<IActionResult> Save(
|
public async Task<IActionResult> Save(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string symbol,
|
[FromQuery] string symbol,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
@ -65,19 +56,12 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
return Ok(new { success = true });
|
return Ok(new { success = true });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("delete")]
|
[HttpDelete("delete")]
|
||||||
public async Task<IActionResult> Delete(
|
public async Task<IActionResult> Delete(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string symbol,
|
[FromQuery] string symbol,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
@ -89,10 +73,5 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
return Ok(new { success = true });
|
return Ok(new { success = true });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
using Backend.ServiceHander;
|
using Backend.ServiceHandler;
|
||||||
using Backend.MarketDataRequest;
|
using Backend.MarketDataRequest;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Backend.Controller
|
namespace Backend.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/market")]
|
[Route("api/market")]
|
||||||
@ -23,8 +23,6 @@ namespace Backend.Controller
|
|||||||
public async Task<IActionResult> GetLastTradingDay(
|
public async Task<IActionResult> GetLastTradingDay(
|
||||||
[FromQuery] string symbol = "SPY",
|
[FromQuery] string symbol = "SPY",
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var lastDay = await _cache.GetLastTradingDayAsync(
|
var lastDay = await _cache.GetLastTradingDayAsync(
|
||||||
symbol.Trim().ToUpperInvariant(), cancellationToken);
|
symbol.Trim().ToUpperInvariant(), cancellationToken);
|
||||||
@ -38,46 +36,19 @@ namespace Backend.Controller
|
|||||||
lastTradingDay = lastDay.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)
|
lastTradingDay = lastDay.Value.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("symbol-check/{symbol}")]
|
[HttpGet("symbol-check/{symbol}")]
|
||||||
public IActionResult CheckSymbol(string symbol)
|
public IActionResult CheckSymbol(string symbol)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var (found, message) = _marketDataService.CheckSymbol(symbol.ToUpperInvariant());
|
var (found, message) = _marketDataService.CheckSymbol(symbol.ToUpperInvariant());
|
||||||
return Ok(new { found, message });
|
return Ok(new { found, message });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("live/{symbol}")]
|
[HttpGet("live/{symbol}")]
|
||||||
public IActionResult GetLive(string symbol)
|
public IActionResult GetLive(string symbol)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var data = _marketDataService.GetLiveQuote(symbol.ToUpperInvariant());
|
var data = _marketDataService.GetLiveQuote(symbol.ToUpperInvariant());
|
||||||
return Ok(new
|
return Ok(data);
|
||||||
{
|
|
||||||
symbol = data.Symbol,
|
|
||||||
lastPrice = data.LastPrice,
|
|
||||||
volume = data.Volume,
|
|
||||||
previousClose = data.PreviousClose,
|
|
||||||
openPrice = data.Open,
|
|
||||||
marketOpen = data.MarketOpen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("historical")]
|
[HttpGet("historical")]
|
||||||
@ -88,8 +59,6 @@ namespace Backend.Controller
|
|||||||
[FromQuery] HistoricalBarSize barSize,
|
[FromQuery] HistoricalBarSize barSize,
|
||||||
[FromQuery] HistoricalWhatToShow whatToShow = HistoricalWhatToShow.Trades,
|
[FromQuery] HistoricalWhatToShow whatToShow = HistoricalWhatToShow.Trades,
|
||||||
[FromQuery] bool useRth = true)
|
[FromQuery] bool useRth = true)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var data = await _marketDataService.GetHistoricalAsync(
|
var data = await _marketDataService.GetHistoricalAsync(
|
||||||
symbol.ToUpperInvariant(),
|
symbol.ToUpperInvariant(),
|
||||||
@ -102,11 +71,6 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("historical-line")]
|
[HttpGet("historical-line")]
|
||||||
public async Task<IActionResult> GetHistoricalLine(
|
public async Task<IActionResult> GetHistoricalLine(
|
||||||
@ -116,8 +80,6 @@ namespace Backend.Controller
|
|||||||
[FromQuery] HistoricalBarSize barSize,
|
[FromQuery] HistoricalBarSize barSize,
|
||||||
[FromQuery] HistoricalWhatToShow whatToShow = HistoricalWhatToShow.Trades,
|
[FromQuery] HistoricalWhatToShow whatToShow = HistoricalWhatToShow.Trades,
|
||||||
[FromQuery] bool useRth = true)
|
[FromQuery] bool useRth = true)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var data = await _marketDataService.GetHistoricalLineAsync(
|
var data = await _marketDataService.GetHistoricalLineAsync(
|
||||||
symbol.ToUpperInvariant(),
|
symbol.ToUpperInvariant(),
|
||||||
@ -130,10 +92,5 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
return Ok(data);
|
return Ok(data);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
|
using Backend.DatabaseHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
using Backend.ServiceHander;
|
using Backend.ServiceHandler;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Backend.Controller
|
namespace Backend.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/news-sentiment")]
|
[Route("api/news-sentiment")]
|
||||||
@ -28,30 +29,9 @@ namespace Backend.Controller
|
|||||||
public async Task<IActionResult> Analyze(
|
public async Task<IActionResult> Analyze(
|
||||||
[FromQuery] string keyword,
|
[FromQuery] string keyword,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var results = await _service.AnalyzeAsync(keyword, cancellationToken);
|
var results = await _service.AnalyzeAsync(keyword, cancellationToken);
|
||||||
return Ok(results.Select(r => new
|
return Ok(results);
|
||||||
{
|
|
||||||
source = r.Source,
|
|
||||||
keyword = r.Keyword,
|
|
||||||
publishedAt = r.PublishedAt,
|
|
||||||
title = r.Title,
|
|
||||||
score = r.Score,
|
|
||||||
sentimentLabel = r.SentimentLabel,
|
|
||||||
publisher = r.Publisher,
|
|
||||||
url = r.Url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("feed")]
|
[HttpGet("feed")]
|
||||||
@ -60,8 +40,6 @@ namespace Backend.Controller
|
|||||||
[FromQuery] string? keyword = null,
|
[FromQuery] string? keyword = null,
|
||||||
[FromQuery(Name = "request-amount")] int requestAmount = 20,
|
[FromQuery(Name = "request-amount")] int requestAmount = 20,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
@ -87,24 +65,9 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
var results = await _store.GetFeedAsync(keywords, requestAmount, cancellationToken);
|
var results = await _store.GetFeedAsync(keywords, requestAmount, cancellationToken);
|
||||||
|
|
||||||
return Ok(results.Select(r => new
|
return Ok(results.Select(NewsFeedItem.From));
|
||||||
{
|
|
||||||
title = r.Title,
|
|
||||||
publisher = r.Publisher,
|
|
||||||
score = r.Score,
|
|
||||||
publishedAt = r.PublishedAt.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
|
||||||
url = r.Url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("refresh")]
|
[HttpGet("refresh")]
|
||||||
public async Task<ActionResult<BoolResponse>> Refresh(
|
public async Task<ActionResult<BoolResponse>> Refresh(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
@ -115,7 +78,6 @@ namespace Backend.Controller
|
|||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
|
|
||||||
// 5 second refresh cooldown - abuse protection
|
|
||||||
lock (_refreshLock)
|
lock (_refreshLock)
|
||||||
{
|
{
|
||||||
if ((DateTime.UtcNow - _lastRefresh).TotalSeconds < 5)
|
if ((DateTime.UtcNow - _lastRefresh).TotalSeconds < 5)
|
||||||
@ -125,11 +87,11 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
var symbols = await _interests.FetchAllAsync(user, "symbol");
|
var symbols = await _interests.FetchAllAsync(user, "symbol");
|
||||||
var topics = await _interests.FetchAllAsync(user, "topic");
|
var topics = await _interests.FetchAllAsync(user, "topic");
|
||||||
var keywords = symbols.Concat(topics).Distinct().ToList();
|
var kws = symbols.Concat(topics).Distinct().ToList();
|
||||||
|
|
||||||
foreach (var keyword in keywords)
|
foreach (var kw in kws)
|
||||||
{
|
{
|
||||||
await _service.AnalyzeAsync(keyword, cancellationToken);
|
await _service.AnalyzeAsync(kw, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new BoolResponse { Success = true });
|
return Ok(new BoolResponse { Success = true });
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.ServiceHandler;
|
||||||
using Backend.ServiceHander;
|
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -20,8 +19,6 @@ namespace Backend.Controllers
|
|||||||
public async Task<ActionResult<UserInterestFetchResponse>> FetchAll(
|
public async Task<ActionResult<UserInterestFetchResponse>> FetchAll(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string type)
|
[FromQuery] string type)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
List<string> items = await _store.FetchAllAsync(user, type);
|
List<string> items = await _store.FetchAllAsync(user, type);
|
||||||
return Ok(new UserInterestFetchResponse
|
return Ok(new UserInterestFetchResponse
|
||||||
@ -29,44 +26,25 @@ namespace Backend.Controllers
|
|||||||
Items = items
|
Items = items
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("add")]
|
[HttpPost("add")]
|
||||||
public async Task<ActionResult<BoolResponse>> Add(
|
public async Task<ActionResult<BoolResponse>> Add(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string type,
|
[FromQuery] string type,
|
||||||
[FromQuery] string value)
|
[FromQuery] string value)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
bool success = await _store.AddAsync(user, type, value);
|
bool success = await _store.AddAsync(user, type, value);
|
||||||
return Ok(new BoolResponse { Success = success });
|
return Ok(new BoolResponse { Success = success });
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("remove")]
|
[HttpDelete("remove")]
|
||||||
public async Task<ActionResult<BoolResponse>> Remove(
|
public async Task<ActionResult<BoolResponse>> Remove(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string type,
|
[FromQuery] string type,
|
||||||
[FromQuery] string value)
|
[FromQuery] string value)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
bool success = await _store.RemoveAsync(user, type, value);
|
bool success = await _store.RemoveAsync(user, type, value);
|
||||||
return Ok(new BoolResponse { Success = success });
|
return Ok(new BoolResponse { Success = success });
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
|
||||||
{
|
|
||||||
return BadRequest(ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
using Backend.ServiceHander;
|
using Backend.ServiceHandler;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Backend.Controller
|
namespace Backend.Controllers
|
||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/weather")]
|
[Route("api/weather")]
|
||||||
@ -18,13 +18,11 @@ namespace Backend.Controller
|
|||||||
_weather = weather;
|
_weather = weather;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("set-location")]
|
[HttpPut("set-location")]
|
||||||
public async Task<IActionResult> SetLocation(
|
public async Task<IActionResult> SetLocation(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
[FromQuery] string city,
|
[FromQuery] string city,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
@ -47,18 +45,11 @@ namespace Backend.Controller
|
|||||||
|
|
||||||
return Ok(new { city = resolvedCity, latitude = lat, longitude = lng });
|
return Ok(new { city = resolvedCity, latitude = lat, longitude = lng });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("current")]
|
[HttpGet("current")]
|
||||||
public async Task<IActionResult> GetCurrent(
|
public async Task<IActionResult> GetCurrent(
|
||||||
[FromQuery] string user,
|
[FromQuery] string user,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user))
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
return BadRequest("User cannot be empty.");
|
return BadRequest("User cannot be empty.");
|
||||||
@ -73,20 +64,7 @@ namespace Backend.Controller
|
|||||||
if (weather is null)
|
if (weather is null)
|
||||||
return StatusCode(500, new { error = "Failed to fetch weather data." });
|
return StatusCode(500, new { error = "Failed to fetch weather data." });
|
||||||
|
|
||||||
return Ok(new
|
return Ok(CurrentWeatherResponse.From(location, weather));
|
||||||
{
|
|
||||||
city = location.City,
|
|
||||||
temperatureF = weather.TemperatureF,
|
|
||||||
humidityPercent = weather.HumidityPercent,
|
|
||||||
windSpeedMph = weather.WindSpeedMph,
|
|
||||||
weatherCode = weather.WeatherCode,
|
|
||||||
description = weather.Description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return StatusCode(500, new { error = ex.ToString() });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Backend.DatabaseHandler
|
namespace Backend.DatabaseHandler
|
||||||
{
|
{
|
||||||
public sealed class NewsSentimentRecord
|
public sealed class NewsSentimentRecord
|
||||||
@ -8,8 +10,28 @@ namespace Backend.DatabaseHandler
|
|||||||
public string Title { get; init; } = string.Empty;
|
public string Title { get; init; } = string.Empty;
|
||||||
public double Score { get; init; }
|
public double Score { get; init; }
|
||||||
public string SentimentLabel { get; init; } = string.Empty;
|
public string SentimentLabel { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public string TitleHash { get; init; } = string.Empty;
|
public string TitleHash { get; init; } = string.Empty;
|
||||||
public string Publisher { get; init; } = string.Empty;
|
public string Publisher { get; init; } = string.Empty;
|
||||||
public string Url { get; init; } = string.Empty;
|
public string Url { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class NewsFeedItem
|
||||||
|
{
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public string Publisher { get; init; } = string.Empty;
|
||||||
|
public double Score { get; init; }
|
||||||
|
public string PublishedAt { get; init; } = string.Empty;
|
||||||
|
public string Url { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public static NewsFeedItem From(NewsSentimentRecord r) => new()
|
||||||
|
{
|
||||||
|
Title = r.Title,
|
||||||
|
Publisher = r.Publisher,
|
||||||
|
Score = r.Score,
|
||||||
|
PublishedAt = r.PublishedAt.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||||
|
Url = r.Url
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
DatabaseHandler/PostgresApiKeyStore.cs
Normal file
33
DatabaseHandler/PostgresApiKeyStore.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Npgsql;
|
||||||
|
using Backend.Interface;
|
||||||
|
|
||||||
|
namespace Backend.DatabaseHandler;
|
||||||
|
|
||||||
|
public sealed class PostgresApiKeyStore : IApiKeyStore
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public PostgresApiKeyStore(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_connectionString = configuration.GetConnectionString("MarketDataDb")
|
||||||
|
?? throw new InvalidOperationException("Missing connection string: MarketDataDb");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ValidateAsync(string apiKey, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM api_keys
|
||||||
|
WHERE key = @key AND is_active = TRUE;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
|
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||||
|
cmd.Parameters.AddWithValue("key", apiKey);
|
||||||
|
|
||||||
|
var result = await cmd.ExecuteScalarAsync(cancellationToken);
|
||||||
|
return Convert.ToInt64(result) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.ServiceHander;
|
using Backend.ServiceHandler;
|
||||||
|
|
||||||
namespace Backend.Interface;
|
namespace Backend.Interface;
|
||||||
|
|
||||||
|
|||||||
6
Interface/IApiKeyStore.cs
Normal file
6
Interface/IApiKeyStore.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Backend.Interface;
|
||||||
|
|
||||||
|
public interface IApiKeyStore
|
||||||
|
{
|
||||||
|
Task<bool> ValidateAsync(string apiKey, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Backend.MarketDataRequest
|
namespace Backend.MarketDataRequest
|
||||||
{
|
{
|
||||||
public sealed class LiveEquitySnapshot
|
public sealed class LiveEquitySnapshot
|
||||||
@ -7,15 +9,19 @@ namespace Backend.MarketDataRequest
|
|||||||
public double? LastPrice { get; set; }
|
public double? LastPrice { get; set; }
|
||||||
public long? Volume { get; set; }
|
public long? Volume { get; set; }
|
||||||
public double? PreviousClose { get; set; }
|
public double? PreviousClose { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("openPrice")]
|
||||||
public double? Open { get; set; }
|
public double? Open { get; set; }
|
||||||
public bool MarketOpen { get; set; } = true;
|
public bool MarketOpen { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public bool IsComplete =>
|
public bool IsComplete =>
|
||||||
LastPrice.HasValue &&
|
LastPrice.HasValue &&
|
||||||
Volume.HasValue &&
|
Volume.HasValue &&
|
||||||
PreviousClose.HasValue &&
|
PreviousClose.HasValue &&
|
||||||
Open.HasValue;
|
Open.HasValue;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
public bool HasMinimumData =>
|
public bool HasMinimumData =>
|
||||||
LastPrice.HasValue &&
|
LastPrice.HasValue &&
|
||||||
PreviousClose.HasValue;
|
PreviousClose.HasValue;
|
||||||
|
|||||||
42
Middleware/ApiKeyMiddleware.cs
Normal file
42
Middleware/ApiKeyMiddleware.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using Backend.Interface;
|
||||||
|
|
||||||
|
namespace Backend.Middleware;
|
||||||
|
|
||||||
|
public sealed class ApiKeyMiddleware
|
||||||
|
{
|
||||||
|
private const string ApiKeyHeader = "X-Api-Key";
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public ApiKeyMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context, IApiKeyStore apiKeyStore)
|
||||||
|
{
|
||||||
|
var path = context.Request.Path.Value ?? "";
|
||||||
|
|
||||||
|
if (path.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue(ApiKeyHeader, out var extractedKey)
|
||||||
|
|| string.IsNullOrWhiteSpace(extractedKey))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
await context.Response.WriteAsJsonAsync(new { error = "API key is required." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await apiKeyStore.ValidateAsync(extractedKey!, context.RequestAborted))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
await context.Response.WriteAsJsonAsync(new { error = "Invalid API key." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Middleware/GlobalExceptionFilter.cs
Normal file
34
Middleware/GlobalExceptionFilter.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Backend.Middleware;
|
||||||
|
|
||||||
|
public sealed class GlobalExceptionFilter : IExceptionFilter
|
||||||
|
{
|
||||||
|
private readonly ILogger<GlobalExceptionFilter> _logger;
|
||||||
|
|
||||||
|
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnException(ExceptionContext context)
|
||||||
|
{
|
||||||
|
_logger.LogError(context.Exception, "Unhandled exception on {Method} {Path}",
|
||||||
|
context.HttpContext.Request.Method,
|
||||||
|
context.HttpContext.Request.Path);
|
||||||
|
|
||||||
|
if (context.Exception is ArgumentException)
|
||||||
|
{
|
||||||
|
context.Result = new BadRequestObjectResult(context.Exception.Message);
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Result = new ObjectResult(new { error = context.Exception.ToString() })
|
||||||
|
{
|
||||||
|
StatusCode = 500
|
||||||
|
};
|
||||||
|
context.ExceptionHandled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Migrations/001_create_api_keys.sql
Normal file
12
Migrations/001_create_api_keys.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Generate a default key (replace with your own or delete after creating real keys)
|
||||||
|
INSERT INTO api_keys (key, name)
|
||||||
|
VALUES (encode(gen_random_bytes(32), 'hex'), 'default')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
117
README.md
117
README.md
@ -1,3 +1,116 @@
|
|||||||
# SoftTraderBackend
|
# SoftTrader Backend
|
||||||
|
|
||||||
The backend for a lite Bloomberg terminal.
|
A .NET 10 Web API backend for a lite Bloomberg-style terminal. Provides live and historical equity data via Interactive Brokers, AI-driven news sentiment analysis using FinBERT, configurable price alerts, and supporting services like weather and chart annotations.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
||||||
|
- [PostgreSQL](https://www.postgresql.org/)
|
||||||
|
- [Interactive Brokers Gateway](https://www.interactivebrokers.com/en/trading/ibgateway-stable.php) running on `127.0.0.1:4002`
|
||||||
|
- FinBERT ONNX model files in `AIModels/finbert/` (`model.onnx`, `vocab.txt`, tokenizer configs)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. **Configure the database** — update the connection string in `appsettings.json`:
|
||||||
|
```json
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"MarketDataDb": "Host=127.0.0.1;Port=5432;Database=softtraderbackend_pricedata;Username=postgres;Password=<your-password>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start IB Gateway** — the application connects on startup and will exit if the connection fails.
|
||||||
|
|
||||||
|
3. **Run the application:**
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
The API starts on `http://localhost:5000` by default.
|
||||||
|
|
||||||
|
4. **Swagger UI** is available at `/swagger` for interactive API exploration (no API key required).
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All API endpoints (except Swagger) require an API key via the `X-Api-Key` header. Keys are validated against PostgreSQL.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Market Data — `/api/market`
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| GET | `/live/{symbol}` | Live quote from IB Gateway |
|
||||||
|
| GET | `/historical` | Historical OHLCV bars (configurable duration, bar size, data type) |
|
||||||
|
| GET | `/historical-line` | Historical line data |
|
||||||
|
| GET | `/symbol-check/{symbol}` | Validate a ticker symbol |
|
||||||
|
| GET | `/last-trading-day` | Last trading day for a symbol |
|
||||||
|
|
||||||
|
### News Sentiment — `/api/news-sentiment`
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| GET | `/` | Analyze sentiment for a keyword (fetches Google News RSS, scores with FinBERT) |
|
||||||
|
| GET | `/feed` | Personalized news feed based on user interests |
|
||||||
|
| GET | `/refresh` | Refresh sentiment data for all of a user's tracked symbols/topics |
|
||||||
|
|
||||||
|
### Alerts — `/api/alerts`
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| POST | `/create` | Create a price alert |
|
||||||
|
| GET | `/list` | List all alerts for a user |
|
||||||
|
| GET | `/triggered` | List triggered alerts for a user |
|
||||||
|
| DELETE | `/delete` | Delete an alert |
|
||||||
|
|
||||||
|
**Alert types:** `price_threshold`, `percent_change`, `price_spike`, `trailing_price`
|
||||||
|
|
||||||
|
Alerts are evaluated by a background service every minute during market hours (Mon–Fri, 6 AM – 2 PM Pacific).
|
||||||
|
|
||||||
|
### User Interests — `/api/user-interest`
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| GET | `/fetch` | Get a user's tracked symbols or topics |
|
||||||
|
| POST | `/add` | Add a symbol or topic |
|
||||||
|
| DELETE | `/remove` | Remove a symbol or topic |
|
||||||
|
|
||||||
|
### Annotations — `/api/annotations`
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| GET | `/fetch` | Get saved chart annotations for a user/symbol |
|
||||||
|
| POST | `/save` | Save chart annotations |
|
||||||
|
| DELETE | `/delete` | Delete chart annotations |
|
||||||
|
|
||||||
|
### Weather — `/api/weather`
|
||||||
|
|
||||||
|
| Method | Route | Description |
|
||||||
|
|--------|-------|-------------|
|
||||||
|
| PUT | `/set-location` | Set a user's city (geocoded) |
|
||||||
|
| GET | `/current` | Get current weather for a user's saved location |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Backend/
|
||||||
|
├── Controller/ # REST API controllers
|
||||||
|
├── ServiceHandler/ # Business logic (market data, news, alerts, weather)
|
||||||
|
├── DatabaseHandler/ # PostgreSQL data access via Npgsql (no ORM)
|
||||||
|
├── Interface/ # Store and service abstractions
|
||||||
|
├── Middleware/ # API key auth, global exception filter
|
||||||
|
├── MarketDataRequest/ # IB Gateway integration and request/response models
|
||||||
|
└── AIModels/finbert/ # FinBERT ONNX model and tokenizer files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Background Services
|
||||||
|
|
||||||
|
- **AlertEvaluationService** — polls active alerts every minute during market hours and evaluates them against live prices
|
||||||
|
- **NewsSentimentRefreshService** — periodically refreshes news sentiment data for tracked interests
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Package | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `Microsoft.ML.OnnxRuntime` | FinBERT model inference |
|
||||||
|
| `Npgsql` | PostgreSQL driver |
|
||||||
|
| `Swashbuckle.AspNetCore` | Swagger / OpenAPI |
|
||||||
|
| `twsapi` (project ref) | Interactive Brokers TWS API client |
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
|
||||||
namespace Backend.ServiceHander;
|
namespace Backend.ServiceHandler;
|
||||||
|
|
||||||
public sealed class AlertEvaluationService : BackgroundService
|
public sealed class AlertEvaluationService : BackgroundService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class EdgarFilingService : IDisposable
|
public sealed class EdgarFilingService : IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,7 +3,7 @@ using Microsoft.ML.OnnxRuntime.Tensors;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class FinBertScoringService : IDisposable
|
public sealed class FinBertScoringService : IDisposable
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@ using Backend.Interface;
|
|||||||
using IBApi;
|
using IBApi;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class MarketDataService
|
public sealed class MarketDataService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
|
||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class NewsSentimentRefreshService : BackgroundService
|
public sealed class NewsSentimentRefreshService : BackgroundService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,7 +4,7 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class NewsSentimentService
|
public sealed class NewsSentimentService
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
|
||||||
namespace Backend.ServiceHander;
|
namespace Backend.ServiceHandler;
|
||||||
|
|
||||||
public sealed class PercentChangeEvaluator : IAlertEvaluator
|
public sealed class PercentChangeEvaluator : IAlertEvaluator
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
|
||||||
namespace Backend.ServiceHander;
|
namespace Backend.ServiceHandler;
|
||||||
|
|
||||||
public sealed class PriceSpikeEvaluator : IAlertEvaluator
|
public sealed class PriceSpikeEvaluator : IAlertEvaluator
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
|
||||||
namespace Backend.ServiceHander;
|
namespace Backend.ServiceHandler;
|
||||||
|
|
||||||
public sealed class PriceThresholdEvaluator : IAlertEvaluator
|
public sealed class PriceThresholdEvaluator : IAlertEvaluator
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,7 @@ using System.Text.Json;
|
|||||||
using Backend.DatabaseHandler;
|
using Backend.DatabaseHandler;
|
||||||
using Backend.Interface;
|
using Backend.Interface;
|
||||||
|
|
||||||
namespace Backend.ServiceHander;
|
namespace Backend.ServiceHandler;
|
||||||
|
|
||||||
public sealed class TrailingPriceEvaluator : IAlertEvaluator
|
public sealed class TrailingPriceEvaluator : IAlertEvaluator
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,18 +1,5 @@
|
|||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class UserInterestFetchRequest
|
|
||||||
{
|
|
||||||
public string User { get; set; } = string.Empty;
|
|
||||||
public string Type { get; set; } = string.Empty; // "symbol" or "topic"
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class UserInterestModifyRequest
|
|
||||||
{
|
|
||||||
public string User { get; set; } = string.Empty;
|
|
||||||
public string Type { get; set; } = string.Empty; // "symbol" or "topic"
|
|
||||||
public string Value { get; set; } = string.Empty; // symbol or topic
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class UserInterestFetchResponse
|
public sealed class UserInterestFetchResponse
|
||||||
{
|
{
|
||||||
public List<string> Items { get; set; } = new();
|
public List<string> Items { get; set; } = new();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Backend.ServiceHander
|
namespace Backend.ServiceHandler
|
||||||
{
|
{
|
||||||
public sealed class WeatherService
|
public sealed class WeatherService
|
||||||
{
|
{
|
||||||
@ -85,4 +85,25 @@ namespace Backend.ServiceHander
|
|||||||
_ => "Unknown"
|
_ => "Unknown"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class CurrentWeatherResponse
|
||||||
|
{
|
||||||
|
public string City { get; init; } = string.Empty;
|
||||||
|
public double TemperatureF { get; init; }
|
||||||
|
public double HumidityPercent { get; init; }
|
||||||
|
public double WindSpeedMph { get; init; }
|
||||||
|
public int WeatherCode { get; init; }
|
||||||
|
public string Description { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public static CurrentWeatherResponse From(
|
||||||
|
Backend.DatabaseHandler.UserLocationRecord location, WeatherResult weather) => new()
|
||||||
|
{
|
||||||
|
City = location.City,
|
||||||
|
TemperatureF = weather.TemperatureF,
|
||||||
|
HumidityPercent = weather.HumidityPercent,
|
||||||
|
WindSpeedMph = weather.WindSpeedMph,
|
||||||
|
WeatherCode = weather.WeatherCode,
|
||||||
|
Description = weather.Description
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"MarketDataDb": "Host=127.0.0.1;Port=5432;Database=softtraderbackend_pricedata;Username=postgres;Password=Database@Dongfeng"
|
"MarketDataDb": "Host=127.0.0.1;Port=5432;Database=softtraderbackend_pricedata;Username=postgres;Password=CHANGE_ME"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,10 +6,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"MarketDataDb": "Host=127.0.0.1;Port=5432;Database=softtraderbackend_pricedata;Username=postgres;Password=Database@Dongfeng"
|
"MarketDataDb": "Host=127.0.0.1;Port=5432;Database=softtraderbackend_pricedata;Username=postgres;Password=CHANGE_ME"
|
||||||
},
|
},
|
||||||
"Edgar": {
|
"Edgar": {
|
||||||
"UserAgent": "D. Z. dzhan116@ucr.edu",
|
"UserAgent": "CHANGE_ME",
|
||||||
"RequestDelayMs": 600
|
"RequestDelayMs": 600
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user