Files
ARM64/MarketDataServer/Controllers/GainLossController.cs
Sean 516dbd8ffd
Some checks failed
Build .NET Project / build (push) Has been cancelled
Code cleanup
2026-03-17 20:09:03 -04:00

399 lines
20 KiB
C#
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using MarketData.MarketDataModel;
using MarketData.DataAccess;
using MarketData.Utils;
using MarketData.Generator.GainLoss;
using MarketData.MarketDataModel.GainLoss;
using MarketDataServer.Authorization;
using MarketData.Cache;
using MarketData.Generator;
using MarketData;
using LogLevel = MarketData.LogLevel;
using Microsoft.AspNetCore.Mvc;
namespace MarketDataServer.Controllers
{
/// <summary>
/// GainLossController :
/// GetGainLossByDate(String token,DateTime selectedDate)
/// GetGainLossByDateAndAccount(String token,DateTime selectedDate,String account)
/// GetGainLossWithDetailByDate(String token,DateTime selectedDate)
/// GetGainLossWithDetailByDateAndAccount(String token, DateTime selectedDate, String account)
/// GetCompoundGainLoss(String token, int selectedDays, bool includeDividends)
/// </summary>
[ApiController]
[Route("api/[controller]/[action]")]
public class GainLossController : ControllerBase
{
private ActiveGainLossGenerator gainLossGenerator=new ActiveGainLossGenerator();
[HttpGet]
public IEnumerable<GainLossSummaryItem> GetGainLossByDate(String token,DateTime selectedDate)
{
Profiler profiler = new Profiler();
try
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Start");
if (!Authorizations.GetInstance().IsAuthorized(token)) return null;
GLPriceCache.GetInstance().Refresh();
PortfolioTrades portfolioTrades = PortfolioDA.GetTrades();
PortfolioTrades tradesOnOrBefore = portfolioTrades.GetTradesOnOrBefore(selectedDate);
GainLossSummaryItemCollection gainLossSummaryItems = new GainLossSummaryItemCollection(tradesOnOrBefore, selectedDate);
// **** Add an aggregate entry
GainLossSummaryItem gainLossSummaryTotals=new GainLossSummaryItem();
gainLossSummaryTotals.Symbol="";
gainLossSummaryTotals.CompanyName="Account Summary";
if(null!=gainLossSummaryItems&&gainLossSummaryItems.Count>0)
{
gainLossSummaryTotals.Date=gainLossSummaryItems.Min(x => x.Date);
gainLossSummaryTotals.Change=gainLossSummaryItems.Sum(x=>x.Change);
gainLossSummaryTotals.CurrentGainLoss=gainLossSummaryItems.Sum(x => x.CurrentGainLoss);
gainLossSummaryTotals.PreviousGainLoss = gainLossSummaryItems.Sum(x => x.PreviousGainLoss);
gainLossSummaryTotals.ChangePercent=((gainLossSummaryTotals.CurrentGainLoss-gainLossSummaryTotals.PreviousGainLoss)/Math.Abs(gainLossSummaryTotals.PreviousGainLoss))*100.00;
}
else
{
gainLossSummaryTotals.Date = selectedDate;
gainLossSummaryTotals.Change = 0.00;
gainLossSummaryTotals.CurrentGainLoss = 0.00;
gainLossSummaryTotals.PreviousGainLoss = 0.00;
gainLossSummaryTotals.ChangePercent = 0.00;
}
gainLossSummaryItems.Insert(0,gainLossSummaryTotals);
// ****
return gainLossSummaryItems;
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Exception:{exception.ToString()}");
return null;
}
finally
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Done, total took {profiler.End()} (ms)");
}
}
[HttpGet]
public IEnumerable<GainLossSummaryItem> GetGainLossByDateAndAccount(String token,DateTime selectedDate,String account)
{
Profiler profiler = new Profiler();
try
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Start");
GLPriceCache.GetInstance().Refresh();
if (!Authorizations.GetInstance().IsAuthorized(token)) return null;
PortfolioTrades portfolioTrades = PortfolioDA.GetTrades();
portfolioTrades=new PortfolioTrades(portfolioTrades.Where(x=>x.Account.Equals(account)).ToList());
PortfolioTrades tradesOnOrBefore = portfolioTrades.GetTradesOnOrBefore(selectedDate);
GainLossSummaryItemCollection gainLossSummaryItems = new GainLossSummaryItemCollection(tradesOnOrBefore, selectedDate);
// **** Add an aggregate entry
GainLossSummaryItem gainLossSummaryTotals = new GainLossSummaryItem();
gainLossSummaryTotals.Symbol = "";
gainLossSummaryTotals.CompanyName="Account Summary";
if (null != gainLossSummaryItems && gainLossSummaryItems.Count > 0)
{
gainLossSummaryTotals.Date = gainLossSummaryItems.Min(x => x.Date);
gainLossSummaryTotals.Change = gainLossSummaryItems.Sum(x => x.Change);
gainLossSummaryTotals.CurrentGainLoss = gainLossSummaryItems.Sum(x => x.CurrentGainLoss);
gainLossSummaryTotals.PreviousGainLoss = gainLossSummaryItems.Sum(x => x.PreviousGainLoss);
gainLossSummaryTotals.ChangePercent = ((gainLossSummaryTotals.CurrentGainLoss - gainLossSummaryTotals.PreviousGainLoss) / Math.Abs(gainLossSummaryTotals.PreviousGainLoss)) * 100.00;
}
else
{
gainLossSummaryTotals.Date = selectedDate;
gainLossSummaryTotals.Change = 0.00;
gainLossSummaryTotals.CurrentGainLoss = 0.00;
gainLossSummaryTotals.PreviousGainLoss = 0.00;
gainLossSummaryTotals.ChangePercent = 0.00;
}
gainLossSummaryItems.Insert(0, gainLossSummaryTotals);
// ****
return gainLossSummaryItems;
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Exception:{exception.ToString()}");
return null;
}
finally
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Done, total took {profiler.End()} (ms)");
}
}
/// <summary>
/// GetGainLossWithDetailByDate2 - Refactored: cache primed once before loop.
/// GenerateGainLoss still called per symbol (GainLossCollection is a time series,
/// not keyed by symbol) but cache is already populated so DB fetches are skipped.
/// </summary>
[HttpGet]
public IEnumerable<GainLossSummaryItemDetail> GetGainLossWithDetailByDate(String token, DateTime selectedDate)
{
Profiler profiler = new Profiler();
try
{
MDTrace.WriteLine(LogLevel.DEBUG, $"Start");
if (!Authorizations.GetInstance().IsAuthorized(token)) return null;
PortfolioTrades portfolioTrades = PortfolioDA.GetTrades();
PortfolioTrades tradesOnOrBefore = portfolioTrades.GetTradesOnOrBefore(selectedDate);
// Prime cache once for all symbols before any calculation begins.
// Guarantees all symbols are cached and open positions have the latest intraday price.
// GenerateGainLoss calls Add(PortfolioTrades) internally per symbol — those calls
// will hit the cache and skip DB fetches since all symbols are already populated.
GLPriceCache.GetInstance().Add(tradesOnOrBefore);
GainLossSummaryItemCollection gainLossSummaryItems = new GainLossSummaryItemCollection(tradesOnOrBefore, selectedDate);
List<String> symbols = gainLossSummaryItems.Select(x => x.Symbol).ToList();
Dictionary<String, DateTime> latestDates = PricingDA.GetLatestDates(symbols);
if (null == gainLossGenerator) gainLossGenerator = new ActiveGainLossGenerator();
List<GainLossSummaryItemDetail> gainLossSummaryItemDetailCollection = new List<GainLossSummaryItemDetail>();
foreach (GainLossSummaryItem gainLossSummaryItem in gainLossSummaryItems)
{
GainLossSummaryItemDetail gainLossSummaryItemDetail = new GainLossSummaryItemDetail(gainLossSummaryItem);
PortfolioTrades symbolTrades = PortfolioDA.GetOpenTradesSymbol(gainLossSummaryItem.Symbol);
if (null == symbolTrades || 0 == symbolTrades.Count) continue;
double weightAdjustedDividendYield = symbolTrades.GetWeightAdjustedDividendYield();
DateTime currentDate = latestDates[gainLossSummaryItem.Symbol];
double shares = symbolTrades.Sum(x => x.Shares);
double exposure = symbolTrades.Sum(x => x.Exposure());
// GainLossCollection is a date-based time series — must be called per symbol
// Cache is already primed so internal Add(PortfolioTrades) skips DB fetches
GainLossCollection gainLoss = gainLossGenerator.GenerateGainLoss(symbolTrades);
GainLossItem gainLossItem = gainLoss.OrderByDescending(x => x.GainLossPercent).FirstOrDefault();
gainLossSummaryItemDetail.Lots = symbolTrades.Count;
gainLossSummaryItemDetail.Shares = shares;
gainLossSummaryItemDetail.Exposure = exposure;
if (!double.IsNaN(weightAdjustedDividendYield))
{
gainLossSummaryItemDetail.DividendYield = weightAdjustedDividendYield;
gainLossSummaryItemDetail.AnnualDividend = exposure * weightAdjustedDividendYield;
}
// Cache is already primed — pure memory read
Prices prices = GLPriceCache.GetInstance().GetPrices(gainLossSummaryItem.Symbol, currentDate, 3);
Price p1 = prices.Count > 0 ? prices[0] : default;
Price p2 = prices.Count > 1 ? prices[1] : default;
PortfolioTrades tradesForSymbol = new PortfolioTrades(symbolTrades.Where(x => x.Symbol.Equals(gainLossSummaryItem.Symbol)).ToList());
ParityElement parityElement = ParityGenerator.GenerateBreakEven(tradesForSymbol, p1);
gainLossSummaryItemDetail.ParityElement = parityElement;
if (null != parityElement && null != gainLossItem)
{
gainLossSummaryItemDetail.AllTimeGainLossPercent = gainLossItem.GainLossPercent;
gainLossSummaryItemDetail.PercentDistanceFromAllTimeGainLossPercent = parityElement.ParityOffsetPercent - (gainLossItem.GainLossPercent / 100);
}
if (null != p1 && null != p2)
{
double change = (p1.Close - p2.Close) / p2.Close;
gainLossSummaryItemDetail.LatestPrice = p1;
gainLossSummaryItemDetail.PriceChange = change;
}
gainLossSummaryItemDetailCollection.Add(gainLossSummaryItemDetail);
}
// **** Add an aggregate entry
GainLossSummaryItemDetail gainLossSummaryTotals = new GainLossSummaryItemDetail();
gainLossSummaryTotals.Symbol = "";
gainLossSummaryTotals.CompanyName = "Account Summary";
if (null != gainLossSummaryItemDetailCollection && gainLossSummaryItemDetailCollection.Count > 0)
{
gainLossSummaryTotals.Date = gainLossSummaryItemDetailCollection.Min(x => x.Date);
gainLossSummaryTotals.Exposure = gainLossSummaryItemDetailCollection.Sum(x => x.Exposure);
gainLossSummaryTotals.Change = gainLossSummaryItemDetailCollection.Sum(x => x.Change);
gainLossSummaryTotals.CurrentGainLoss = gainLossSummaryItemDetailCollection.Sum(x => x.CurrentGainLoss);
gainLossSummaryTotals.PreviousGainLoss = gainLossSummaryItemDetailCollection.Sum(x => x.PreviousGainLoss);
gainLossSummaryTotals.ChangePercent = ((gainLossSummaryTotals.CurrentGainLoss - gainLossSummaryTotals.PreviousGainLoss) / Math.Abs(gainLossSummaryTotals.PreviousGainLoss)) * 100.00;
gainLossSummaryTotals.LatestPrice = new Price();
gainLossSummaryTotals.PriceChange = 0;
}
else
{
gainLossSummaryTotals.Date = selectedDate;
gainLossSummaryTotals.Change = 0.00;
gainLossSummaryTotals.CurrentGainLoss = 0.00;
gainLossSummaryTotals.PreviousGainLoss = 0.00;
gainLossSummaryTotals.ChangePercent = 0.00;
gainLossSummaryTotals.LatestPrice = new Price();
gainLossSummaryTotals.PriceChange = 0;
}
gainLossSummaryItemDetailCollection.Insert(0, gainLossSummaryTotals);
// ****
return gainLossSummaryItemDetailCollection;
}
catch (Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG, $"Exception:{exception.ToString()}");
return null;
}
finally
{
MDTrace.WriteLine(LogLevel.DEBUG, $"Done, total took {profiler.End()} (ms)");
}
}
[HttpGet]
public IEnumerable<GainLossSummaryItemDetail> GetGainLossWithDetailByDateAndAccount(String token, DateTime selectedDate, String account)
{
Profiler profiler = new Profiler();
try
{
MDTrace.WriteLine(LogLevel.DEBUG, $"Start");
if (!Authorizations.GetInstance().IsAuthorized(token)) return null;
PortfolioTrades portfolioTrades = PortfolioDA.GetTrades();
portfolioTrades = new PortfolioTrades(portfolioTrades.Where(x => x.Account.Equals(account)).ToList());
PortfolioTrades tradesOnOrBefore = portfolioTrades.GetTradesOnOrBefore(selectedDate);
// Prime cache once for all symbols before any calculation begins.
// Guarantees all symbols are cached and open positions have the latest intraday price.
// GenerateGainLoss calls Add(PortfolioTrades) internally per symbol — those calls
// will hit the cache and skip DB fetches since all symbols are already populated.
GLPriceCache.GetInstance().Add(tradesOnOrBefore);
GainLossSummaryItemCollection gainLossSummaryItems = new GainLossSummaryItemCollection(tradesOnOrBefore, selectedDate);
List<String> symbols = gainLossSummaryItems.Select(x => x.Symbol).ToList();
Dictionary<String, DateTime> latestDates = PricingDA.GetLatestDates(symbols);
if (null == gainLossGenerator) gainLossGenerator = new ActiveGainLossGenerator();
List<GainLossSummaryItemDetail> gainLossSummaryItemDetailCollection = new List<GainLossSummaryItemDetail>();
foreach (GainLossSummaryItem gainLossSummaryItem in gainLossSummaryItems)
{
GainLossSummaryItemDetail gainLossSummaryItemDetail = new GainLossSummaryItemDetail(gainLossSummaryItem);
PortfolioTrades symbolTrades = PortfolioDA.GetOpenTradesSymbol(gainLossSummaryItem.Symbol);
if (null == symbolTrades || 0 == symbolTrades.Count) continue;
double weightAdjustedDividendYield = symbolTrades.GetWeightAdjustedDividendYield();
DateTime currentDate = latestDates[gainLossSummaryItem.Symbol];
double shares = symbolTrades.Sum(x => x.Shares);
double exposure = symbolTrades.Sum(x => x.Exposure());
// GainLossCollection is a date-based time series — must be called per symbol
// Cache is already primed so internal Add(PortfolioTrades) skips DB fetches
GainLossCollection gainLoss = gainLossGenerator.GenerateGainLoss(symbolTrades);
GainLossItem gainLossItem = gainLoss.OrderByDescending(x => x.GainLossPercent).FirstOrDefault();
gainLossSummaryItemDetail.Lots = symbolTrades.Count;
gainLossSummaryItemDetail.Shares = shares;
gainLossSummaryItemDetail.Exposure = exposure;
if (!double.IsNaN(weightAdjustedDividendYield))
{
gainLossSummaryItemDetail.DividendYield = weightAdjustedDividendYield;
gainLossSummaryItemDetail.AnnualDividend = exposure * weightAdjustedDividendYield;
}
// Cache is already primed — pure memory read
Prices prices = GLPriceCache.GetInstance().GetPrices(gainLossSummaryItem.Symbol, currentDate, 3);
Price p1 = prices.Count > 0 ? prices[0] : default;
Price p2 = prices.Count > 1 ? prices[1] : default;
PortfolioTrades tradesForSymbol = new PortfolioTrades(symbolTrades.Where(x => x.Symbol.Equals(gainLossSummaryItem.Symbol)).ToList());
ParityElement parityElement = ParityGenerator.GenerateBreakEven(tradesForSymbol, p1);
gainLossSummaryItemDetail.ParityElement = parityElement;
if (null != parityElement && null != gainLossItem)
{
gainLossSummaryItemDetail.AllTimeGainLossPercent = gainLossItem.GainLossPercent;
gainLossSummaryItemDetail.PercentDistanceFromAllTimeGainLossPercent = parityElement.ParityOffsetPercent - (gainLossItem.GainLossPercent / 100);
}
if (null != p1 && null != p2)
{
double change = (p1.Close - p2.Close) / p2.Close;
gainLossSummaryItemDetail.LatestPrice = p1;
gainLossSummaryItemDetail.PriceChange = change;
}
gainLossSummaryItemDetailCollection.Add(gainLossSummaryItemDetail);
}
// **** Add an aggregate entry
GainLossSummaryItemDetail gainLossSummaryTotals = new GainLossSummaryItemDetail();
gainLossSummaryTotals.Symbol = "";
gainLossSummaryTotals.CompanyName = "Account Summary";
if (null != gainLossSummaryItemDetailCollection && gainLossSummaryItemDetailCollection.Count > 0)
{
gainLossSummaryTotals.Date = gainLossSummaryItemDetailCollection.Min(x => x.Date);
gainLossSummaryTotals.Exposure = gainLossSummaryItemDetailCollection.Sum(x => x.Exposure);
gainLossSummaryTotals.Change = gainLossSummaryItemDetailCollection.Sum(x => x.Change);
gainLossSummaryTotals.CurrentGainLoss = gainLossSummaryItemDetailCollection.Sum(x => x.CurrentGainLoss);
gainLossSummaryTotals.PreviousGainLoss = gainLossSummaryItemDetailCollection.Sum(x => x.PreviousGainLoss);
gainLossSummaryTotals.ChangePercent = ((gainLossSummaryTotals.CurrentGainLoss - gainLossSummaryTotals.PreviousGainLoss) / Math.Abs(gainLossSummaryTotals.PreviousGainLoss)) * 100.00;
gainLossSummaryTotals.LatestPrice = new Price();
gainLossSummaryTotals.PriceChange = 0;
}
else
{
gainLossSummaryTotals.Date = selectedDate;
gainLossSummaryTotals.Change = 0.00;
gainLossSummaryTotals.CurrentGainLoss = 0.00;
gainLossSummaryTotals.PreviousGainLoss = 0.00;
gainLossSummaryTotals.ChangePercent = 0.00;
gainLossSummaryTotals.LatestPrice = new Price();
gainLossSummaryTotals.PriceChange = 0;
}
gainLossSummaryItemDetailCollection.Insert(0, gainLossSummaryTotals);
return gainLossSummaryItemDetailCollection;
}
catch (Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG, $"Exception:{exception.ToString()}");
return null;
}
finally
{
MDTrace.WriteLine(LogLevel.DEBUG, $"Done, total took {profiler.End()} (ms)");
}
}
[HttpGet(Name = "GetCompoundGainLoss")]
public GainLossCompoundModelCollection GetCompoundGainLoss(String token, int selectedDays, bool includeDividends)
{
Profiler profiler = new Profiler();
try
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Start");
if(!Authorizations.GetInstance().IsAuthorized(token)) return null;
GLPriceCache.GetInstance().Refresh();
DividendPayments dividendPayments = null;
PortfolioTrades portfolioTrades = PortfolioDA.GetTrades();
GainLossGenerator gainLossGenerator=new GainLossGenerator();
if(includeDividends)dividendPayments=DividendPaymentDA.GetDividendPayments();
ActiveGainLossGenerator activeGainLossGenerator=new ActiveGainLossGenerator();
GainLossCollection gainLoss=activeGainLossGenerator.GenerateGainLoss(portfolioTrades); // gainLoss contains the gain/loss from active positions. Never includes dividends .. just positions
TotalGainLossCollection totalGainLoss=null;
if(null!=dividendPayments)totalGainLoss=gainLossGenerator.GenerateTotalGainLossWithDividends(portfolioTrades,dividendPayments);
else totalGainLoss=gainLossGenerator.GenerateTotalGainLoss(portfolioTrades);
GainLossCompoundModelCollection gainLossModelCollection=null;
gainLossModelCollection=new GainLossCompoundModelCollection(gainLoss,totalGainLoss);
if(-1==selectedDays)return gainLossModelCollection;
int skip=gainLossModelCollection.Count-selectedDays;
if(skip<0)return gainLossModelCollection;
return new GainLossCompoundModelCollection(gainLossModelCollection.Skip(skip).ToList());
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Exception:{exception.ToString()}");
return null;
}
finally
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Done, total took {profiler.End()} (ms)");
}
}
}
}