Skip to content

Commit

Permalink
moved security timeliness calculations into separate model and added …
Browse files Browse the repository at this point in the history
…unit tests, added new configuration tab for quotes and moved existing config setting to update quotes on startup there, included list of stale securities into tooltip
  • Loading branch information
ma4nn committed Oct 9, 2022
1 parent 27d57f5 commit ea119c0
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package name.abuchen.portfolio.util;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;

import org.junit.Test;

import name.abuchen.portfolio.model.LatestSecurityPrice;
import name.abuchen.portfolio.model.Security;

@SuppressWarnings("nls")
public class SecurityTimelinessTest
{
private static final LocalDate LOCAL_DATE = LocalDate.of(2020, 5, 6);
private Clock clock;

public SecurityTimelinessTest()
{
this.clock = Clock.fixed(LOCAL_DATE.atStartOfDay(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault());
}

@Test
public void testStaleIfNoLatestFeed()
{
Security security = new Security();
security.setRetired(false);

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertTrue(st.isStale());
}

@Test
public void testNotStaleIfRetired()
{
Security security = new Security();
security.setRetired(true);

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertFalse(st.isStale());
}

@Test
public void testStaleIfNotUpdatedWithin8DaysWith1HolidayAndWeekend()
{
Security security = new Security();
security.setRetired(false);
security.setLatest(new LatestSecurityPrice(LocalDate.of(2020, 4, 23), 10));

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertTrue(st.isStale());
}

@Test
public void testNotStaleIfNotUpdatedWithin7DaysWith1HolidayAndWeekend()
{
Security security = new Security();
security.setRetired(false);
security.setLatest(new LatestSecurityPrice(LocalDate.of(2020, 4, 24), 10));

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertFalse(st.isStale());
}

@Test
public void testNotStaleIfUpdatedToday()
{
Security security = new Security();
security.setRetired(false);
security.setLatest(new LatestSecurityPrice(LocalDate.of(2020, 5, 6), 10));

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertFalse(st.isStale());
}

@Test
public void testNotStaleIfUpdatedYesterday()
{
Security security = new Security();
security.setRetired(false);
security.setLatest(new LatestSecurityPrice(LocalDate.of(2020, 5, 5), 10));

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertFalse(st.isStale());
}

@Test
public void testStaleIfUpdatedYesterdayAndInterval0()
{
Security security = new Security();
security.setRetired(false);
security.setLatest(new LatestSecurityPrice(LocalDate.of(2020, 5, 5), 10));

SecurityTimeliness st = new SecurityTimeliness(security, 0, this.clock);

assertTrue(st.isStale());
}

@Test
public void testNoHolidaysOrWeekendsIfSecurityHasNoCalendar()
{
Security security = new Security();
security.setRetired(false);
security.setLatest(new LatestSecurityPrice(LocalDate.of(2020, 4, 28), 10));
security.setCalendar("empty");

SecurityTimeliness st = new SecurityTimeliness(security, 7, this.clock);

assertTrue(st.isStale());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ interface Preferences // NOSONAR
* Preference for directory from which to import CSV files
*/
String CSV_IMPORT_PATH = "CSV_IMPORT_PATH"; //$NON-NLS-1$

String QUOTES_STALE_AFTER_DAYS_PATH = "QUOTES_STALE_AFTER_DAYS_PATH"; //$NON-NLS-1$
}

interface CSS // NOSONAR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import name.abuchen.portfolio.ui.preferences.PresetsPreferencePage;
import name.abuchen.portfolio.ui.preferences.ProxyPreferencePage;
import name.abuchen.portfolio.ui.preferences.QuandlPreferencePage;
import name.abuchen.portfolio.ui.preferences.QuotesPreferencePage;
import name.abuchen.portfolio.ui.preferences.ThemePreferencePage;
import name.abuchen.portfolio.ui.preferences.UpdatePreferencePage;

Expand Down Expand Up @@ -69,6 +70,8 @@ public void execute(@Named(IServiceConstants.ACTIVE_SHELL) Shell shell,

pm.addToRoot(new PreferenceNode("calendar", new CalendarPreferencePage())); //$NON-NLS-1$

pm.addToRoot(new PreferenceNode("quotes", new QuotesPreferencePage()));

pm.addToRoot(new PreferenceNode("api", new APIKeyPreferencePage())); //$NON-NLS-1$
pm.addTo("api", new PreferenceNode("alphavantage", new AlphaVantagePreferencePage())); //$NON-NLS-1$ //$NON-NLS-2$
pm.addTo("api", new PreferenceNode("divvydiary", new DivvyDiaryPreferencePage())); //$NON-NLS-1$ //$NON-NLS-2$
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ public GeneralPreferencePage()
@Override
public void createFieldEditors()
{
addField(new BooleanFieldEditor(UIConstants.Preferences.UPDATE_QUOTES_AFTER_FILE_OPEN, //
Messages.PrefUpdateQuotesAfterFileOpen, getFieldEditorParent()));

addField(new BooleanFieldEditor(UIConstants.Preferences.STORE_SETTINGS_NEXT_TO_FILE, //
Messages.PrefStoreSettingsNextToFile, getFieldEditorParent()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public void initializeDefaultPreferences()
store.setDefault(UIConstants.Preferences.CALENDAR, "default"); //$NON-NLS-1$
store.setDefault(UIConstants.Preferences.PORTFOLIO_REPORT_API_URL, "https://api.portfolio-report.net"); //$NON-NLS-1$
store.setDefault(UIConstants.Preferences.PRESET_VALUE_TIME, PresetValues.TimePreset.MIDNIGHT.name());
store.setDefault(UIConstants.Preferences.QUOTES_STALE_AFTER_DAYS_PATH, 7);

// Backup
store.setDefault(UIConstants.Preferences.BACKUP_MODE, BackupMode.getDefault().name());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package name.abuchen.portfolio.ui.preferences;

import org.eclipse.jface.preference.BooleanFieldEditor;
import org.eclipse.jface.preference.FieldEditorPreferencePage;
import org.eclipse.jface.preference.IntegerFieldEditor;

import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.UIConstants;

public class QuotesPreferencePage extends FieldEditorPreferencePage
{
public QuotesPreferencePage()
{
super(GRID);
setTitle("Quotes");
}

@Override
public void createFieldEditors()
{
addField(new BooleanFieldEditor(UIConstants.Preferences.UPDATE_QUOTES_AFTER_FILE_OPEN, //
Messages.PrefUpdateQuotesAfterFileOpen, getFieldEditorParent()));

addField(new IntegerFieldEditor(UIConstants.Preferences.QUOTES_STALE_AFTER_DAYS_PATH,
"Number of days after a security price is not up-to-date anymore",
getFieldEditorParent()));

createNoteComposite(getFieldEditorParent().getFont(), getFieldEditorParent(), //
Messages.PrefLabelNote,
"After this amount of days a security price is marked as not up-to-date.\nPlease note that only days with open trade markets are considered\n(depends on the configured calendar for each security)");
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package name.abuchen.portfolio.ui.views.dashboard;

import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.Clock;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.eclipse.e4.core.di.extensions.Preference;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.swt.SWT;
Expand All @@ -12,38 +17,30 @@
import org.eclipse.swt.widgets.Label;

import name.abuchen.portfolio.model.Dashboard.Widget;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.UIConstants;
import name.abuchen.portfolio.ui.util.Colors;
import name.abuchen.portfolio.ui.util.InfoToolTip;
import name.abuchen.portfolio.ui.util.swt.ColoredLabel;
import name.abuchen.portfolio.util.SecurityTimeliness;
import name.abuchen.portfolio.util.TextUtil;

public class SecurityPriceTimelinessWidget extends WidgetDelegate<Number>
{
/**
* @see name.abuchen.portfolio.ui.views.SecuritiesTable#addColumnDateOfLatestPrice()
*/
private static final int CONSIDER_AS_OLD_AFTER_DAYS = 7;
protected Label title;
protected ColoredLabel indicator;
private LocalDate daysAgo;
private long oldSecuritiesCount;
private List<Security> staleSecurities;
private long allSecuritiesCount;

@Preference(value = UIConstants.Preferences.QUOTES_STALE_AFTER_DAYS_PATH)
@Inject
private int numberOfTradeDaysToLookBack;

protected SecurityPriceTimelinessWidget(Widget widget, DashboardData dashboardData)
{
super(widget, dashboardData);

this.daysAgo = LocalDate.now().minusDays(CONSIDER_AS_OLD_AFTER_DAYS);

this.oldSecuritiesCount = this.getClient().getSecurities().stream()
.filter(s -> !s.isRetired()
&& (s.getLatest() == null || s.getLatest().getDate().isBefore(this.daysAgo)))
.count();

this.allSecuritiesCount = this.getClient().getSecurities().stream().filter(s -> !s.isRetired()).count();
}

@Override
Expand All @@ -67,10 +64,9 @@ public Composite createControl(Composite parent, DashboardResources resources)

GridDataFactory.fillDefaults().grab(true, false).applyTo(indicator);

InfoToolTip.attach(indicator, () -> {
return MessageFormat.format(Messages.TooltipSecurityPriceTimeliness, this.oldSecuritiesCount, this.allSecuritiesCount,
CONSIDER_AS_OLD_AFTER_DAYS);
});
this.update();

InfoToolTip.attach(indicator, this::getTooltip);

return container;
}
Expand All @@ -91,7 +87,30 @@ public void update(Number value)
@Override
public Supplier<Number> getUpdateTask()
{
return () -> 1 - (double) this.oldSecuritiesCount / this.allSecuritiesCount;
this.staleSecurities = this.getClient().getSecurities().stream().filter(
s -> (new SecurityTimeliness(s, this.numberOfTradeDaysToLookBack, Clock.systemDefaultZone()))
.isStale())
.collect(Collectors.toList());

this.allSecuritiesCount = this.getClient().getSecurities().stream().filter(s -> !s.isRetired()).count();

return () -> this.allSecuritiesCount > 0 ? 1 - (double) this.staleSecurities.size() / this.allSecuritiesCount
: 0;
}

private String getTooltip()
{
if (this.staleSecurities == null)
return ""; //$NON-NLS-1$

String securities = this.staleSecurities.stream()
.map(s -> s.getName() + (s.getLatest() != null
? " (" + Values.Date.format(s.getLatest().getDate()) + ")" //$NON-NLS-1$//$NON-NLS-2$
: "")) //$NON-NLS-1$
.sorted().collect(Collectors.joining("\n")); //$NON-NLS-1$

return MessageFormat.format(Messages.TooltipSecurityPriceTimeliness, this.staleSecurities.size(),
this.allSecuritiesCount, this.numberOfTradeDaysToLookBack)
+ (!securities.equals("") ? ":\n\n" + securities : ""); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package name.abuchen.portfolio.util;

import java.time.Clock;
import java.time.LocalDate;

import name.abuchen.portfolio.model.Security;

public final class SecurityTimeliness
{
private Security security;
private TradeCalendar tradeCalendar;
private Clock clock;
private int numberOfTradeDaysToLookBack;

public SecurityTimeliness(Security security, int numberOfTradeDaysToLookBack, Clock clock)
{
this.security = security;
this.numberOfTradeDaysToLookBack = numberOfTradeDaysToLookBack;
this.clock = clock;
this.tradeCalendar = TradeCalendarManager.getInstance(security);
}

public boolean isStale()
{
final LocalDate daysAgo = this.getStartDate();

return !this.security.isRetired()
&& (this.security.getLatest() == null || this.security.getLatest().getDate().isBefore(daysAgo));
}

private LocalDate getStartDate()
{
LocalDate currentDay = LocalDate.now(this.clock);
while (this.numberOfTradeDaysToLookBack > 0)
{
currentDay = currentDay.minusDays(1);

if (this.tradeCalendar.isHoliday(currentDay) || this.tradeCalendar.isWeekend(currentDay))
{
continue;
}

numberOfTradeDaysToLookBack--;
}

return currentDay;
}
}

0 comments on commit ea119c0

Please sign in to comment.