【问题标题】:How to join in sliding windows如何加入滑动窗口
【发布时间】:2018-10-21 19:03:03
【问题描述】:

我有一系列股票代码和一系列股票价格。每次我得到一个股票代码(它保证是唯一的)时,我都需要跨越一个 100 毫秒的窗口并从股票价格序列中处理股票的价格。如果这 100 毫秒内的序列中缺少股票价格,我需要处理没有价格的股票。

大理石图更好地可视化需求:

Stock : -S1--S2--

Price : ---P1-P2-

Result: -S1---S2P2-

所以股票1 进来了,100 毫秒的窗口被跨越,但是没有价格的符号,因此结果应该只是股票1 (S1)。

然后股票2 进来,再次跨越100 毫秒的窗口,还没有股票2 的价格。然而,在 100 毫秒窗口关闭之前,我们得到股票 2 (P2) 的价格,因此结果是股票 2 及其价格 (S2P2)。

价格有可能是随机出现的,所以不能对价格顺序做出假设。

我已经看到了这个related SO question,但不能让它工作。 我正在尝试使用GroupJoin

stockSubject
.GroupJoin(
    stockPriceSubject,
    stock => Observable.Timer(TimeSpan.FromMilliseconds(100)),
    price => Observable.Never<Unit>(),
    (stock, stockPrice) =>
    {            
        var stockPrices = stockPrice.Where(sprice => sprice.Stock.Equals(stock))
                                    .FirstOrDefaultAsync()
                                    .DefaultIfEmpty();
        return (Stock: stock, StockPrices: stockPrices);
    })
.Subscribe(async tuple => WriteLine($"{tuple.Stock} | {(await tuple.StockPrices)?.Price ?? 'N'}"));

这不起作用,因为它会降低一些价格(发生不确定性,因此无法真正找出问题所在)。

我在工作时尝试过的另一种方法看起来并不理想

stockSubject
    .Subscribe(stock =>
    {
        stockPriceSubject
            .Buffer(TimeSpan.FromMilliseconds(100))
            .Take(1)
            .Subscribe(bufferedPrices =>
            {
                var stockPrice = bufferedPrices.FirstOrDefault(price => price.Stock.Equals(stock));
                if (stockPrice == null)
                {
                    Console.WriteLine($"{stock} is w/o price");
                    return;
                }

                Console.WriteLine($"{stockPrice}");
            });
    });

真正不喜欢这个的一件事是,每次有新股票订阅缓冲价格时,我都会放弃订阅。

知道使用 Rx 实现此场景的最佳方法是什么吗?

股票和股价的相关类

sealed class Stock : IEquatable<Stock>
{
    public Stock(string symbol)
    {
        Symbol = symbol;
    }

    public string Symbol { get; }

    public override string ToString() =>
        $"Stock[{Symbol}]";

    // IEquatable implementation is emitted for the sake of brevity
}

sealed class StockPrice
{
    public StockPrice(Stock stock, decimal price)
    {
        Stock = stock;
        Price = price;
    }

    public Stock Stock { get; }
    public decimal Price { get; }

    public override string ToString() =>
        $"{Stock} is traded @ {Price}";
}

按要求编辑添加测试数据代码生成器

每 10 毫秒将新股票推送到股票序列(MSFT -> GOOG -> APPL)。

每 20 毫秒将新价格推送到价格序列 (APPL -> GOOG)。

1 秒后,股票 MSFT 的价格被推送到价格序列。

预期输出:

一旦 MSFT 被推送到股票序列,100 毫秒的价格窗口打开......在 100 毫秒内,MSFT 没有价格被推送到价格序列,因此MSFT 股票应不带价格处理(在结果集中价格为空/null)

GOOG被推送到股票序列后,再次打开100毫秒窗口,这一次GOOG股票在100毫秒内有一个价格,因此 GOOG库存应按价格处理(1500万)。

最后是 APPL - 这里的预期输出与 MSFT 相同...因为在 100 毫秒内没有为 APPL 推送价格窗口,因为它被推送到股票序列,它应该无价处理。在此,之前发布了 APPL 股票价格这一事实不应影响任何事情。

var stockSubject = new Subject<Stock>();
var stockPriceSubject = new Subject<StockPrice>();

Observable
   .Interval(TimeSpan.FromMilliseconds(10))
   .Take(3)
   .Subscribe(_ =>
   {
       switch (_)
       {
           case 0:
               {
                   var stock = new Stock("MSFT");
                   stockSubject.OnNext(stock);
                   break;
               }
           case 1:
               {
                   var stock = new Stock("GOOG");
                   stockSubject.OnNext(stock);
                   break;
               }
           case 2:
               {
                   var stock = new Stock("APPL");
                   stockSubject.OnNext(stock);
                   break;
               }
       }
   });

