//----------------------------------------------------------------------------------------
        public static Frame<DateTime, String> GenerateRollingBondForwardSeries(
            List<string> tickers,
            DateTime start,
            DateTime end,
            string tenor,
            string countryCode,
            string measure,
            string holidayCode,
            bool bOIS = true,
            int optExDivDays = 0,
            bool bSpreadOrRepo = true,
            List<double> reposOrSpreads = null, 
            ICarbonClient client = null)
        //----------------------------------------------------------------------------------------
        {

            // Checks
            if (client == null) 
            {
                throw new ArgumentException("No Carbon Client!");
            }
            
            
            #region cache
            string all_tickers = ";";
            foreach (var s in tickers) all_tickers += s;
            string allRepoOis = "";
            if (reposOrSpreads != null)
                foreach (var s in reposOrSpreads) allRepoOis+= s;

            string cacheKey = "GenerateRollingBondForwardSeries" + tickers.ToString() + start.ToString() + end.ToString() +
                              tenor + countryCode + measure + holidayCode + bOIS.ToString() 
                              + optExDivDays.ToString() + bSpreadOrRepo.ToString() + allRepoOis;

            if (TimeSeriesCache.ContainsKey(cacheKey))
            {
                return TimeSeriesCache[cacheKey];
            }
            #endregion
            
            #region Checks
            if (start >end) throw new ArgumentException("#Error: start date cannot be after end date");
            #endregion

            var settings = new List<Tuple<string/*moniker*/, string/*tickercol*/, string/*pricecol*/>>();

            // Get Configs
            foreach (var ticker in tickers)
            {
                if (ticker.Contains('.')) // it's a CTD series!
                {
                    settings.Add(MonikerConfigs.getCTDMoniker(countryCode,ticker,client));
                }
                else if (ticker.ToLower().Contains("ct"))
                {
                    settings.Add(MonikerConfigs.getCTMoniker(countryCode, ticker, client));
                }
                else
                {
                    throw new ArgumentException("#Error: Invalid ticker found - " + ticker);
                }
            }

            #region DATA
            bool bHaveEverything    = true;
            var configs             = new SpreadTimeSeriesConfigs(countryCode, !bOIS);
            var spreadConventions   = new SpreadConventions(configs);
            var dateRange           = TimeSeriesUtilities.GetAllDatesInDateime(start, end, configs.calendarcode, client, "1b"); // Force daily sampling
            var fwdDates            = dateRange.Select(dte => dte.AddTenor(Tenor.FromString(tenor))).ToList();
            var holidays            = TimeSeriesUtilities.GetHolidays(holidayCode,client);


            // Parcel input repo rates or spreads
            Series<DateTime, double> inputSrs = null;
            if (reposOrSpreads != null )
                if (reposOrSpreads.Count != dateRange.Count && reposOrSpreads.Count != 1)
                {
                    throw new ArgumentException("#Error: number of spreads or rates do not much the days in range");
                }
                else
                {
                    var sb = new SeriesBuilder<DateTime, double>();
                    double scale_factor = bSpreadOrRepo ? 10000 : 1;
                    for (int i = 0; i < dateRange.Count; ++i)
                    {
                        if (reposOrSpreads.Count == 1)
                        {
                            sb.Add(dateRange[i], reposOrSpreads[0]/scale_factor);
                        }
                        else
                        {
                            sb.Add(dateRange[i], reposOrSpreads[i]/scale_factor);
                        }
                    }
                    inputSrs = sb.Series;
                }


            // Swap Curves
            var curves = TimeSeriesUtilities.GetCurvesFromCarbon(dateRange, configs.ccy, client);
            if (curves == null) bHaveEverything = false;


            // Prices and tickers from ad-hoc persistence
            var frames = new Dictionary<string, Frame<DateTime, string>>();
            
            for (int i = 0; i < tickers.Count; ++i)
            {
                var setting = settings[i];
                var ticker  = tickers[i];
                var moniker = setting.Item1;
                var tickercol = setting.Item2;
                var pricecol = setting.Item3;

                var tmp_df = client.GetFrameAsync<DateTime>(moniker, "AdHocData").Result;
                if (tmp_df == null)
                {
                    bHaveEverything = false;
                }
                else
                {
                    // Extract price and ticker cols only
                    var colsOfInterest = new Dictionary<string, Series<DateTime,object>>();

                    // Filter data to contain date range
                    tmp_df = tmp_df.Where(kvp => (kvp.Key >= start) && (kvp.Key <= end));

                    colsOfInterest["price"] = tmp_df.GetColumn<object>(pricecol);
                    colsOfInterest["isin"] = tmp_df.GetColumn<object>(tickercol);

                    frames[ticker] = Frame.FromColumns(colsOfInterest);
                }
            }

            //Bond static
            var bondIsinSet = new HashSet<string>();
            foreach(var kvp in frames)
            {
                var ticker  = kvp.Key;
                var data    = kvp.Value;

                var isins = data.GetColumn<string>("isin").Values;

                //isins.Select(isin => bondIsinSet.Add(isin));
                foreach (string isin in isins)
                {
                    bondIsinSet.Add(isin);
                }
            }
   
            var bondStatics = client.BulkGetBondStaticData( bondIsinSet.ToList());
            if (curves == null || bondStatics == null ) return null;
            #endregion

            #region RepoRegion
            Series<DateTime, double> repoSrs = null;
            if (reposOrSpreads == null)
            {
                // Load default repo-OIS Spread
                double[,] repoRates =
                    client.GetRepoRateFromSavedRepoOisSpread(BondAnalytics.CountryMappings[countryCode], dateRange,
                        fwdDates, holidays);

                // Break out if no data
                if (repoRates == null) return null;

                var repoSrsBlder = new SeriesBuilder<DateTime, double>();
                for (int i = 0; i < dateRange.Count; ++i)
                {
                    repoSrsBlder.Add(dateRange[i], repoRates[i, 1]);
                }
                repoSrs = repoSrsBlder.Series;
            }

            else
            {
                // User-defined repo-OIS Spread or repo rates
                if (bSpreadOrRepo)
                {
                    //Inputs are spreads
                    double[,] repoRates =
                    client.CalcRepoRatesFromOisSpread(inputSrs, BondAnalytics.CountryMappings[countryCode], dateRange, fwdDates, holidays);

                    // Break out if no data
                    if (repoRates == null) return null;

                    var repoSrsBlder = new SeriesBuilder<DateTime, double>();
                    for (int i = 0; i < dateRange.Count; ++i)
                    {
                        repoSrsBlder.Add(dateRange[i], repoRates[i, 1]);
                    }
                    repoSrs = repoSrsBlder.Series;
                }
                else
                {
                    //Inputs are repo rates
                    repoSrs = inputSrs;
                }
            }

            #endregion

            // Calculation loop
            var forecastCurves = curves.Item1;
            var discountCurves = curves.Item2;

            // Overwrite forecast curve if we want OIS
            if (bOIS)
            {
                forecastCurves = discountCurves;
            }

            var outputs = new Dictionary<string, Series<DateTime, double>> ();

            foreach (string ticker in tickers)
            {
                var sb = new SeriesBuilder<DateTime, double>();
                var df = frames[ticker];

                var idx = df.RowKeys;

                var isins   = df.GetColumn<string>("isin");
                var prices  = df.GetColumn<double>("price");
                
                foreach (var dte in idx)
                {
                    var tryisin = isins.TryGet(dte);
                    var tryprice = prices.TryGet(dte);
                    var tryrepo = repoSrs.TryGet(dte);

                    if (!tryprice.HasValue || !tryisin.HasValue || !tryrepo.HasValue)
                        continue;

                    var isin    = tryisin.Value;
                    var price   = tryprice.Value;
                    var repo    = tryrepo.Value;
                    var fwddate = dte.AddTenor(Tenor.FromString(tenor));
                    var bondstatic  = bondStatics[isin];
                    double value = double.NaN;

                    if (measure == "Repo")
                    {
                        value = repo;
                    }
                    
                    else
                    {
                        var fwdprice = BondAnalytics.CalcBondFwd(configs.country, dte, price, bondstatic.EffectiveDate,
                            bondstatic.FirstCouponDate, bondstatic.MaturityDate, bondstatic.Coupon, bondstatic.CpnFreq,
                            fwddate, repo, optExDivDays)[0];

                        if (measure == "Price")
                        {
                            value = fwdprice;
                        }
                        else
                        {
                            value = MeasureCalculator.calcFwdBondMeasure(measure, bondstatic, fwdprice, dte, fwddate,
                                spreadConventions, forecastCurves, discountCurves, holidays);
                        }
                    }

                    sb.Add(dte,value);
                }

                outputs[ticker] = sb.Series;
            }

            var consolidatedMeasures = Frame.FromColumns(outputs);

            // Cache results
            TimeSeriesCache[cacheKey] = consolidatedMeasures;

            return consolidatedMeasures;
        }