@@ -69,6 +69,8 @@ private static TestCaseData[] TestParameters
6969 new TestCaseData ( Symbols . AAPL , Resolution . Daily , false , false , 60 , TickType . Trade , - 1 ) ,
7070 new TestCaseData ( Symbols . AAPL , Resolution . Daily , false , false , 6 , TickType . Trade , - 1 ) ,
7171
72+ new TestCaseData ( Symbols . SPX , Resolution . Tick , false , false , 60 * 6 * 2 , TickType . Trade , - 1 ) ,
73+
7274 // invalid tick type, null result
7375 new TestCaseData ( Symbols . AAPL , Resolution . Minute , true , false , 0 , TickType . Quote , - 1 ) ,
7476 new TestCaseData ( Symbols . AAPL , Resolution . Minute , true , false , 0 , TickType . OpenInterest , - 1 ) ,
@@ -82,6 +84,75 @@ private static TestCaseData[] TestParameters
8284 }
8385 }
8486
87+ private static TestCaseData [ ] OptionTestParameters
88+ {
89+ get
90+ {
91+ return new [ ]
92+ {
93+ // SPY
94+ new TestCaseData ( Symbols . SPY , Resolution . Daily , 20 ) ,
95+ new TestCaseData ( Symbols . SPY , Resolution . Hour , 30 ) ,
96+ new TestCaseData ( Symbols . SPY , Resolution . Minute , 60 * 10 ) ,
97+ new TestCaseData ( Symbols . SPY , Resolution . Second , 60 * 10 * 5 ) ,
98+ new TestCaseData ( Symbols . SPY , Resolution . Tick , 30 ) ,
99+
100+ // AAPL
101+ new TestCaseData ( Symbols . AAPL , Resolution . Daily , 20 ) ,
102+ new TestCaseData ( Symbols . AAPL , Resolution . Hour , 30 ) ,
103+ new TestCaseData ( Symbols . AAPL , Resolution . Minute , 60 * 10 ) ,
104+ new TestCaseData ( Symbols . AAPL , Resolution . Second , 60 * 10 * 5 ) ,
105+ new TestCaseData ( Symbols . AAPL , Resolution . Tick , 30 ) ,
106+
107+ // SPX
108+ new TestCaseData ( Symbols . SPX , Resolution . Daily , 20 ) ,
109+ new TestCaseData ( Symbols . SPX , Resolution . Hour , 30 ) ,
110+ new TestCaseData ( Symbols . SPX , Resolution . Minute , 60 * 10 ) ,
111+ new TestCaseData ( Symbols . SPX , Resolution . Second , 60 * 10 * 5 ) ,
112+ new TestCaseData ( Symbols . SPX , Resolution . Tick , 30 ) ,
113+
114+ //SPXW
115+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbols . SPX , "SPXW" , Market . USA , "?SPXW" ) , Resolution . Minute , 60 ) ,
116+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbols . SPX , "SPXW" , Market . USA , "?SPXW" ) , Resolution . Hour , 18 ) ,
117+
118+ // XSP
119+ new TestCaseData ( Symbol . Create ( "XSP" , SecurityType . Index , Market . USA ) , Resolution . Daily , 20 ) ,
120+ new TestCaseData ( Symbol . Create ( "XSP" , SecurityType . Index , Market . USA ) , Resolution . Hour , 30 ) ,
121+ new TestCaseData ( Symbol . Create ( "XSP" , SecurityType . Index , Market . USA ) , Resolution . Minute , 60 * 10 ) ,
122+ new TestCaseData ( Symbol . Create ( "XSP" , SecurityType . Index , Market . USA ) , Resolution . Second , 60 * 10 * 5 ) ,
123+ new TestCaseData ( Symbol . Create ( "XSP" , SecurityType . Index , Market . USA ) , Resolution . Tick , 30 ) ,
124+
125+ // QQQ Options (ETF Options)
126+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbol . Create ( "QQQ" , SecurityType . Equity , Market . USA ) ) , Resolution . Daily , 20 ) ,
127+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbol . Create ( "QQQ" , SecurityType . Equity , Market . USA ) ) , Resolution . Hour , 30 ) ,
128+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbol . Create ( "QQQ" , SecurityType . Equity , Market . USA ) ) , Resolution . Minute , 60 * 10 ) ,
129+
130+ // IWM Options (ETF Options)
131+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbol . Create ( "IWM" , SecurityType . Equity , Market . USA ) ) , Resolution . Daily , 20 ) ,
132+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbol . Create ( "IWM" , SecurityType . Equity , Market . USA ) ) , Resolution . Hour , 30 ) ,
133+
134+ // BRK.B Options (Equity with dot ticker)
135+ new TestCaseData ( Symbol . CreateCanonicalOption ( Symbol . Create ( "BRK.B" , SecurityType . Equity , Market . USA ) ) , Resolution . Daily , 10 )
136+ } ;
137+ }
138+ }
139+
140+ private static TestCaseData [ ] InvalidOptionTestParameters
141+ {
142+ get
143+ {
144+ return new [ ]
145+ {
146+ // Forex symbols should return empty (not supported by LookupSymbols)
147+ new TestCaseData ( Symbols . EURUSD , Resolution . Daily , 0 ) ,
148+ new TestCaseData ( Symbol . Create ( "GBPUSD" , SecurityType . Forex , Market . USA ) , Resolution . Hour , 0 ) ,
149+
150+ // Crypto symbols should return empty (not supported by LookupSymbols)
151+ new TestCaseData ( Symbol . Create ( "BTCUSD" , SecurityType . Crypto , Market . USA ) , Resolution . Minute , 0 ) ,
152+ } ;
153+ }
154+ }
155+
85156 [ OneTimeSetUp ]
86157 public void Setup ( )
87158 {
@@ -107,7 +178,7 @@ public void GetsHistory(Symbol symbol, Resolution resolution, bool unsupported,
107178 if ( _useSandbox && ( resolution == Resolution . Tick || resolution == Resolution . Second ) )
108179 {
109180 // sandbox doesn't allow tick data, we generate second resolution from tick
110- return ;
181+ Assert . Fail ( "sandbox doesn't allow tick data or Second data resolution" ) ;
111182 }
112183 var mhdb = MarketHoursDatabase . FromDataFolder ( ) . GetEntry ( symbol . ID . Market , symbol , symbol . SecurityType ) ;
113184
@@ -146,37 +217,52 @@ public void GetsHistory(Symbol symbol, Resolution resolution, bool unsupported,
146217 }
147218 }
148219
149- [ TestCase ( Resolution . Daily , 20 ) ]
150- [ TestCase ( Resolution . Hour , 30 ) ]
151- [ TestCase ( Resolution . Minute , 60 * 10 ) ]
152- [ TestCase ( Resolution . Second , 60 * 10 * 5 ) ]
153- [ TestCase ( Resolution . Tick , 30 ) ]
154- public void GetsOptionHistory ( Resolution resolution , int expectedCount )
220+ [ Test , TestCaseSource ( nameof ( OptionTestParameters ) ) ]
221+ public void GetsOptionHistory ( Symbol symbol , Resolution resolution , int expectedCount )
155222 {
156223 if ( _useSandbox && ( resolution == Resolution . Tick || resolution == Resolution . Second ) )
157224 {
158225 // sandbox doesn't allow tick data, we generate second resolution from tick
159- return ;
226+ Assert . Fail ( "sandbox doesn't allow tick data or Second data resolution" ) ;
160227 }
161- var spy = Symbol . Create ( "SPY" , SecurityType . Equity , Market . USA ) ;
162- var mhdb = MarketHoursDatabase . FromDataFolder ( ) . GetEntry ( spy . ID . Market , spy , spy . SecurityType ) ;
163228
164- GetStartEndTime ( mhdb , resolution , expectedCount , false , out var startUtc , out var endUtc ) ;
229+ var mhdb = MarketHoursDatabase . FromDataFolder ( ) . GetEntry ( symbol . ID . Market , symbol , symbol . SecurityType ) ;
165230
166- var chain = _chainProvider . GetOptionContractList ( spy , startUtc . ConvertFromUtc ( mhdb . ExchangeHours . TimeZone ) ) . ToList ( ) ;
231+ GetStartEndTime ( mhdb , resolution , expectedCount , false , out var startUtc , out var endUtc ) ;
167232
168- var quote = _brokerage . GetQuotes ( new ( ) { "SPY" } ) . First ( ) . Last ;
169- var option = chain . Where ( x => x . ID . OptionRight == OptionRight . Call )
170- // drop weeklies
171- . Where ( x => OptionSymbol . IsStandard ( x ) )
172- // not expired
233+ var chain = _brokerage . LookupSymbols ( symbol , includeExpired : false ) ? . ToList ( ) ?? [ ] ;
234+
235+ if ( chain . Count == 0 )
236+ {
237+ Assert . Fail ( $ "No options found for { symbol . Value } ") ;
238+ }
239+ // Get quote for the underlying symbol
240+ var underlyingSymbol = symbol . Underlying ?? symbol ;
241+ // Convert dot tickers to brokerage format (slashes) when sending raw strings
242+ var mapper = new TradierSymbolMapper ( brokerageSymbol => brokerageSymbol ) ;
243+ var underlyingTickerForBrokerage = mapper . GetBrokerageSymbol ( underlyingSymbol ) ;
244+ var quote = _brokerage . GetQuotes ( new ( ) { underlyingTickerForBrokerage } ) ? . FirstOrDefault ( ) ? . Last ?? 0 ;
245+ if ( quote == 0 )
246+ {
247+ Assert . Fail ( $ "No quote available for { symbol . Value } . Cannot proceed with option selection.") ;
248+ }
249+ // Improved option selection logic
250+ var option = chain
251+ // Include both standard and weekly options (many liquid options are weeklies)
173252 . Where ( x => x . ID . Date >= endUtc . ConvertFromUtc ( mhdb . ExchangeHours . TimeZone ) )
174- // closest to expire first
253+ // Prefer calls for better liquidity
254+ . Where ( x => x . ID . OptionRight == OptionRight . Call )
255+ // Order by expiration (closest first)
175256 . OrderBy ( x => x . ID . Date )
176- // most in the money
177- . ThenBy ( x => x . ID . StrikePrice )
178- // but not too far in the money
179- . First ( x => ( x . ID . StrikePrice + quote * 0.01m ) > quote ) ;
257+ // Then by strike proximity to current price (ATM or slightly ITM)
258+ . ThenBy ( x => Math . Abs ( x . ID . StrikePrice - quote ) )
259+ // Take the first one that should have reasonable liquidity
260+ . FirstOrDefault ( ) ;
261+
262+ if ( option == null )
263+ {
264+ Assert . Fail ( $ "No suitable options found for { symbol . Value } . Available options: { chain . Count } ") ;
265+ }
180266
181267 var request = new HistoryRequest ( startUtc ,
182268 endUtc ,
@@ -192,12 +278,23 @@ public void GetsOptionHistory(Resolution resolution, int expectedCount)
192278 TickType . Trade ) ;
193279
194280 var count = GetHistoryHelper ( request ) ;
195-
281+
196282 // more than X points
197- Assert . Greater ( count , 15 , $ "Symbol: { request . Symbol . Value } . Resolution { request . Resolution } ") ;
283+ Assert . Greater ( count , 0 , $ "Symbol: { request . Symbol . Value } . Resolution { request . Resolution } ") ;
284+ }
285+
286+ [ Test , TestCaseSource ( nameof ( InvalidOptionTestParameters ) ) ]
287+ public void LookupSymbolsReturnsEmptyForUnsupportedSymbols ( Symbol symbol , Resolution resolution , int expectedCount )
288+ {
289+ // Test that LookupSymbols correctly returns empty for unsupported symbol types
290+ var chain = _brokerage . LookupSymbols ( symbol , includeExpired : false ) ? . ToList ( ) ?? [ ] ;
291+
292+ // Should return empty for unsupported symbol types
293+ Assert . AreEqual ( 0 , chain . Count ,
294+ $ "LookupSymbols should return empty for { symbol . SecurityType } symbol { symbol . Value } , but returned { chain . Count } options") ;
198295 }
199296
200- private void GetStartEndTime ( MarketHoursDatabase . Entry entry , Resolution resolution , int expectedCount ,
297+ private void GetStartEndTime ( MarketHoursDatabase . Entry entry , Resolution resolution , int expectedCount ,
201298 bool extendedMarketHours , out DateTime startTimeUtc , out DateTime endTimeUtc )
202299 {
203300 if ( resolution == Resolution . Tick || resolution == Resolution . Second )
@@ -229,7 +326,7 @@ private int GetHistoryHelper(HistoryRequest request)
229326
230327 if ( previous != null )
231328 {
232- if ( request . Resolution == Resolution . Tick )
329+ if ( request . Resolution == Resolution . Tick )
233330 {
234331 Assert . IsTrue ( previous . EndTime <= data . EndTime ) ;
235332 }
0 commit comments