Observable
    .Interval(TimeSpan.FromMilliseconds(20))
    .Take(3)
    .Subscribe(_ =>
    {
        switch (_)
        {
            case 0:
                {
                    var stockPrice = new StockPrice(new Stock("APPL"), 10m);
                    stockPriceSubject.OnNext(stockPrice);
                    break;
                }
            case 1:
                {
                    var stockPrice = new StockPrice(new Stock("GOOG"), 15m);
                    stockPriceSubject.OnNext(stockPrice);
                    break;
                }
        }
    });

Observable
    .Timer(TimeSpan.FromSeconds(1))
    .Subscribe(_ =>
    {
        var stockPrice = new StockPrice(new Stock("MSFT"), 20m);
        stockPriceSubject.OnNext(stockPrice);
    });

【问题讨论】:

    标签: c# system.reactive


    【解决方案1】:

    如果没有一些测试代码,就不可能测试您的答案。我也不确定你想对下游的数据做什么。如果此答案不充分,请使用该信息修改问题。

    我相信你所问的解决方案很简单:

    stocks
        .Select(s => (Stock: s, StockPrices: prices
            .TakeUntil(Observable.Timer(TimeSpan.FromMilliseconds(100)))
            .Where(p => p.Stock == s)
        ));
    

    这将导致针对prices 的多个订阅问题,因此可以通过以下方式解决:

    prices.Publish(_prices => 
        stocks
            .Select(s => (Stock: s, StockPrices: _prices
                .TakeUntil(Observable.Timer(TimeSpan.FromMilliseconds(100)))
                .Where(p => p.Stock == s)
            ))
        );
    

    JoinGroupJoin 在您获得 0 股票价格的情况下将无法正常工作。对于您的情况,我不会推荐它。但是,如果您返回它,您应该将Observable.Never 更改为Observable.EmptyNever 让价格窗口永远打开,因此旧价格可以与新股票结合。


    编辑

    这里有一些使用Microsoft.Reactive.Testing的测试代码:

    TestScheduler ts = new TestScheduler();
    var stockSource = ts.CreateHotObservable<Stock>(
        new Recorded<Notification<Stock>>(10.MsTicks(), Notification.CreateOnNext(new Stock("MSFT"))),
        new Recorded<Notification<Stock>>(20.MsTicks(), Notification.CreateOnNext(new Stock("GOOG"))),
        new Recorded<Notification<Stock>>(30.MsTicks(), Notification.CreateOnNext(new Stock("AAPL")))
    );
    
    var priceSource = ts.CreateHotObservable<StockPrice>(
        new Recorded<Notification<StockPrice>>(20.MsTicks(), Notification.CreateOnNext(new StockPrice(new Stock("AAPL"), 10m))),
        new Recorded<Notification<StockPrice>>(40.MsTicks(), Notification.CreateOnNext(new StockPrice(new Stock("GOOG"), 15m)))
    );
    
    
    var target = priceSource.Publish(_prices =>
        stockSource
            .Select(s => (Stock: s, StockPrices: _prices
                .TakeUntil(Observable.Timer(TimeSpan.FromMilliseconds(100), ts))
                .Where(p => p.Stock.Symbol == s.Symbol)
            ))
        );
    var observer = ts.CreateObserver<(Stock, IObservable<StockPrice>)>();
    target.Subscribe(observer);
    
    var target2 = target.SelectMany(t => t.StockPrices.Select(sp => (Stock: t.Stock, Price: sp)));
    var observer2 = ts.CreateObserver<(Stock, StockPrice)>();
    target2.Subscribe(observer2);
    ts.Start();
    
    observer.Messages.Dump();   //LinqPad
    observer2.Messages.Dump();  //LinqPad
    

    并使用扩展方法:

    public static class Extensions
    {
        public static long MsTicks(this int i)
        {
            return TimeSpan.FromMilliseconds(i).Ticks;
        }
    }
    

    对我来说,这是可行的。唯一的问题是 IEquatable 缺乏实施。所以我从.Where(p =&gt; p.Stock == s) 切换到.Where(p =&gt; p.Stock.Symbol == s.Symbol)。这可能是你的问题吗?

    【讨论】:

    • 感谢您对此进行调查。添加了请求的测试数据生成器代码,并解释了预期的行为。试过你的代码,StockPrices 总是空的。
    • 更新了答案。告诉我。
    • 还是不行。在priceSource 中再添加一条记录:new Recorded&lt;Notification&lt;StockPrice&gt;&gt;(80.MsTicks(), Notification.CreateOnNext(new StockPrice(new Stock("APPL"), 25m)))。现在结果还应该打印APPL股票,价格25,因为APPL股票被推@@30毫秒并跨越100毫秒窗口,提交的价格@@80毫秒应该已经被拾取。
    • 更改为“AAPL”。 :)
    • 哎呀,草率抱歉...现在可以使用了,感谢您的帮助!
    猜你喜欢
    • 2014-07-09
    • 2015-02-05
    • 2012-03-05
    • 2012-08-08
    • 2012-10-20
    • 1970-01-01
    • 1970-01-01
    • 2020-04-02
    • 2021-06-22
    相关资源
    最近更新 更多