using MBN_STOCK_WEBVIEW.Core.Playout; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW; /// /// Application composition root for native DB-to-scene routes. Routes are added only /// when their explicit loader and mutation builder have been ported and tested. /// internal static class LegacySceneRuntimeFactory { public static LegacyPlayoutWorkflow Create( IPlayoutEngine engine, IDataQueryExecutor executor, LegacySceneCueCompositionOptions? compositionOptions = null, ILegacySceneCueCompositionOptionsSource? compositionOptionsSource = null, IS5025TrustedManualDataSource? trustedManualSource = null, ITrustedViStockNameSnapshotSource? trustedViSource = null) { ArgumentNullException.ThrowIfNull(engine); ArgumentNullException.ThrowIfNull(executor); var revenueLoader = new S5076SceneDataLoader(executor); var profitLoader = new S5081SceneDataLoader(executor); var tradingLoader = new S5082SceneDataLoader(executor); var nxtDetailLoader = new S5006NxtSceneDataLoader(executor); var domesticDetailLoader = new S5006DomesticSceneDataLoader(executor); var equityDetailLoader = new S5011SceneDataLoader(executor); var threePanelLoader = new S5016SceneDataLoader(executor); var twoPanelLoader = new S50160SceneDataLoader(executor); var imageQuoteLoader = new S6001SceneDataLoader(executor); var worldMapLoader = new S8067SceneDataLoader(executor); var candleLoader = new S8010SceneDataLoader(executor); var fiveRowLoader = new S5074SceneDataLoader(executor); var sixRowLoader = new S5077SceneDataLoader(executor); var twelveRowLoader = new S5088SceneDataLoader(executor); var parameterizedRequestResolver = new LegacyParameterizedSceneRequestResolver(executor); var oneColumnLoader = new S5001SceneDataLoader(executor); var futuresPairLoader = new S5032SceneDataLoader(executor); var twoColumnLoader = new S8018SceneDataLoader(executor); var sectorSquareLoader = new S8001SceneDataLoader(executor); var comparisonRequestResolver = new ComparisonAndYieldLegacyRequestResolver(executor); var candlePairLoader = new S5026SceneDataLoader(executor); var comparisonYieldLoader = new S5029SceneDataLoader(executor); var singleYieldLoader = new S5086SceneDataLoader(executor); var quotedYieldLoader = new S50860SceneDataLoader(executor); var linePairLoader = new S5087SceneDataLoader(executor); var chartRequestResolver = new ChartLegacySceneRequestResolver(); var sectorIndexLoader = new S5078SceneDataLoader(executor); var growthMetricsLoader = new S5079SceneDataLoader(executor); var salesLoader = new S5080SceneDataLoader(executor); var participantTrendLoader = new S5083SceneDataLoader(executor); var indexTrendLoader = new S5084SceneDataLoader(executor); if (trustedManualSource is null) { var manualDataDirectory = Environment.GetEnvironmentVariable( S5025TrustedManualFileDataSource.DirectoryEnvironmentVariable); trustedManualSource = string.IsNullOrWhiteSpace(manualDataDirectory) ? null : new S5025TrustedManualFileDataSource(manualDataDirectory); } var gridMarketRequestResolver = new LegacyGridMarketSceneRequestResolver( executor, trustedManualSource); var dailyInvestorLoader = new S5023SceneDataLoader(executor); var investorBarLoader = new S5024SceneDataLoader(executor); var traderLoader = new S5037SceneDataLoader(executor); var programTradingLoader = new S5085SceneDataLoader(executor); var institutionNetBuyLoader = new S6067SceneDataLoader(executor); var orderBookLoader = new S8003SceneDataLoader(executor); var dataSource = new LegacySceneDataSourceRouter( [ new LegacySceneDataSourceRoute(["5076"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var data = await revenueLoader.LoadAsync( selection.Subject, cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s5076", data, 1); }), new LegacySceneDataSourceRoute(["5081"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var data = await profitLoader.LoadAsync( selection.Subject, cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s5081", data, 1); }), new LegacySceneDataSourceRoute(["5082"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var data = await tradingLoader.LoadAsync(cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s5082", data, 1); }), new LegacySceneDataSourceRoute(["5001", "N5001"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var request = parameterizedRequestResolver.CreateS5001Request(entry); var data = await oneColumnLoader.LoadAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5001", data, 1); }), new LegacySceneDataSourceRoute(["8018", "8032", "5032"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var resolved = await parameterizedRequestResolver.ResolveTwoColumnRequestAsync( entry, cancellationToken).ConfigureAwait(false); return resolved switch { LegacyS5032LoadRequest request => new LegacySceneDataPage( "s5032", await futuresPairLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS8018LoadRequest request => new LegacySceneDataPage( "s8018", await twoColumnLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), _ => throw new LegacySceneDataException( "The two-column scene selection is invalid.") }; }), new LegacySceneDataSourceRoute(["8001", "8002"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var request = parameterizedRequestResolver.CreateS8001Request(entry); var data = await sectorSquareLoader.LoadAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s8001", data, 1); }), new LegacySceneDataSourceRoute( ["5026", "5029", "5086", "50860", "5087"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var resolved = await comparisonRequestResolver.ResolveAsync( entry, cancellationToken).ConfigureAwait(false); return resolved switch { LegacyS5026SceneLoadRequest request => new LegacySceneDataPage( "s5026", await candlePairLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5029SceneLoadRequest request => new LegacySceneDataPage( "s5029", await comparisonYieldLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5086SceneLoadRequest request => new LegacySceneDataPage( "s5086", await singleYieldLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS50860SceneLoadRequest request => new LegacySceneDataPage( "s50860", await quotedYieldLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5087SceneLoadRequest request => new LegacySceneDataPage( "s5087", await linePairLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), _ => throw new LegacySceneDataException( "The comparison scene selection is invalid.") }; }), new LegacySceneDataSourceRoute( ["5078", "5079", "5080", "5083", "5084"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var resolved = chartRequestResolver.Resolve(entry); return resolved switch { LegacyS5078ChartLoadRequest request => new LegacySceneDataPage( "s5078", await sectorIndexLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5079ChartLoadRequest request => new LegacySceneDataPage( "s5079", await growthMetricsLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5080ChartLoadRequest request => new LegacySceneDataPage( "s5080", await salesLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5083ChartLoadRequest request => new LegacySceneDataPage( "s5083", await participantTrendLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5084ChartLoadRequest request => new LegacySceneDataPage( "s5084", await indexTrendLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), _ => throw new LegacySceneDataException( "The chart scene selection is invalid.") }; }), new LegacySceneDataSourceRoute( ["5023", "5024", "5025", "5037", "5085", "6067", "8003"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var resolved = await gridMarketRequestResolver.ResolveAsync( entry, cancellationToken).ConfigureAwait(false); return resolved switch { LegacyS5023SceneLoadRequest request => new LegacySceneDataPage( "s5023", await dailyInvestorLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5024SceneLoadRequest request => new LegacySceneDataPage( "s5024", await investorBarLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5025SceneLoadRequest => new LegacySceneDataPage( "s5025", await gridMarketRequestResolver.LoadTrustedS5025Async( entry, cancellationToken).ConfigureAwait(false), 1), LegacyS5037SceneLoadRequest request => new LegacySceneDataPage( "s5037", await traderLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), LegacyS5085SceneLoadRequest => new LegacySceneDataPage( "s5085", await programTradingLoader.LoadAsync(cancellationToken) .ConfigureAwait(false), 1), LegacyS6067SceneLoadRequest => new LegacySceneDataPage( "s6067", await institutionNetBuyLoader.LoadAsync(cancellationToken) .ConfigureAwait(false), 1), LegacyS8003SceneLoadRequest request => new LegacySceneDataPage( "s8003", await orderBookLoader.LoadAsync(request.Request, cancellationToken) .ConfigureAwait(false), 1), _ => throw new LegacySceneDataException( "The grid-market scene selection is invalid.") }; }), new LegacySceneDataSourceRoute(["5006"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var market = ParseDomesticMarket(selection.GroupCode); S5006SceneData data; if (market is LegacyDomesticEquityMarket.NxtKospi or LegacyDomesticEquityMarket.NxtKosdaq) { data = await nxtDetailLoader.LoadAsync( market, selection.DataCode, cancellationToken).ConfigureAwait(false); } else { data = await domesticDetailLoader.LoadAsync( new S5006DomesticSceneDataRequest(market, selection.DataCode), cancellationToken).ConfigureAwait(false); } return new LegacySceneDataPage("s5006", data, 1); }), new LegacySceneDataSourceRoute(["5011"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var data = await equityDetailLoader.LoadAsync( new S5011SceneDataRequest( ParseDomesticMarket(selection.GroupCode), ParseClosedEnum(selection.Subtype, "5011 detail branch"), selection.DataCode), cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s5011", data, 1); }), new LegacySceneDataSourceRoute(["5016"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var data = await threePanelLoader.LoadAsync( ParseClosedEnum(selection.Subtype, "5016 panel target"), cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s5016", data, 1); }), new LegacySceneDataSourceRoute(["50160"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var data = await twoPanelLoader.LoadAsync( ParseClosedEnum(selection.Subtype, "50160 panel target"), cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s50160", data, 1); }), new LegacySceneDataSourceRoute(["6001"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var data = await imageQuoteLoader.LoadAsync( ParseClosedEnum(selection.Subtype, "6001 quote target"), cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s6001", data, 1); }), new LegacySceneDataSourceRoute( ["8067", "5068", "5070", "5072"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var data = await worldMapLoader.LoadAsync(cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s8067", data, 1); }), new LegacySceneDataSourceRoute( ["8035", "8061", "8040", "8046", "8051", "8056"], async (entry, page, cancellationToken) => { RequireFirstPage(page); var selection = RequireSelection(entry); var target = ParseCandleMarket(selection.GroupCode); var foreignIdentity = target is CandleMarketTarget.OverseasIndex or CandleMarketTarget.OverseasStock ? ParseForeignCandleIdentity(selection.DataCode, target) : default; var request = new S8010SceneDataRequest( ParseCandleAlias(entry.CutCode), target, ParseCandleMode(selection.Subtype), CandleRequiresInstrument(target) ? RequireLookupValue(selection.Subject, "instrument") : null, HasClosedFlag(selection.GraphicType, "MA5"), HasClosedFlag(selection.GraphicType, "MA20"), foreignIdentity.Symbol, foreignIdentity.NationCode); var data = await candleLoader.LoadAsync( request, cancellationToken).ConfigureAwait(false); return new LegacySceneDataPage("s8010", data, 1); }), new LegacySceneDataSourceRoute(["5074"], async (entry, page, cancellationToken) => { var selection = RequireSelection(entry); var request = selection.GroupCode == "PAGED_VI" ? await TrustedViManualReference.ResolveAsync( selection, page, trustedViSource, cancellationToken).ConfigureAwait(false) : CreatePagedQuoteRequest(selection, page); var data = await fiveRowLoader.LoadAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5074", data, data.Rows.Count); }, async (entry, cancellationToken) => { var selection = RequireSelection(entry); var request = selection.GroupCode == "PAGED_VI" ? await TrustedViManualReference.ResolveAsync( selection, pageIndexZeroBased: 0, trustedSource: trustedViSource, cancellationToken: cancellationToken).ConfigureAwait(false) : CreatePagedQuoteRequest(selection, 0); var data = await fiveRowLoader.LoadPagePlanAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5074", data, data.Rows.Count); }), new LegacySceneDataSourceRoute(["5077"], async (entry, page, cancellationToken) => { var request = CreatePagedQuoteRequest(RequireSelection(entry), page); var data = await sixRowLoader.LoadAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5077", data, data.Rows.Count); }, async (entry, cancellationToken) => { var request = CreatePagedQuoteRequest(RequireSelection(entry), 0); var data = await sixRowLoader.LoadPagePlanAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5077", data, data.Rows.Count); }), new LegacySceneDataSourceRoute(["5088"], async (entry, page, cancellationToken) => { var request = CreatePagedQuoteRequest(RequireSelection(entry), page); var data = await twelveRowLoader.LoadAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5088", data, data.Rows.Count); }, async (entry, cancellationToken) => { var request = CreatePagedQuoteRequest(RequireSelection(entry), 0); var data = await twelveRowLoader.LoadPagePlanAsync(request, cancellationToken) .ConfigureAwait(false); return new LegacySceneDataPage("s5088", data, data.Rows.Count); }) ]); LegacySceneRuntimeCoverage.Validate(dataSource); if (compositionOptions is not null && compositionOptionsSource is not null) { throw new ArgumentException( "Specify either fixed or mutable scene composition options, not both."); } var cueProvider = compositionOptionsSource is null ? new RegisteredLegacySceneCueProvider(dataSource, options: compositionOptions) : new RegisteredLegacySceneCueProvider(dataSource, compositionOptionsSource); return new LegacyPlayoutWorkflow(engine, cueProvider); } private static LegacySceneSelection RequireSelection(LegacyPlaylistEntry entry) => entry.Selection ?? throw new LegacySceneDataException( "The selected cut requires native database lookup information."); private static void RequireFirstPage(int pageIndexZeroBased) { if (pageIndexZeroBased != 0) { throw new LegacySceneDataException("The selected cut is not paged."); } } private static LegacyDomesticEquityMarket ParseDomesticMarket(string value) => value switch { "KOSPI" or "KOSPI_STOCK" or "코스피" or "코스피종목" => LegacyDomesticEquityMarket.Kospi, "KOSDAQ" or "KOSDAQ_STOCK" or "코스닥" or "코스닥종목" => LegacyDomesticEquityMarket.Kosdaq, "NXT_KOSPI" or "NXT코스피" or "코스피(NXT)" => LegacyDomesticEquityMarket.NxtKospi, "NXT_KOSDAQ" or "NXT코스닥" or "코스닥(NXT)" => LegacyDomesticEquityMarket.NxtKosdaq, _ => throw new LegacySceneDataException( "The domestic equity market selection is invalid.") }; private static CandleCutAlias ParseCandleAlias(string value) => value switch { "8035" => CandleCutAlias.Cut8035, "8061" => CandleCutAlias.Cut8061, "8040" => CandleCutAlias.Cut8040, "8046" => CandleCutAlias.Cut8046, "8051" => CandleCutAlias.Cut8051, "8056" => CandleCutAlias.Cut8056, _ => throw new LegacySceneDataException("The candle cut alias is invalid.") }; private static CandleMarketTarget ParseCandleMarket(string value) => value switch { "KOSPI_INDEX" or "코스피지수" => CandleMarketTarget.KospiIndex, "KOSDAQ_INDEX" or "코스닥지수" => CandleMarketTarget.KosdaqIndex, "KOSPI200_INDEX" or "코스피200" => CandleMarketTarget.Kospi200Index, "KRX100_INDEX" or "KRX100" => CandleMarketTarget.Krx100Index, "FUTURES_INDEX" or "선물" => CandleMarketTarget.FuturesIndex, "KOSPI_INDUSTRY" or "코스피업종" => CandleMarketTarget.KospiIndustry, "KOSDAQ_INDUSTRY" or "코스닥업종" => CandleMarketTarget.KosdaqIndustry, "FOREIGN_INDEX" or "해외지수" => CandleMarketTarget.OverseasIndex, "FOREIGN_STOCK" or "해외종목" => CandleMarketTarget.OverseasStock, "KOSPI_STOCK" or "코스피종목" => CandleMarketTarget.KospiStock, "KOSDAQ_STOCK" or "코스닥종목" => CandleMarketTarget.KosdaqStock, _ => throw new LegacySceneDataException("The candle market selection is invalid.") }; private static CandleChartMode ParseCandleMode(string value) => value switch { "" or "PRICE" or "현재가" => CandleChartMode.Price, "EXPECTED" or "예상체결" => CandleChartMode.ExpectedExecution, "VOLUME" or "거래량" => CandleChartMode.PriceWithVolume, "TRADING_HALT" or "거래정지" => CandleChartMode.TradingHalt, _ => throw new LegacySceneDataException("The candle chart mode is invalid.") }; private static bool CandleRequiresInstrument(CandleMarketTarget value) => value is CandleMarketTarget.KospiIndustry or CandleMarketTarget.KosdaqIndustry or CandleMarketTarget.OverseasIndex or CandleMarketTarget.OverseasStock or CandleMarketTarget.KospiStock or CandleMarketTarget.KosdaqStock; private static (string? Symbol, string? NationCode) ParseForeignCandleIdentity( string value, CandleMarketTarget target) { var parts = value.Split('|', StringSplitOptions.None); var expectedType = target switch { CandleMarketTarget.OverseasIndex => "0", CandleMarketTarget.OverseasStock => "1", _ => throw new LegacySceneDataException( "The foreign candle target is invalid.") }; if (parts.Length != 3 || parts[0].Length is < 1 or > 64 || parts[0].Any(character => char.IsControl(character) || char.IsSurrogate(character)) || parts[1].Length != 2 || parts[1].Any(character => character is < 'A' or > 'Z') || !string.Equals(parts[2], expectedType, StringComparison.Ordinal) || target == CandleMarketTarget.OverseasStock && parts[1] is not ("US" or "TW")) { throw new LegacySceneDataException( "The foreign candle identity is invalid."); } return (parts[0], parts[1]); } private static string RequireLookupValue(string value, string label) { if (string.IsNullOrWhiteSpace(value)) { throw new LegacySceneDataException($"The scene {label} selection is required."); } return value; } private static bool HasClosedFlag(string value, string flag) => value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) .Contains(flag, StringComparer.Ordinal); private static PagedQuoteLoadRequest CreatePagedQuoteRequest( LegacySceneSelection selection, int pageIndexZeroBased) { var page = checked(pageIndexZeroBased + 1); return selection.GroupCode switch { "PAGED_DOMESTIC_KOSPI" => new DomesticPagedQuoteLoadRequest( PagedQuoteMarket.Kospi, ParseDomesticPagedList(selection.Subtype), ParseAsOfDate(selection.DataCode), page), "PAGED_DOMESTIC_KOSDAQ" => new DomesticPagedQuoteLoadRequest( PagedQuoteMarket.Kosdaq, ParseDomesticPagedList(selection.Subtype), ParseAsOfDate(selection.DataCode), page), "PAGED_NXT_KOSPI" => new NxtPagedQuoteLoadRequest( PagedQuoteMarket.Kospi, ParseNxtPagedList(selection.Subtype), ParseNxtSession(selection.GraphicType), page), "PAGED_NXT_KOSDAQ" => new NxtPagedQuoteLoadRequest( PagedQuoteMarket.Kosdaq, ParseNxtPagedList(selection.Subtype), ParseNxtSession(selection.GraphicType), page), "PAGED_THEME" => new ThemePagedQuoteLoadRequest( RequireLookupValue(selection.DataCode, "theme code"), RequireLookupValue(selection.Subject, "theme"), ParseThemeSort(selection.GraphicType), ParseThemeValueKind(selection.Subtype), page), "PAGED_NXT_THEME" => new NxtThemePagedQuoteLoadRequest( RequireLookupValue(selection.DataCode, "NXT theme code"), RequireLookupValue(selection.Subject, "NXT theme"), ParseThemeSort(selection.Subtype), ParseNxtSession(selection.GraphicType), page), "PAGED_EXPERT" => new ExpertPagedQuoteLoadRequest( RequireLookupValue(selection.DataCode, "expert code"), RequireLookupValue(selection.Subject, "expert"), page), "PAGED_VI" => new ViPagedQuoteLoadRequest( ParseStockNames(selection.Subject), page), _ => throw new LegacySceneDataException("The paged quote branch is invalid.") }; } private static DomesticPagedQuoteList ParseDomesticPagedList(string value) => value switch { "EXPECTED_MARKET_CAP" => DomesticPagedQuoteList.ExpectedMarketCapitalization, "MARKET_CAP" => DomesticPagedQuoteList.MarketCapitalization, "INDUSTRY" => DomesticPagedQuoteList.Industry, "AFTER_HOURS_GAIN_RATE" => DomesticPagedQuoteList.AfterHoursGainRate, "AFTER_HOURS_LOSS_RATE" => DomesticPagedQuoteList.AfterHoursLossRate, "GAIN_RATE" => DomesticPagedQuoteList.GainRate, "LOSS_RATE" => DomesticPagedQuoteList.LossRate, "52_WEEK_HIGH" => DomesticPagedQuoteList.FiftyTwoWeekHigh, "52_WEEK_LOW" => DomesticPagedQuoteList.FiftyTwoWeekLow, "KOSPI200" => DomesticPagedQuoteList.Kospi200, "ETF" => DomesticPagedQuoteList.Etf, _ => throw new LegacySceneDataException("The domestic paged quote list is invalid.") }; private static NxtPagedQuoteList ParseNxtPagedList(string value) => value switch { "MARKET_CAP" => NxtPagedQuoteList.MarketCapitalization, "AFTER_HOURS_GAIN_RATE" => NxtPagedQuoteList.AfterHoursGainRate, "AFTER_HOURS_LOSS_RATE" => NxtPagedQuoteList.AfterHoursLossRate, "GAIN_RATE" => NxtPagedQuoteList.GainRate, "LOSS_RATE" => NxtPagedQuoteList.LossRate, "VOLUME" => NxtPagedQuoteList.Volume, _ => throw new LegacySceneDataException("The NXT paged quote list is invalid.") }; private static NxtMarketSession ParseNxtSession(string value) => value switch { "PRE_MARKET" => NxtMarketSession.PreMarket, "AFTER_MARKET" => NxtMarketSession.AfterMarket, _ => throw new LegacySceneDataException("The NXT market session is invalid.") }; private static PagedQuoteThemeSort ParseThemeSort(string value) => value switch { "" or "INPUT_ORDER" => PagedQuoteThemeSort.InputOrder, "GAIN_DESC" => PagedQuoteThemeSort.GainRateDescending, "GAIN_ASC" => PagedQuoteThemeSort.GainRateAscending, _ => throw new LegacySceneDataException("The theme sort selection is invalid.") }; private static PagedQuoteThemeValueKind ParseThemeValueKind(string value) => value switch { "" or "CURRENT" => PagedQuoteThemeValueKind.Current, "EXPECTED" => PagedQuoteThemeValueKind.ExpectedExecution, _ => throw new LegacySceneDataException("The theme value selection is invalid.") }; private static DateOnly ParseAsOfDate(string value) => DateOnly.TryParseExact( value, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var date) ? date : throw new LegacySceneDataException("The paged quote as-of date is invalid."); private static IReadOnlyList ParseStockNames(string value) { var names = value.Split( ',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (names.Length is < 1 or > 240) { throw new LegacySceneDataException("The VI stock-name selection is invalid."); } return Array.AsReadOnly(names); } private static TEnum ParseClosedEnum(string value, string label) where TEnum : struct, Enum { if (!Enum.TryParse(value, ignoreCase: false, out var parsed) || !Enum.IsDefined(parsed)) { throw new LegacySceneDataException($"The {label} selection is invalid."); } return parsed; } }