-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathProgram.cs
More file actions
148 lines (127 loc) · 6.23 KB
/
Copy pathProgram.cs
File metadata and controls
148 lines (127 loc) · 6.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright (c) 2023 Philippe Matray. All rights reserved.
// This file is part of TaLibStandard.
// TaLibStandard is licensed under the GNU General Public License v3.0.
// See the LICENSE file in the project root for the full license text.
// For more information, visit https://github.com/phmatray/TaLibStandard.
using System.Net.WebSockets;
using TechnicalAnalysis.Samples.RealTime.Configuration;
using TechnicalAnalysis.Samples.RealTime.Contracts;
using TechnicalAnalysis.Samples.RealTime.Hubs;
using TechnicalAnalysis.Samples.RealTime.Indicators;
using TechnicalAnalysis.Samples.RealTime.Streaming;
using TechnicalAnalysis.Samples.RealTime.WebSockets;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// ---------------------------------------------------------------------------
// Options. Everything the sample can be tuned with lives in the RealTime
// section and can be overridden on the command line, for example:
// dotnet run -- --RealTime:BarSeconds=1 --RealTime:TickIntervalMilliseconds=100
// ---------------------------------------------------------------------------
builder.Services
.AddOptions<RealTimeOptions>()
.Bind(builder.Configuration.GetSection(RealTimeOptions.SectionName))
.Validate(RealTimeOptionsValidation.IsValid, RealTimeOptionsValidation.Message)
.ValidateOnStart();
// ---------------------------------------------------------------------------
// Serialization. The source generated context is wired into all three exits:
// minimal API responses, the SignalR JSON protocol, and the raw socket handler
// (which references RealTimeJsonContext.Default directly).
// ---------------------------------------------------------------------------
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(0, RealTimeJsonContext.Default));
builder.Services
.AddSignalR()
.AddJsonProtocol(options =>
options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, RealTimeJsonContext.Default));
// ---------------------------------------------------------------------------
// The pipeline. The feed is registered twice on purpose: once as the singleton
// everything subscribes to, once as the hosted service that drives it.
// ---------------------------------------------------------------------------
builder.Services.AddSingleton<SyntheticMarketDataFeed>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<SyntheticMarketDataFeed>());
builder.Services.AddSingleton<SnapshotBroadcaster>();
builder.Services.AddSingleton<IndicatorWebSocketHandler>();
builder.Services.AddSingleton<IndicatorPipeline>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<IndicatorPipeline>());
WebApplication app = builder.Build();
app.UseWebSockets();
app.UseDefaultFiles();
app.UseStaticFiles();
// SignalR transport: push via per-symbol groups, plus a server-to-client streaming method.
app.MapHub<IndicatorHub>("/hubs/indicators");
// Raw WebSocket transport, for clients that would rather not take a SignalR dependency.
app.Map("/ws/indicators", async (HttpContext context, IndicatorWebSocketHandler handler) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync(
"This endpoint speaks WebSocket. Open / in a browser for the live dashboard, "
+ "or connect a client to ws://<host>/ws/indicators?symbol=<symbol>.");
return;
}
string? symbol = context.Request.Query["symbol"];
using WebSocket socket = await context.WebSockets.AcceptWebSocketAsync();
await handler.HandleAsync(socket, symbol, context.RequestAborted);
});
DateTimeOffset startedAt = DateTimeOffset.UtcNow;
app.MapGet("/health", (SyntheticMarketDataFeed feed, SnapshotBroadcaster broadcaster, IndicatorPipeline pipeline)
=> new HealthResponse(
"healthy",
DateTimeOffset.UtcNow,
Math.Round((DateTimeOffset.UtcNow - startedAt).TotalSeconds, 1),
feed.Symbols,
feed.PublishedTicks,
pipeline.BarsClosed,
feed.SubscriberCount,
broadcaster.ChannelSubscriberCount,
broadcaster.GroupSubscriberCount,
pipeline.PushesDropped));
app.MapGet("/api/symbols", (SnapshotBroadcaster broadcaster) => broadcaster.Symbols);
await app.RunAsync();
/// <summary>
/// Startup validation for <see cref="RealTimeOptions"/>. Catching a bad period here turns a per-bar
/// <c>RetCode.BadParam</c> — which would silently null an indicator forever — into a loud failure at boot.
/// </summary>
internal static class RealTimeOptionsValidation
{
/// <summary>The message shown when validation fails.</summary>
public const string Message =
"RealTime options are invalid: need at least one symbol, a positive tick interval, bar period and "
+ "queue capacity, indicator periods of at least 2, and a window size larger than the slowest "
+ "indicator's lookback.";
/// <summary>
/// Checks that the bound options can actually drive the pipeline.
/// </summary>
/// <param name="options">The bound options.</param>
/// <returns><see langword="true"/> when the options are usable.</returns>
public static bool IsValid(RealTimeOptions options)
{
if (options.Symbols.Length == 0 || options.Symbols.Any(string.IsNullOrWhiteSpace))
{
return false;
}
if (options.TickIntervalMilliseconds <= 0 || options.BarSeconds <= 0 || options.SubscriberQueueCapacity <= 0)
{
return false;
}
IndicatorOptions indicators = options.Indicators;
int[] periods =
[
indicators.SmaFastPeriod,
indicators.SmaSlowPeriod,
indicators.EmaPeriod,
indicators.RsiPeriod,
indicators.MacdFastPeriod,
indicators.MacdSlowPeriod,
indicators.MacdSignalPeriod,
indicators.BollingerPeriod,
indicators.AtrPeriod
];
// TAMath rejects any period below 2 with RetCode.BadParam.
if (periods.Any(period => period < 2))
{
return false;
}
return options.WindowSize > RollingIndicatorEngine.LargestLookback(indicators);
}
}