In the code below, the line ...
I'm using the free version of the Zacks earnings provider, but it seems to be working perfectly annotating each earnings event with a green triangular tooltip as expected. I'm running WL Build 141. This was not a problem with Build 138.
CODE:fails to return the "latest" earnings, although it does return the earlier ones. In contrast, the line employing the Fundamental indicator works as expected returning a TimeSeries with all the earnings. I tried recreating the EarningsDateCache.txt cache, but that didn't fix the problem.
List<EventDataPoint> earnings = bars.GetEventDataPoints("Earnings");
I'm using the free version of the Zacks earnings provider, but it seems to be working perfectly annotating each earnings event with a green triangular tooltip as expected. I'm running WL Build 141. This was not a problem with Build 138.
CODE:
using WealthLab.Backtest; using WealthLab.Core; using WealthLab.Indicators; using WealthLab.Data; //* Fundamental event data here *// using System.Collections.Generic; namespace WealthScript1 { public class MyStrategy : UserStrategyBase { public override void Initialize(BarHistory bars) { List<EventDataPoint> earnings = bars.GetEventDataPoints("Earnings"); //fails to return latest earnings WriteToDebugLog(earnings.Count + "=earnings.Count"); EventDataPoint latestEarnings = earnings[earnings.Count-1]; WriteToDebugLog(latestEarnings.Date.ToShortDateString() + "=latest earnings date"); //earnings date isn't the latest IndicatorBase ind1 = new Fundamental(bars,"Earnings",true); //works as expected returning "all" earnings PlotIndicator(ind1, WLColor.Black, PlotStyle.GradientBlocks); } public override void Execute(BarHistory bars, int idx) { } } }
Rename
I ran your code and their was no problem. Earnings were returned for all valid symbols that I tested. While I understand the earnings events are showing in the charts perhaps they are cached (guessing). Double-check in the Data Manager, in the event providers tab, that Zacks is checked AND earnings is checked (in the right pane) for Zacks. I am running build 141 with all extensions up-to-date.
As an aside, not all symbols have their earnings events returned in date order, so you might want to do this:
As an aside, not all symbols have their earnings events returned in date order, so you might want to do this:
CODE:
using System.Linq; //... then in your Initialize method... List<EventDataPoint> earnings = bars.GetEventDataPoints("Earnings").OrderBy(e => e.Date).ToList();
QUOTE:
not all symbols have their earnings events returned in date order
That's what changed from WL Build 138 to Build 141. I have a method that compares the most recent four quarters to the previous four quarters to compute the annual earnings growth. That method "assumes" an ordered earnings List<> by date. (Why on earth would one want scrambled ordering?)
I would like to suggest that the Zacks provider employ a SortedSet datatype to save sorted earnings by date on disk as *.QF files.
The YCharts provider has the same problem for "dividends". It needs to save its dividends on disk as SortedSet's by date. If they are presorted on disk, they will load faster as a sorted datatype.
I now have a simple question. How can I easily Find(...), in an unordered earnings event List<>, the one earnings event with the Max() date without doing the presort shown below. (And without installing a special Linq extension library.) If it requires two passes [say with Max() followed by Find()] that's okay.
CODE:Yes, I know a brute force single-pass FOREACH procedure would be fastest. I'm just trying to learn some simple Linq here.
List<EventDataPoint> earnings = bars.GetEventDataPoints("Earnings").OrderBy(e => e.Date).ToList(); if (earnings.Count != 0) { EventDataPoint fiEPSmrq = earnings[earnings.Count-1]; //most recent quarter (mrq) earnings
Use MaxBy. Here's your original code adjusted accordingly...
Also, see: https://www.csharptutorial.net/csharp-linq/linq-maxby/
CODE:
using WealthLab.Backtest; using WealthLab.Core; using WealthLab.Indicators; using WealthLab.Data; //* Fundamental event data here *// using System.Collections.Generic; using System.Linq; namespace WealthScript1 { public class MyStrategy : UserStrategyBase { public override void Initialize(BarHistory bars) { List<EventDataPoint> earnings = bars.GetEventDataPoints("Earnings"); WriteToDebugLog(earnings.Count + "=earnings.Count"); if (earnings.Count > 0) { EventDataPoint latestEarnings = earnings.MaxBy(e => e.Date); WriteToDebugLog(latestEarnings.Date.ToShortDateString() + "=latest earnings date"); IndicatorBase ind1 = new Fundamental(bars, "Earnings", true); PlotIndicator(ind1, WLColor.Black, PlotStyle.GradientBlocks); } } public override void Execute(BarHistory bars, int idx) { } } }
Also, see: https://www.csharptutorial.net/csharp-linq/linq-maxby/
Thanks Paul! Your solution works well without downloading a special Linq extension method library.
I actually looked at the .MaxBy() Linq method on Stack Overflow, but it sounded like one had to download a special GitHub library to use it, which I didn't want to do. But apparently, .MaxBy() is actually included (now anyways) in the standard System.Linq library without any special download--which is just what I was looking for. Thanks again for all the help.
I actually looked at the .MaxBy() Linq method on Stack Overflow, but it sounded like one had to download a special GitHub library to use it, which I didn't want to do. But apparently, .MaxBy() is actually included (now anyways) in the standard System.Linq library without any special download--which is just what I was looking for. Thanks again for all the help.
For those WL C# coders that want to use Earnings (or any other Fundamental event value) in their C# strategies, I've included the code below for GETting the latest Earnings from the Zacks event provider. Be sure to first install the WL Fundamental extension and enable the Zacks event provider as well as select its checkbox for "Earnings".
Notice that I'm using the .MaxBy() Linq query mentioned in Post #4 to fetch the Earnings object with the latest (or highest) Date. Happy coding.
CODE:As you can see from the code below, results are returned as a Dictionary<...,string> object because these stats are scraped from the Zacks webpage as strings. If you want to make comparisons in your strategy, you'll need to convert these strings to double with the .NET Parse method (not shown).
---Symbol by Symbol Debug Logs--- ---AAPL--- 9/2025=latestEarnings.Details[PeriodEnding] $1.73=latestEarnings.Details[Estimate] +0.12=latestEarnings.Details[Surprise] +6.94%=latestEarnings.Details[SurprisePct] After Close=latestEarnings.Details[Time]
Notice that I'm using the .MaxBy() Linq query mentioned in Post #4 to fetch the Earnings object with the latest (or highest) Date. Happy coding.
CODE:
using WealthLab.Backtest; using WealthLab.Core; using WealthLab.Data; //* Fundamental event data here *// using System.Linq; namespace WealthScript92 { public class MyStrategy : UserStrategyBase { public override void Initialize(BarHistory bars) { EventDataPoint? latestEarnings = bars.GetEventDataPoints("Earnings").MaxBy(e => e.Date); if (latestEarnings != null) //Is there an earnings report? foreach (string keyItem in latestEarnings.Details.Keys) WriteToDebugLog(latestEarnings.Details[keyItem] + "=latestEarnings.Details[" + keyItem + "]"); } public override void Execute(BarHistory bars, int idx) { } } }
Below is a handy string extension method that takes a string and a decimal separator, keeps only the digits and the decimal separator, and then attempts to convert the resulting string to a double. If the input string cannot be converted then the method returns double.NaN. The reason for the decimal separator parameter, rather than relying on region settings, is that the source data (input) may have been produced using a region that is not necessarily the same as the user's current region setting.
Here's an example of using the extension method:
CODE:
using System; using System.Linq; using System.Globalization; public static class StringExtensions { /// <summary> /// Given a string, keep the numbers and the given decimal separator and convert /// the resulting string to a double. If the input cannot be converted then /// double.NaN is returned. /// </summary> /// <param name="input">The string to be converted to a double. It may contain non-numeric characters.</param> /// <param name="decimalSeparator">Specifies the decimal separator used in the input string</param> /// <returns>The input string converted to a double, if possible. /// If the input string cannot be converted to a double then double.NaN is returned.</returns> public static double ToFilteredDouble(this string input, char decimalSeparator = '.') { if (string.IsNullOrEmpty(input)) return double.NaN; // Keep only digits and the specified decimal separator var filtered = new string( input.Where(c => char.IsDigit(c) || c == decimalSeparator).ToArray() ); // If no digits remain, return NaN if (string.IsNullOrEmpty(filtered) || !filtered.Any(char.IsDigit)) return double.NaN; // make sure decimalSeparator is used by double.TryParse and not the regional settings var nfi = new NumberFormatInfo { NumberDecimalSeparator = decimalSeparator.ToString() }; // Parse with the custom format (and, yes, NumberStyles.Float is the correct style) if (double.TryParse(filtered, NumberStyles.Float, nfi, out double result)) return result; return double.NaN; } }
Here's an example of using the extension method:
CODE:
WriteToDebugLog(latestEarnings.Details["Estimate"].ToFilteredDouble('.'));
I asked CoPilot to create a more efficient version of the code in Post #7. The following is clearly better. CoPilot also estimated the following runs about 4 times as fast...
CODE:
using System; using System.Globalization; //... public static class StringExtensions { /// <summary> /// Given a string, keep the numbers and the given decimal separator and convert /// the resulting string to a double. If the input cannot be converted then /// double.NaN is returned. /// </summary> /// <param name="input">The string to be converted to a double. It may contain non-numeric characters.</param> /// <param name="decimalSeparator">Specifies the decimal separator used in the input string</param> /// <returns>The input string converted to a double, if possible. /// If the input string cannot be converted to a double then double.NaN is returned.</returns> public static double ToFilteredDouble(this string input, char decimalSeparator = '.') { if (string.IsNullOrEmpty(input)) return double.NaN; // Use stackalloc for small strings to avoid heap allocation Span<char> buffer = input.Length <= 256 ? stackalloc char[input.Length] : new char[input.Length]; int length = 0; bool hasDigit = false; foreach (char c in input) { if (char.IsDigit(c)) { buffer[length++] = c; hasDigit = true; } else if (c == decimalSeparator) { buffer[length++] = c; } } if (!hasDigit || length == 0) return double.NaN; var nfi = new NumberFormatInfo { NumberDecimalSeparator = decimalSeparator.ToString() }; // Parse directly from the span without creating a new string return double.TryParse(buffer[..length], NumberStyles.Float, nfi, out double result) ? result : double.NaN; } }
Not to belabor the point, but if all you want is the "earnings price" (i.e. latestEarnings.Value), that's returned as a double by default; see code below. However, if you want any of the "Details" part from a free provider, that's returned as a string instead. The problem is those strings contain weird characters like '+', '$', and '%', which will trip up the Parse (or TryParse) .NET method, so Paul's solution strips out those weird characters before calling TryParse. I hope that clarifies this discussion better.
CODE:As a trader (not an investor), I use the "free" Fundamentals (such as Zacks) just to check for problems, and the "Details" are returned as strings, which you can employ Paul's code to parse into double's. But if you're using a paid Fundamental subscriber, which downloads much faster, they may likely be returned as double's in the first place.
public override void Initialize(BarHistory bars) { EventDataPoint? latestEarnings = bars.GetEventDataPoints("Earnings").MaxBy(e => e.Date); if (latestEarnings != null) WriteToDebugLog(latestEarnings.Value + "=earnings per share"); //earnings themselves returned as a "double" foreach (string keyItem in latestEarnings.Details.Keys) WriteToDebugLog(latestEarnings.Details[keyItem] + "=latestEarnings.Details[" + keyItem + "]"); }
Your Response
Post
Edit Post
Login is required