private IEnumerable <Slice> GetSlices(Symbol symbol, int initialVolume) { var subscriptionDataConfig = new SubscriptionDataConfig(typeof(ZipEntryName), symbol, Resolution.Second, TimeZones.Utc, TimeZones.Utc, true, true, false); var security = new Security(SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), subscriptionDataConfig, new Cash(CashBook.AccountCurrency, 0, 1m), SymbolProperties.GetDefault(CashBook.AccountCurrency)); var refTime = DateTime.UtcNow; return(Enumerable .Range(0, 10) .Select(i => { var time = refTime.AddSeconds(i); var bid = new Bar(100, 100, 100, 100); var ask = new Bar(110, 110, 110, 110); var volume = (i + 1) * initialVolume; return TimeSlice.Create( time, TimeZones.Utc, new CashBook(), new List <DataFeedPacket> { new DataFeedPacket(security, subscriptionDataConfig, new List <BaseData> { new QuoteBar(time, symbol, bid, i * 10, ask, (i + 1) * 11), new TradeBar(time, symbol, 100, 100, 110, 106, volume) }), }, new SecurityChanges(Enumerable.Empty <Security>(), Enumerable.Empty <Security>()), new Dictionary <Universe, BaseDataCollection>()) .Slice; })); }
/// <summary> /// Gets the history for the requested securities /// </summary> /// <param name="requests">The historical data requests</param> /// <param name="sliceTimeZone">The time zone used when time stamping the slice instances</param> /// <returns>An enumerable of the slices of data covering the span specified in each request</returns> public IEnumerable <Slice> GetHistory(IEnumerable <HistoryRequest> requests, DateTimeZone sliceTimeZone) { var securitiesByDateTime = GetSecuritiesByDateTime(requests); var count = securitiesByDateTime.Count; var i = 0; foreach (var kvp in securitiesByDateTime) { var utcDateTime = kvp.Key; var securities = kvp.Value; var last = Convert.ToDecimal(100 + 10 * Math.Sin(Math.PI * (360 - count + i) / 180.0)); var high = last * 1.005m; var low = last / 1.005m; var packets = new List <DataFeedPacket>(); foreach (var security in securities) { var configuration = security.Subscriptions.FirstOrDefault(x => x.Resolution == security.Resolution); var period = security.Resolution.ToTimeSpan(); var time = (utcDateTime - period).ConvertFromUtc(configuration.DataTimeZone); var data = new TradeBar(time, security.Symbol, last, high, last, last, 1000, period); security.SetMarketPrice(data); packets.Add(new DataFeedPacket(security, configuration, new List <BaseData> { data })); } i++; yield return(TimeSlice.Create(utcDateTime, sliceTimeZone, _cashBook, packets, _securityChanges).Slice); } }
public void TimeSliceCreateDoesNotThrowNullReferanceWhenUnderlyingSecurityLastDataIsNull() { var optionSymbol = Symbol.Create("SVXY", SecurityType.Option, Market.USA); var underlyingSecurity = new Equity(optionSymbol.Underlying, SecurityExchangeHours.AlwaysOpen(DateTimeZone.Utc), new Cash("USD", 0, 1), SymbolProperties.GetDefault("USD")); var subscriptionDataConfig = new SubscriptionDataConfig( typeof(DailyFx), optionSymbol, Resolution.Daily, TimeZones.Utc, TimeZones.Utc, true, true, false, isCustom: true); var optionSecurity = new Option(optionSymbol, SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), new Cash(CashBook.AccountCurrency, 0, 1m), new OptionSymbolProperties(SymbolProperties.GetDefault("USD"))) { Underlying = underlyingSecurity }; var refTime = DateTime.UtcNow; var timeSlice = TimeSlice.Create(refTime, TimeZones.Utc, new CashBook(), new List <DataFeedPacket> { new DataFeedPacket(optionSecurity, subscriptionDataConfig, new List <BaseData> { new QuoteBar { Symbol = optionSymbol, Time = refTime, Value = 1, Ask = new Bar(1, 1, 1, 1), Bid = new Bar(1, 1, 1, 1) } }) }, new SecurityChanges(Enumerable.Empty <Security>(), Enumerable.Empty <Security>()), new Dictionary <Universe, BaseDataCollection>()); Assert.AreEqual(timeSlice.SecurityChanges.Count, 0); }
public void ChangeState(T nextState) { if (Equals(nextState, CurrentState)) { return; } HashSet <OnExit> exitCallbacks; if (OnStateExit.TryGetValue(CurrentState, out exitCallbacks)) { foreach (var callback in exitCallbacks) { callback(nextState); } } PreviousState = CurrentState; CurrentState = nextState; HashSet <OnEnter> enterCallbacks; if (OnStateEnter.TryGetValue(CurrentState, out enterCallbacks)) { foreach (var callback in enterCallbacks) { callback(PreviousState); } } StateEnterTime = TimeSlice.Create(); StateChanged.Publish(PreviousState, CurrentState); }
public void HandlesMultipleCustomDataOfSameTypeWithDifferentSymbols() { var symbol1 = Symbol.Create("SCF/CBOE_VX1_EW", SecurityType.Base, Market.USA); var symbol2 = Symbol.Create("SCF/CBOE_VX2_EW", SecurityType.Base, Market.USA); var subscriptionDataConfig1 = new SubscriptionDataConfig( typeof(QuandlFuture), symbol1, Resolution.Daily, TimeZones.Utc, TimeZones.Utc, true, true, false, isCustom: true); var subscriptionDataConfig2 = new SubscriptionDataConfig( typeof(QuandlFuture), symbol2, Resolution.Daily, TimeZones.Utc, TimeZones.Utc, true, true, false, isCustom: true); var security1 = new Security( SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), subscriptionDataConfig1, new Cash(CashBook.AccountCurrency, 0, 1m), SymbolProperties.GetDefault(CashBook.AccountCurrency), ErrorCurrencyConverter.Instance ); var security2 = new Security( SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), subscriptionDataConfig1, new Cash(CashBook.AccountCurrency, 0, 1m), SymbolProperties.GetDefault(CashBook.AccountCurrency), ErrorCurrencyConverter.Instance ); var timeSlice = TimeSlice.Create(DateTime.UtcNow, TimeZones.Utc, new CashBook(), new List <DataFeedPacket> { new DataFeedPacket(security1, subscriptionDataConfig1, new List <BaseData> { new QuandlFuture { Symbol = symbol1, Time = DateTime.UtcNow.Date, Value = 15 } }), new DataFeedPacket(security2, subscriptionDataConfig2, new List <BaseData> { new QuandlFuture { Symbol = symbol2, Time = DateTime.UtcNow.Date, Value = 20 } }), }, new SecurityChanges(Enumerable.Empty <Security>(), Enumerable.Empty <Security>()), new Dictionary <Universe, BaseDataCollection>()); Assert.AreEqual(2, timeSlice.CustomData.Count); var data1 = timeSlice.CustomData[0].Data[0]; var data2 = timeSlice.CustomData[1].Data[0]; Assert.IsInstanceOf(typeof(QuandlFuture), data1); Assert.IsInstanceOf(typeof(QuandlFuture), data2); Assert.AreEqual(symbol1, data1.Symbol); Assert.AreEqual(symbol2, data2.Symbol); Assert.AreEqual(15, data1.Value); Assert.AreEqual(20, data2.Value); }
public void HandlesTicks_ExpectInOrderWithNoDuplicates() { var subscriptionDataConfig = new SubscriptionDataConfig( typeof(Tick), Symbols.EURUSD, Resolution.Tick, TimeZones.Utc, TimeZones.Utc, true, true, false); var security = new Security( SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), subscriptionDataConfig, new Cash(CashBook.AccountCurrency, 0, 1m), SymbolProperties.GetDefault(CashBook.AccountCurrency), ErrorCurrencyConverter.Instance ); DateTime refTime = DateTime.UtcNow; Tick[] rawTicks = Enumerable .Range(0, 10) .Select(i => new Tick(refTime.AddSeconds(i), Symbols.EURUSD, 1.3465m, 1.34652m)) .ToArray(); IEnumerable <TimeSlice> timeSlices = rawTicks.Select(t => TimeSlice.Create( t.Time, TimeZones.Utc, new CashBook(), new List <DataFeedPacket> { new DataFeedPacket(security, subscriptionDataConfig, new List <BaseData>() { t }) }, new SecurityChanges(Enumerable.Empty <Security>(), Enumerable.Empty <Security>()), new Dictionary <Universe, BaseDataCollection>())); Tick[] timeSliceTicks = timeSlices.SelectMany(ts => ts.Slice.Ticks.Values.SelectMany(x => x)).ToArray(); Assert.AreEqual(rawTicks.Length, timeSliceTicks.Length); for (int i = 0; i < rawTicks.Length; i++) { Assert.IsTrue(Compare(rawTicks[i], timeSliceTicks[i])); } }
public void HandlesMultipleCustomDataOfSameTypeSameSymbol() { var symbol = Symbol.Create("DFX", SecurityType.Base, Market.USA); var subscriptionDataConfig = new SubscriptionDataConfig( typeof(DailyFx), symbol, Resolution.Daily, TimeZones.Utc, TimeZones.Utc, true, true, false, isCustom: true); var security = new Security( SecurityExchangeHours.AlwaysOpen(TimeZones.Utc), subscriptionDataConfig, new Cash(CashBook.AccountCurrency, 0, 1m), SymbolProperties.GetDefault(CashBook.AccountCurrency), ErrorCurrencyConverter.Instance ); var refTime = DateTime.UtcNow; var timeSlice = TimeSlice.Create(refTime, TimeZones.Utc, new CashBook(), new List <DataFeedPacket> { new DataFeedPacket(security, subscriptionDataConfig, new List <BaseData> { new DailyFx { Symbol = symbol, Time = refTime, Title = "Item 1" }, new DailyFx { Symbol = symbol, Time = refTime, Title = "Item 2" }, }), }, new SecurityChanges(Enumerable.Empty <Security>(), Enumerable.Empty <Security>()), new Dictionary <Universe, BaseDataCollection>()); Assert.AreEqual(1, timeSlice.CustomData.Count); var data1 = timeSlice.CustomData[0].Data[0]; var data2 = timeSlice.CustomData[0].Data[1]; Assert.IsInstanceOf(typeof(DailyFx), data1); Assert.IsInstanceOf(typeof(DailyFx), data2); Assert.AreEqual(symbol, data1.Symbol); Assert.AreEqual(symbol, data2.Symbol); Assert.AreEqual("Item 1", ((DailyFx)data1).Title); Assert.AreEqual("Item 2", ((DailyFx)data2).Title); }
public IEnumerable <Slice> GetHistory <T>(Symbol symbol, Resolution resolution, IEnumerable <T> data) where T : IBaseData { var subscriptionDataConfig = GetSubscriptionDataConfig <T>(symbol, resolution); var security = GetSecurity(subscriptionDataConfig); return(data.Select(t => TimeSlice.Create( t.Time, TimeZones.Utc, new CashBook(), new List <DataFeedPacket> { new DataFeedPacket(security, subscriptionDataConfig, new List <BaseData>() { t as BaseData }) }, new SecurityChanges(Enumerable.Empty <Security>(), Enumerable.Empty <Security>())).Slice)); }
public IEnumerator <TimeSlice> GetEnumerator() { var dataFeedPacket = new DataFeedPacket(_algorithm.Securities[_symbol], _algorithm.SubscriptionManager.Subscriptions.First(s => s.Symbol == _symbol), new List <BaseData> { _dividend }, Ref.CreateReadOnly(() => false)); yield return(TimeSlice.Create(DateTime.UtcNow, TimeZones.NewYork, _algorithm.Portfolio.CashBook, new List <DataFeedPacket> { dataFeedPacket }, SecurityChanges.None, new Dictionary <Universe, BaseDataCollection>() )); }
/// <summary> /// Creates an enumerable of Slice to update the alpha model /// </summary> protected virtual IEnumerable <Slice> CreateSlices() { var cashBook = new CashBook(); var changes = SecurityChanges.None; var sliceDateTimes = GetSliceDateTimes(MaxSliceCount); for (var i = 0; i < sliceDateTimes.Count; i++) { var utcDateTime = sliceDateTimes[i]; var packets = new List <DataFeedPacket>(); // TODO : Give securities different values -- will require updating all derived types var last = Convert.ToDecimal(100 + 10 * Math.Sin(Math.PI * i / 180.0)); var high = last * 1.005m; var low = last / 1.005m; foreach (var kvp in _algorithm.Securities) { var security = kvp.Value; var exchange = security.Exchange.Hours; var extendedMarket = security.IsExtendedMarketHours; var localDateTime = utcDateTime.ConvertFromUtc(exchange.TimeZone); if (!exchange.IsOpen(localDateTime, extendedMarket)) { continue; } var configuration = security.Subscriptions.FirstOrDefault(); var period = security.Resolution.ToTimeSpan(); var time = (utcDateTime - period).ConvertFromUtc(configuration.DataTimeZone); var tradeBar = new TradeBar(time, security.Symbol, last, high, low, last, 1000, period); packets.Add(new DataFeedPacket(security, configuration, new List <BaseData> { tradeBar })); } if (packets.Count > 0) { yield return(TimeSlice.Create(utcDateTime, TimeZones.NewYork, cashBook, packets, changes, new Dictionary <Universe, BaseDataCollection>()).Slice); } } }
/// <summary> /// Gets the history for the requested securities /// </summary> /// <param name="requests">The historical data requests</param> /// <param name="sliceTimeZone">The time zone used when time stamping the slice instances</param> /// <returns>An enumerable of the slices of data covering the span specified in each request</returns> public override IEnumerable <Slice> GetHistory(IEnumerable <HistoryRequest> requests, DateTimeZone sliceTimeZone) { var configsByDateTime = GetSubscriptionDataConfigByDateTime(requests); var count = configsByDateTime.Count; var i = 0; foreach (var kvp in configsByDateTime) { var utcDateTime = kvp.Key; var configs = kvp.Value; var last = Convert.ToDecimal(100 + 10 * Math.Sin(Math.PI * (360 - count + i) / 180.0)); var high = last * 1.005m; var low = last / 1.005m; var packets = new List <DataFeedPacket>(); foreach (var config in configs) { Security security; if (!_securities.TryGetValue(config.Symbol, out security)) { continue; } var period = config.Resolution.ToTimeSpan(); var time = (utcDateTime - period).ConvertFromUtc(config.DataTimeZone); var data = new TradeBar(time, config.Symbol, last, high, last, last, 1000, period); security.SetMarketPrice(data); packets.Add(new DataFeedPacket(security, config, new List <BaseData> { data })); } i++; yield return(TimeSlice.Create(utcDateTime, sliceTimeZone, _cashBook, packets, _securityChanges, new Dictionary <Universe, BaseDataCollection>()).Slice); } }
private IEnumerable <TimeSlice> Stream(AlgorithmNodePacket job, IAlgorithm algorithm, IDataFeed feed, IResultHandler results, CancellationToken cancellationToken) { bool setStartTime = false; var timeZone = algorithm.TimeZone; var history = algorithm.HistoryProvider; // get the required history job from the algorithm DateTime?lastHistoryTimeUtc = null; var historyRequests = algorithm.GetWarmupHistoryRequests().ToList(); // initialize variables for progress computation var start = DateTime.UtcNow.Ticks; var nextStatusTime = DateTime.UtcNow.AddSeconds(1); var minimumIncrement = algorithm.UniverseManager .Select(x => x.Value.Configuration.Resolution.ToTimeSpan()) .DefaultIfEmpty(Time.OneSecond) .Min(); minimumIncrement = minimumIncrement == TimeSpan.Zero ? Time.OneSecond : minimumIncrement; if (historyRequests.Count != 0) { // rewrite internal feed requests var subscriptions = algorithm.SubscriptionManager.Subscriptions.Where(x => !x.IsInternalFeed).ToList(); var minResolution = subscriptions.Count > 0 ? subscriptions.Min(x => x.Resolution) : Resolution.Second; foreach (var request in historyRequests) { Security security; if (algorithm.Securities.TryGetValue(request.Symbol, out security) && security.SubscriptionDataConfig.IsInternalFeed) { if (request.Resolution < minResolution) { request.Resolution = minResolution; request.FillForwardResolution = request.FillForwardResolution.HasValue ? minResolution : (Resolution?)null; } } } // rewrite all to share the same fill forward resolution if (historyRequests.Any(x => x.FillForwardResolution.HasValue)) { minResolution = historyRequests.Where(x => x.FillForwardResolution.HasValue).Min(x => x.FillForwardResolution.Value); foreach (var request in historyRequests.Where(x => x.FillForwardResolution.HasValue)) { request.FillForwardResolution = minResolution; } } foreach (var request in historyRequests) { start = Math.Min(request.StartTimeUtc.Ticks, start); Log.Trace(string.Format("AlgorithmManager.Stream(): WarmupHistoryRequest: {0}: Start: {1} End: {2} Resolution: {3}", request.Symbol, request.StartTimeUtc, request.EndTimeUtc, request.Resolution)); } // make the history request and build time slices foreach (var slice in history.GetHistory(historyRequests, timeZone)) { TimeSlice timeSlice; try { // we need to recombine this slice into a time slice var paired = new List <KeyValuePair <Security, List <BaseData> > >(); foreach (var symbol in slice.Keys) { var security = algorithm.Securities[symbol]; var data = slice[symbol]; var list = new List <BaseData>(); var ticks = data as List <Tick>; if (ticks != null) { list.AddRange(ticks); } else { list.Add(data); } paired.Add(new KeyValuePair <Security, List <BaseData> >(security, list)); } timeSlice = TimeSlice.Create(slice.Time.ConvertToUtc(timeZone), timeZone, algorithm.Portfolio.CashBook, paired, SecurityChanges.None); } catch (Exception err) { Log.Error(err); algorithm.RunTimeError = err; yield break; } if (timeSlice != null) { if (!setStartTime) { setStartTime = true; _previousTime = timeSlice.Time; algorithm.Debug("Algorithm warming up..."); } if (DateTime.UtcNow > nextStatusTime) { // send some status to the user letting them know we're done history, but still warming up, // catching up to real time data nextStatusTime = DateTime.UtcNow.AddSeconds(1); var percent = (int)(100 * (timeSlice.Time.Ticks - start) / (double)(DateTime.UtcNow.Ticks - start)); results.SendStatusUpdate(job.AlgorithmId, AlgorithmStatus.History, string.Format("Catching up to realtime {0}%...", percent)); } yield return(timeSlice); lastHistoryTimeUtc = timeSlice.Time; } } } // if we're not live or didn't event request warmup, then set us as not warming up if (!algorithm.LiveMode || historyRequests.Count == 0) { algorithm.SetFinishedWarmingUp(); results.SendStatusUpdate(job.AlgorithmId, AlgorithmStatus.Running); if (historyRequests.Count != 0) { algorithm.Debug("Algorithm finished warming up."); Log.Trace("AlgorithmManager.Stream(): Finished warmup"); } } foreach (var timeSlice in feed) { if (!setStartTime) { setStartTime = true; _previousTime = timeSlice.Time; } if (algorithm.LiveMode && algorithm.IsWarmingUp) { // this is hand-over logic, we spin up the data feed first and then request // the history for warmup, so there will be some overlap between the data if (lastHistoryTimeUtc.HasValue) { // make sure there's no historical data, this only matters for the handover var hasHistoricalData = false; foreach (var data in timeSlice.Slice.Ticks.Values.SelectMany(x => x).Concat <BaseData>(timeSlice.Slice.Bars.Values)) { // check if any ticks in the list are on or after our last warmup point, if so, skip this data if (data.EndTime.ConvertToUtc(algorithm.Securities[data.Symbol].Exchange.TimeZone) >= lastHistoryTimeUtc) { hasHistoricalData = true; break; } } if (hasHistoricalData) { continue; } // prevent us from doing these checks every loop lastHistoryTimeUtc = null; } // in live mode wait to mark us as finished warming up when // the data feed has caught up to now within the min increment if (timeSlice.Time > DateTime.UtcNow.Subtract(minimumIncrement)) { algorithm.SetFinishedWarmingUp(); results.SendStatusUpdate(job.AlgorithmId, AlgorithmStatus.Running); algorithm.Debug("Algorithm finished warming up."); Log.Trace("AlgorithmManager.Stream(): Finished warmup"); } else if (DateTime.UtcNow > nextStatusTime) { // send some status to the user letting them know we're done history, but still warming up, // catching up to real time data nextStatusTime = DateTime.UtcNow.AddSeconds(1); var percent = (int)(100 * (timeSlice.Time.Ticks - start) / (double)(DateTime.UtcNow.Ticks - start)); results.SendStatusUpdate(job.AlgorithmId, AlgorithmStatus.History, string.Format("Catching up to realtime {0}%...", percent)); } } yield return(timeSlice); } }
/// <summary> /// Enumerates the subscriptions into slices /// </summary> private IEnumerable <Slice> CreateSliceEnumerableFromSubscriptions(List <Subscription> subscriptions, DateTimeZone sliceTimeZone) { // required by TimeSlice.Create, but we don't need it's behavior var cashBook = new CashBook(); cashBook.Clear(); var frontier = DateTime.MinValue; while (true) { var earlyBirdTicks = long.MaxValue; var data = new List <DataFeedPacket>(); foreach (var subscription in subscriptions) { if (subscription.EndOfStream) { continue; } var packet = new DataFeedPacket(subscription.Security, subscription.Configuration); var offsetProvider = subscription.OffsetProvider; var currentOffsetTicks = offsetProvider.GetOffsetTicks(frontier); while (subscription.Current.EndTime.Ticks - currentOffsetTicks <= frontier.Ticks) { // we want bars rounded using their subscription times, we make a clone // so we don't interfere with the enumerator's internal logic var clone = subscription.Current.Clone(subscription.Current.IsFillForward); clone.Time = clone.Time.RoundDown(subscription.Configuration.Increment); packet.Add(clone); Interlocked.Increment(ref _dataPointCount); if (!subscription.MoveNext()) { break; } } // only add if we have data if (packet.Count != 0) { data.Add(packet); } // udate our early bird ticks (next frontier time) if (subscription.Current != null) { // take the earliest between the next piece of data or the next tz discontinuity var nextDataOrDiscontinuity = Math.Min(subscription.Current.EndTime.Ticks - currentOffsetTicks, offsetProvider.GetNextDiscontinuity()); earlyBirdTicks = Math.Min(earlyBirdTicks, nextDataOrDiscontinuity); } } // end of subscriptions if (earlyBirdTicks == long.MaxValue) { break; } if (data.Count != 0) { // reuse the slice construction code from TimeSlice.Create yield return(TimeSlice.Create(frontier, sliceTimeZone, cashBook, data, SecurityChanges.None).Slice); } frontier = new DateTime(Math.Max(earlyBirdTicks, frontier.Ticks), DateTimeKind.Utc); } // make sure we clean up after ourselves foreach (var subscription in subscriptions) { subscription.Dispose(); } }
/// <summary> /// Enumerates the subscriptions into slices /// </summary> protected IEnumerable <Slice> CreateSliceEnumerableFromSubscriptions(List <Subscription> subscriptions, DateTimeZone sliceTimeZone) { // required by TimeSlice.Create, but we don't need it's behavior var cashBook = new CashBook(); cashBook.Clear(); var frontier = DateTime.MinValue; while (true) { var earlyBirdTicks = long.MaxValue; var data = new List <DataFeedPacket>(); foreach (var subscription in subscriptions) { if (subscription.EndOfStream) { continue; } var packet = new DataFeedPacket(subscription.Security, subscription.Configuration); while (subscription.Current.EmitTimeUtc <= frontier) { packet.Add(subscription.Current.Data); Interlocked.Increment(ref _dataPointCount); if (!subscription.MoveNext()) { break; } } // only add if we have data if (packet.Count != 0) { data.Add(packet); } // udate our early bird ticks (next frontier time) if (subscription.Current != null) { // take the earliest between the next piece of data or the next tz discontinuity earlyBirdTicks = Math.Min(earlyBirdTicks, subscription.Current.EmitTimeUtc.Ticks); } } // end of subscriptions if (earlyBirdTicks == long.MaxValue) { break; } if (data.Count != 0) { // reuse the slice construction code from TimeSlice.Create yield return(TimeSlice.Create(frontier, sliceTimeZone, cashBook, data, SecurityChanges.None, new Dictionary <Universe, BaseDataCollection>()).Slice); } frontier = new DateTime(Math.Max(earlyBirdTicks, frontier.Ticks), DateTimeKind.Utc); } // make sure we clean up after ourselves foreach (var subscription in subscriptions) { subscription.Dispose(); } }