Files
Avalonia/PortfolioManager/ViewModels/MomentumViewModel.cs
2025-06-30 17:39:52 -04:00

661 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.Input;
using Eremex.AvaloniaUI.Controls;
using MarketData;
using MarketData.DataAccess;
using MarketData.Generator;
using MarketData.Generator.Interface;
using MarketData.Generator.Model;
using MarketData.Generator.Momentum;
using MarketData.MarketDataModel;
using MarketData.Utils;
using PortfolioManager.DataSeriesViewModels;
using PortfolioManager.Dialogs;
using PortfolioManager.Models;
using PortfolioManager.UIUtils;
using Position = MarketData.Generator.Momentum.Position;
namespace PortfolioManager.ViewModels
{
public partial class MomentumViewModel : WorkspaceViewModel
{
private bool isBusy = false;
private MGSessionParams sessionParams;
private ObservableCollection<String> nvpDictionaryKeys = default;
private NVPDictionary nvpDictionary = null;
private String selectedParameter = null;
private String pathFileName;
private ModelPerformanceSeries modelPerformanceSeries = null;
private ModelStatistics modelStatistics = null;
private MGConfiguration configuration = null;
private MGPositionModelCollection positions = null;
private bool showAsGainLoss = true;
private MGPositionModel selectedPosition = null;
private bool showMarkers = false;
public MomentumViewModel()
{
DisplayName = "MGMomentum Model";
PropertyChanged += OnViewModelPropertyChanged;
}
protected override void OnDispose()
{
MDTrace.WriteLine(LogLevel.DEBUG,$"Dispose MomentumViewModel");
base.OnDispose();
}
private void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs eventArgs)
{
}
public override String Title
{
get
{
return DisplayName;
}
}
public override String DisplayName
{
get
{
if (null == pathFileName) return "MGMomentum Model";
String pureFileName = Utility.GetFileNameNoExtension(pathFileName);
return "MGMomentum Model (" + pureFileName + ")";
}
}
public bool IsBusy
{
get
{
return isBusy;
}
set
{
isBusy = value;
base.OnPropertyChanged("IsBusy");
}
}
public bool ShowMarkers
{
get
{
return showMarkers;
}
set
{
showMarkers = value;
base.OnPropertyChanged("ShowMarkers");
}
}
public String TradeDate
{
get
{
if (null == sessionParams) return Constants.CONST_DASHES;
DateGenerator dateGenerator = new DateGenerator();
return Utility.DateTimeToStringMMSDDSYYYY(dateGenerator.FindPrevBusinessDay(sessionParams.TradeDate));
}
}
public ObservableCollection<String> Parameters
{
get { return nvpDictionaryKeys; }
}
public String SelectedParameter
{
get
{
return selectedParameter;
}
set
{
selectedParameter = value;
base.OnPropertyChanged("SelectedParameter");
base.OnPropertyChanged("ParameterValue");
}
}
public String ParameterValue
{
get
{
if (null == nvpDictionary || null == nvpDictionaryKeys || null == selectedParameter) return null;
return nvpDictionary[selectedParameter].Value;
}
}
public String CashBalance
{
get
{
if (null == sessionParams) return Constants.CONST_DASHES;
return Utility.FormatCurrency(sessionParams.CashBalance);
}
}
public ObservableCollection<MenuItem> PositionsMenuItems
{
get
{
ObservableCollection<MenuItem> collection = new ObservableCollection<MenuItem>();
collection.Add(new MenuItem() { Header = "Bollinger Bands...", Command = BollingerBandsCommand, StaysOpenOnClick = false });
collection.Add(new MenuItem() { Header = "Close Position...", Command = CloseCommand, StaysOpenOnClick = false });
collection.Add(new MenuItem() { Header = "Edit Position...", Command = EditCommand, StaysOpenOnClick = false });
collection.Add(new MenuItem() { Header = "Add To WatchList", Command = AddToWatchListCommand, StaysOpenOnClick = false });
collection.Add(new MenuItem() { Header = "Remove From WatchList", Command = RemoveFromWatchListCommand, StaysOpenOnClick = false });
return collection;
}
}
public bool ReloadEnabled
{
get
{
return !String.IsNullOrEmpty(pathFileName);
}
}
public MGPositionModelCollection AllPositions
{
get { return positions; }
}
public String NonTradeableCash
{
get
{
if (null == sessionParams) return Constants.CONST_DASHES;
return Utility.FormatCurrency(sessionParams.NonTradeableCash);
}
}
public String PercentButtonText
{
get
{
if (!showAsGainLoss) return "Show $";
else return "Show %";
}
}
public String ModelExpectation
{
get
{
if (null == modelStatistics) return "";
return Utility.FormatNumber(modelStatistics.Expectancy, 2);
}
}
public IBrush ExpectationColor
{
get
{
if (null == modelStatistics) return BrushCollection.GetContextBrush(BrushCollection.BrushColor.Black);
if (modelStatistics.Expectancy > 0.00) return BrushCollection.GetContextBrush(BrushCollection.BrushColor.Black);
return BrushCollection.GetContextBrush(BrushCollection.BrushColor.Red);
}
}
public MGPositionModel SelectedPosition
{
get
{
return selectedPosition;
}
set
{
selectedPosition = value;
base.OnPropertyChanged("SelectedPosition");
}
}
public CompositeDataSource Data
{
get
{
if (null == modelPerformanceSeries) return GainLossModel.Empty();
CompositeDataSource compositeDataSource = null;
compositeDataSource = GainLossModel.GainLoss(modelPerformanceSeries, showAsGainLoss);
return compositeDataSource;
}
}
public String GraphTitle
{
get
{
if (null == sessionParams || null == modelPerformanceSeries) return "";
StringBuilder sb = new StringBuilder();
Positions allPositions = sessionParams.GetCombinedPositions();
DateTime minDate = allPositions.Min(x => x.PurchaseDate);
DateTime maxDate = PricingDA.GetLatestDate();
if (modelPerformanceSeries.Count < 2)
{
sb.Append(showAsGainLoss ? "$ GainLoss" : "% Return");
sb.Append(" ");
sb.Append("(").Append(minDate.ToShortDateString()).Append("-").Append(maxDate.ToShortDateString()).Append(")");
sb.Append(showAsGainLoss ? Utility.FormatCurrency(modelPerformanceSeries[modelPerformanceSeries.Count - 1].CumulativeGainLoss) : Utility.FormatPercent(modelPerformanceSeries[modelPerformanceSeries.Count - 1].CumProdMinusOne));
return sb.ToString();
}
if (showAsGainLoss)
{
double latestGainLoss = modelPerformanceSeries[modelPerformanceSeries.Count - 1].CumulativeGainLoss;
double change = modelPerformanceSeries[modelPerformanceSeries.Count - 1].GainLossDOD;
sb.Append("$ GainLoss");
sb.Append(" ");
sb.Append("(").Append(minDate.ToShortDateString()).Append("-").Append(maxDate.ToShortDateString()).Append(")");
sb.Append(" ");
sb.Append(Utility.FormatCurrency(latestGainLoss));
sb.Append(",");
sb.Append(" ");
sb.Append(change > 0.00 ? "+" : "").Append(Utility.FormatCurrency(change));
}
else
{
double latestCumGainLoss = modelPerformanceSeries[modelPerformanceSeries.Count - 1].CumProdMinusOne;
double prevCumGainLoss = modelPerformanceSeries[modelPerformanceSeries.Count - 2].CumProdMinusOne;
double change = latestCumGainLoss - prevCumGainLoss;
sb.Append("% Return");
sb.Append(" ");
sb.Append("(").Append(minDate.ToShortDateString()).Append("-").Append(maxDate.ToShortDateString()).Append(")");
sb.Append(" ");
sb.Append(Utility.FormatPercent(latestCumGainLoss));
sb.Append(",");
sb.Append(" ");
sb.Append(change > 0.00 ? "+" : "").Append(Utility.FormatPercent(change));
}
return sb.ToString();
}
}
// *********************************************************** R E L A Y ***************************************************************
[RelayCommand(CanExecute = nameof(CanExecuteBollingerBands))]
public async Task BollingerBands()
{
await ExecuteBollingerBands();
}
public bool CanExecuteBollingerBands()
{
if (null == selectedPosition || null == selectedPosition.Symbol) return false;
return true;
}
[RelayCommand]
public async Task LoadFile()
{
await LoadTradeFile();
}
[RelayCommand]
public async Task Reload()
{
await ReloadTradeFile();
}
[RelayCommand(CanExecute = nameof(CanClosePosition))]
public async Task Close()
{
await OpenCloseDialog();
}
[RelayCommand(CanExecute = nameof(CanEdit))]
public async Task Edit()
{
await OpenEditDialog();
}
[RelayCommand]
public void ToggleReturnOrPercent()
{
HandleToggleReturnOrPercent();
}
[RelayCommand(CanExecute = nameof(CanAddToWatchList))]
public async Task AddToWatchList()
{
WatchListDA.AddToWatchList(selectedPosition.Symbol);
MxMessageBox.Show(GetTopLevelWindow(), $"Added {selectedPosition.Symbol} to WatchList", "Add To WatchList");
await Task.FromResult(true);
}
[RelayCommand(CanExecute = nameof(CanRemoveFromWatchList))]
public async Task RemoveFromWatchList()
{
WatchListDA.RemoveFromWatchList(selectedPosition.Symbol);
MxMessageBox.Show(GetTopLevelWindow(), $"Removed {selectedPosition.Symbol} from WatchList", "Remove From WatchList");
await Task.FromResult(true);
}
// ****************************************************************************************************************************************
public async Task ExecuteBollingerBands()
{
SaveParameters saveParams = SaveParameters.Parse("Type,PortfolioManager.ViewModels.BollingerBandViewModel,SelectedSymbol," + selectedPosition.Symbol + ",SelectedWatchList,{All},SelectedDayCount,90");
saveParams.Referer=this;
WorkspaceInstantiator.Invoke(saveParams);
await Task.FromResult(true);
}
public async Task OpenCloseDialog()
{
IPurePosition clonedPosition = Position.Clone(selectedPosition.Position);
ClosePositionDialog dialog = new ClosePositionDialog();
ClosePositionDialogViewModel closePositionViewModel = new ClosePositionDialogViewModel(dialog, clonedPosition);
dialog.DataContext = closePositionViewModel;
await dialog.ShowDialog(GetTopLevelWindow());
if (!closePositionViewModel.IsSuccess) return;
MomentumBacktest momentumModel = new MomentumBacktest();
if (!momentumModel.ClosePosition(clonedPosition.Symbol, clonedPosition.PurchaseDate, clonedPosition.SellDate, clonedPosition.CurrentPrice, pathFileName))
{
MxMessageBox.Show(GetTopLevelWindow(), "Failed to close the position, check log for details.", "Close Position");
return;
}
String strMessage = String.Format("Closed position for {0}, Purchase Date:{1}, Sell Date{2}, Current Price:{3}. Saved to {4}. A backup was created.",
clonedPosition.Symbol,
clonedPosition.PurchaseDate.ToShortDateString(),
clonedPosition.SellDate.ToShortDateString(),
Utility.FormatCurrency(clonedPosition.CurrentPrice),
pathFileName);
MxMessageBox.Show(GetTopLevelWindow(), strMessage, "Close Position");
LoadSessionFile();
await Task.FromResult(true);
}
public async Task OpenEditDialog()
{
EditPositionDialogNoStop dialog = new EditPositionDialogNoStop();
Position clonedPosition = Position.Clone(selectedPosition.Position);
EditPositionDialogNoStopViewModel editPositionViewModel = new EditPositionDialogNoStopViewModel(dialog, clonedPosition);
dialog.DataContext = editPositionViewModel;
await dialog.ShowDialog(GetTopLevelWindow());
GetTopLevelWindow().BringIntoView();
if (!editPositionViewModel.IsSuccess) return;
MomentumBacktest momentumBacktest = new MomentumBacktest();
if (!momentumBacktest.EditPosition(clonedPosition.Symbol, clonedPosition.PurchaseDate, clonedPosition.PurchasePrice, pathFileName))
{
MxMessageBox.Show(GetTopLevelWindow(), "Failed to edit the position, check log for details.", "Edit Position");
return;
}
selectedPosition.PurchaseDate = clonedPosition.PurchaseDate;
selectedPosition.PurchasePrice = clonedPosition.PurchasePrice;
String strMessage = String.Format("Edited Position for {0} Purchase Date:{1} Purchase Price:{2}. A backup was created.",
selectedPosition.Symbol, selectedPosition.PurchaseDate.ToShortDateString(), Utility.FormatCurrency(selectedPosition.PurchasePrice));
MxMessageBox.Show(GetTopLevelWindow(), strMessage, "Edit Position");
LoadSessionFile();
}
private void HandleToggleReturnOrPercent()
{
showAsGainLoss = !showAsGainLoss;
base.OnPropertyChanged("Data");
base.OnPropertyChanged("PercentButtonText");
base.OnPropertyChanged("GraphTitle");
}
public bool CanAddToWatchList()
{
if (null == selectedPosition) return false;
return WatchListDA.IsInWatchList(selectedPosition.Symbol) ? false : true;
}
public bool CanClosePosition()
{
if (null == selectedPosition || null == selectedPosition.Symbol) return false;
return true;
}
public bool CanEdit()
{
if (null == selectedPosition || null == selectedPosition.Symbol || !Utility.IsEpoch(selectedPosition.SellDate)) return false;
return true;
}
public bool CanRemoveFromWatchList()
{
if (null == selectedPosition) return false;
if (!WatchListDA.IsInWatchList(selectedPosition.Symbol)) return false;
PortfolioTrades portfolioTrades = PortfolioDA.GetOpenTrades();
if (portfolioTrades.Any(x => x.Symbol.Equals(selectedPosition.Symbol))) return false;
return true;
}
public async Task ReloadTradeFile()
{
LoadSessionFile();
await Task.FromResult(true);
}
public async Task LoadTradeFile()
{
TopLevel topLevel = TopLevel.GetTopLevel(GetTopLevelWindow());
IReadOnlyList<IStorageFile> files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open Trade File",
AllowMultiple = false,
});
IStorageFile storageFile = files.FirstOrDefault();
if (null == storageFile) return;
Uri uri = storageFile.Path;
pathFileName = uri.LocalPath;
if (null == pathFileName) return;
if (!MGSessionManager.IsValidSessionFile(pathFileName))
{
pathFileName = null;
}
else LoadSessionFile();
}
public bool LoadSessionFile()
{
IsBusy = true;
Task workerTask = Task.Factory.StartNew(() =>
{
try
{
if (!MGSessionManager.IsValidSessionFile(pathFileName))
{
MxMessageBox.Show(GetTopLevelWindow(), String.Format("'{0}' is not a valid model. IsValidSessionFile returned false.", pathFileName));
pathFileName = null;
return false;
}
sessionParams = MGSessionManager.RestoreSession(pathFileName);
if (null == sessionParams)
{
MxMessageBox.Show(GetTopLevelWindow(), String.Format("Unable to open '{0}'. Restore session failed.", pathFileName));
pathFileName = null;
return false;
}
modelStatistics = MomentumBacktest.GetModelStatistics(sessionParams);
modelPerformanceSeries = MomentumBacktest.GetModelPerformance(sessionParams);
configuration = sessionParams.Configuration;
NVPCollection nvpCollection = sessionParams.Configuration.ToNVPCollection();
nvpDictionary = nvpCollection.ToDictionary();
List<String> dictionaryKeys = new List<String>(nvpDictionary.Keys);
dictionaryKeys.Sort();
nvpDictionaryKeys = new ObservableCollection<String>(dictionaryKeys);
selectedParameter = nvpDictionaryKeys[0];
positions = new MGPositionModelCollection();
positions.Add(sessionParams.ActivePositions);
positions.Add(sessionParams.AllPositions);
UpdatePositionPrices(false);
UpdatePositionRSI3(true);
RunPerformance();
return true;
}
catch (Exception exception)
{
MxMessageBox.Show(GetTopLevelWindow(), String.Format("Exception {0}", exception.ToString()), "Error");
return false;
}
});
workerTask.ContinueWith(continuation =>
{
IsBusy = false;
base.OnPropertyChanged("Parameters");
base.OnPropertyChanged("SelectedParameter");
base.OnPropertyChanged("ParameterValue");
base.OnPropertyChanged("Title");
base.OnPropertyChanged("DisplayName");
base.OnPropertyChanged("AllPositions");
base.OnPropertyChanged("CashBalance");
base.OnPropertyChanged("NonTradeableCash");
base.OnPropertyChanged("ModelExpectation");
base.OnPropertyChanged("ExpectationColor");
base.OnPropertyChanged("ReloadEnabled");
});
return true;
}
private void UpdateTooltipProperties()
{
base.OnPropertyChanged("ExpectationDescription");
base.OnPropertyChanged("CompanyDescriptionSelectedPosition");
}
private void UpdatePositionPrices(bool change = true)
{
try
{
DateTime today = DateTime.Now;
if (null == positions || 0 == positions.Count) return;
List<String> symbols = (from MGPositionModel position in positions where position.IsActivePosition select position.Symbol).Distinct().ToList();
foreach (String symbol in symbols)
{
var selectedPositions = (from MGPositionModel position in positions where position.IsActivePosition && position.Symbol.Equals(symbol) select position);
foreach (MGPositionModel selectedPosition in selectedPositions)
{
Price price = PricingDA.GetPrice(symbol);
if (null == price) continue;
selectedPosition.CurrentPrice = price.Close;
selectedPosition.Volume = price.Volume;
selectedPosition.LastUpdated = price.Date.Date < today.Date ? price.Date : today;
}
}
if (change) positions.OnCollectionChanged();
}
catch (Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG, exception.ToString());
}
}
private void UpdatePositionRSI3(bool change = true)
{
try
{
if (null == positions || 0 == positions.Count) return;
List<String> symbols = (from MGPositionModel position in positions where position.IsActivePosition select position.Symbol).Distinct().ToList();
foreach (String symbol in symbols)
{
var selectedPositions = (from MGPositionModel position in positions where position.IsActivePosition && position.Symbol.Equals(symbol) select position);
foreach (MGPositionModel selectedPosition in selectedPositions)
{
RSICollection rsiCollection = RSIGenerator.GenerateRSI(symbol, 30, 3);
if (null == rsiCollection || 0 == rsiCollection.Count) continue;
selectedPosition.RSI3 = rsiCollection[rsiCollection.Count - 1].RSI;
}
}
if (change) positions.OnCollectionChanged();
}
catch (Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG, exception.ToString());
}
}
private void RunPerformance()
{
if (null == sessionParams) return;
modelPerformanceSeries = MomentumBacktest.GetModelPerformance(sessionParams);
base.OnPropertyChanged("Data");
base.OnPropertyChanged("GraphTitle");
}
// ********************************************************** P E R S I S T E N C E ******************************************************
public override bool CanPersist()
{
return true;
}
public override SaveParameters GetSaveParameters()
{
SaveParameters saveParams = new SaveParameters();
if (null == pathFileName) return null;
saveParams.Add(new KeyValuePair<String, String>("Type", GetType().Namespace + "." + GetType().Name));
saveParams.Add(new KeyValuePair<String, String>("PathFileName", pathFileName));
return saveParams;
}
public override void SetSaveParameters(SaveParameters saveParameters)
{
try
{
pathFileName = (from KeyValuePair<String, String> item in saveParameters where item.Key.Equals("PathFileName") select item).FirstOrDefault().Value;
if (!LoadSessionFile()) pathFileName = null;
}
catch (Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG, String.Format("Exception:{0}", exception.ToString()));
}
}
// ************************************************** T O O L T I P S *************************************************
public String ExpectationDescription
{
get
{
if (null == modelStatistics) return "";
StringBuilder sb = new StringBuilder();
sb.Append("Expectancy is (percentage of winning trades * average gain) / (percentage of losing trades * average loss).").Append("\n");
sb.Append("Total Trades : ").Append(modelStatistics.TotalTrades).Append("\n");
sb.Append("Winning Trades : ").Append(modelStatistics.WinningTrades).Append("\n");
sb.Append("Losing Trades : ").Append(modelStatistics.LosingTrades).Append("\n");
sb.Append("Winning Trades : ").Append(Utility.FormatNumber(modelStatistics.WinningTradesPercent, 2)).Append("%").Append("\n");
sb.Append("Losing Trades : ").Append(Utility.FormatNumber(modelStatistics.LosingTradesPercent, 2)).Append("%").Append("\n");
sb.Append("Average Winning Trade Gain : ").Append(Utility.FormatNumber(modelStatistics.AverageWinningTradePercentGain, 2)).Append("%").Append("\n");
sb.Append("Average Losing Trade Loss : ").Append(Utility.FormatNumber(modelStatistics.AverageLosingTradePercentLoss, 2)).Append("%").Append("\n");
sb.Append("Expectancy : ").Append(Utility.FormatNumber(modelStatistics.Expectancy, 2)).Append("\n");
sb.Append("\n");
sb.Append("Maintain a positive Expectancy and you're a winner.");
sb.Append("\n");
sb.Append("The calculations are based on closed positions.");
return sb.ToString();
}
}
public String CompanyDescriptionSelectedPosition
{
get
{
if (null == selectedPosition || null == selectedPosition.Symbol) return "No row selected.";
CompanyProfile companyProfile = CompanyProfileDA.GetCompanyProfile(selectedPosition.Symbol);
if (null == companyProfile || null == companyProfile.Description || "".Equals(companyProfile.Description)) return "No description found.";
StringBuilder sb = new StringBuilder();
sb.Append(companyProfile.Symbol).Append(" - ").Append(companyProfile.CompanyName).Append("\n");
sb.Append(companyProfile.Sector).Append("/").Append(companyProfile.Industry).Append("\n").Append(companyProfile.Description);
return sb.ToString();
}
}
}
}