A platform for trading crypto currencies and running crypto currency strategies including TradeView, which is a WPF UI built on the Origin framework, and TradeServer, a ASP.NET Core web host.
TradeView and TradeServer enables you to:
- Connect to your exchage accounts, trade currencies, manage open orders and your asset balances
- Subscribe to live trade feed and order book for assets
- Create and run your own custom trading strategies
- Monitor running trading strategies
- Send real-time updates to strategy parameters for running strategies
- Easily extend the number of supported exchanges by creating wrappers for exchange api's that implement a common interface (currently supports Binance and Kucoin exchanges)
- Getting Started
- TradeView WPF UI
- TradeServer AspNetCore WebHost
- Strategies
- Extending TradeView
TIP:
See thread #88 for some additional advice about getting the demo strategy to run.
Download the source code. Open the solution file TradeView.sln, go to multiple start-up projects and select the following projects to start:
- DevelopmentInProgress.TradeView.Wpf.Host
- DevelopmentInProgress.TradeServer.Console
Compile and run the solution. This will start both the TradeView UI and the TradeServer WebHost, which runs as a console app.
Select the Trading module in the navigational panel then click Demo Account. This will open the trading tab for the Demo Account and connect to its exchange, Binance, and subscribe to order book and trade feed for the default asset.
To trade you must create a new account. You can do this in the Configuration module by selecting Manage Accounts. Specify the exchange and provide an api key and api secret (and optionally a api pass phrase e.g. for Kucoin accounts). Also sepcify the default asset. Once the account has been created you can access it in the Trading module. You will see the account balances and real-time trade feed and order book for the default asset. You can start trading and receive real-time updates for your open orders.
Select the Strategies module in the navigation panel and click the Binance Moving Average - ETHBTC demo strategy. This will open the strategy tab. From the drop down list in the top left corned select the server you want to run the strategy on. The default is Trade Server, which runs as a console app. Click on the Run Strategy button. This will upload the strategy to the Trade Server instance and start running it. In the Trade Server console window you will see logged in the console the request has been received to run the strategy. A webSocket connection is established between the TradeView and TradeServer and the TradeServer publishes notifications to the TradeView so the strategy can be monitored in real time.
Note: the demo strategy only shows the real time trades with moving average and buy / sell indicators above and below the moving average.
TIP:
See thread #88 for some additional advice about getting the demo strategy to run.
See Strategies for more details of how running a strategy works.
Strategy parameters contain information the startegy uses to determine when to buy and sell assets. You can update the parameters of a running strategy in real-time. For example, in the UI the strategy parameters is displayed in JSON format and can be edited. Bump the sell indicator of the demo strategy from 0.00015 to 0.00115 and then click the push button. The updated strategy parameters is pushed to the strategy running on the server. You will notice in the trade chart the sell indicator line is adjusted accordingly.
See Updating Strategy Parameters for more details of how updating strategy parameters works.
Creating a strategy involves creating a class library with a class that inherits TradeStrategyBase which implements ITradeStrategy.
You will also need to create a config entry for the strategy in the Configuration module by selecting Manage Strategies.
The best way to understand how to create a strategy is to look at the demo strategy MovingAverageStrategy and how it has been configured.
Creating a WPF strategy component involves creating a class library with a view model that inherits StrategyDisplayViewModelBase and a corresponding view.
The best way to understand how to create a WPF strategy component is to look at the project DevelopmentInProgress.Strategy.MovingAverage.Wpf for an example of how the demo WPF strategy component has been created.
You will also need to update the config entry for the strategy in the Configuration module by selecting Manage Strategies.
TradeView consists of modules, accessible from the navigation panel on the left, including Configuration, Trading, Strategies and the Dashboard-module.
The Configuration module is where configuration for trading accounts, running strategies and strategy servers is managed.
Allows you to create and persist trading accounts including account name, exchange, api key and secret key. It also persists display preferences for the trading screen, such as favourite symbols, and default selected symbol.
Manage configuration for running a trading strategy and displaying a running strategy in realtime.
Manage trade server details for servers that run trading strategies
The Trading module shows a list of trading accounts in the navigation panel. Selecting an account will open a trading document in the main window for that account. From the trading document you can:
- see the account's balances
- view realtime pricing of favourite symbols
- select a symbol to subscribe to live orderbook and trade feed
- place buy and sell orders
- monitor and manage open orders in realtime
Strategies are run on an instance of TradeServer and can be monitored by one or more users. It is possible to update a running strategy's parameters in realtime e.g. buy and sell triggers or suspend trading. See Running a Strategy.
You can view all accounts and open orders in realtime.
You can view all configured TradeServer's and whether they are active. An active TradeServer will show the strategies currently running on it, including each strategy's parameters and its active connections i.e. which users are monitoring the strategy. See Running a Strategy.
The console app takes three parameters:
- s = server name
- u = url of the webhost
- p = MaxDegreeOfParallelism for the dataflow StrategyRunnerActionBlock execution options
It creates and runs an instance of a WebHost, passing the parameters into it.
dotnet DevelopmentInProgress.TradeServer.Console.dll --s=ServerName --u=http://+:5500 --p=5
var webHost = WebHost.CreateDefaultBuilder()
.UseUrls(url)
.UseStrategyRunnerStartup(args)
.UseSerilog()
.Build();
The WebHost's UseStrategyRunnerStartup extension method passes in the command line args to the WebHost and specifies the Startup class to use.
public static class WebHostExtensions
{
public static IWebHostBuilder UseStrategyRunnerStartup(this IWebHostBuilder webHost, string[] args)
{
return webHost.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddCommandLine(args);
}).UseStartup<Startup>();
}
}
The Startup configures the request processing pipeline to branch the request path to the appropriate middleware and configures the services to be consumed via dependency injection.
public void Configure(IApplicationBuilder app)
{
app.UseDipSocket<NotificationHub>("/notificationhub");
app.Map("/runstrategy", HandleRun);
app.Map("/updatestrategy", HandleUpdate);
app.Map("/stopstrategy", HandleStop);
app.Map("/isstrategyrunning", HandleIsStrategyRunning);
app.Map("/ping", HandlePing);
}
public void ConfigureServices(IServiceCollection services)
{
var server = new Server();
server.Started = DateTime.Now;
server.StartedBy = Environment.UserName;
server.Name = Configuration["s"].ToString();
server.Url = Configuration["u"].ToString();
if(Convert.ToInt32(Configuration["p"]) > 0)
{
server.MaxDegreeOfParallelism = Convert.ToInt32(Configuration["p"]);
}
services.AddSingleton<IServer>(server);
services.AddSingleton<IStrategyRunnerActionBlock, StrategyRunnerActionBlock>();
services.AddSingleton<IStrategyRunner, StrategyRunner>();
services.AddSingleton<INotificationPublisherContext, NotificationPublisherContext>();
services.AddSingleton<INotificationPublisher, NotificationPublisher>();
services.AddSingleton<IBatchNotificationFactory<StrategyNotification>, StrategyBatchNotificationFactory>();
services.AddSingleton<IExchangeApiFactory, ExchangeApiFactory>();
services.AddSingleton<IExchangeService, ExchangeService>();
services.AddSingleton<IExchangeSubscriptionsCacheFactory, ExchangeSubscriptionsCacheFactory>();
services.AddSingleton<ISubscriptionsCacheManager, SubscriptionsCacheManager>();
services.AddSingleton<ITradeStrategyCacheManager, TradeStrategyCacheManager>();
services.AddHostedService<StrategyRunnerBackgroundService>();
services.AddDipSocket<NotificationHub>();
}
The following table shows the middleware each request path is mapped to.
Request Path | Maps to Middleware | Description |
---|---|---|
http://localhost:5500/runstrategy |
RunStrategyMiddleware | Request to run a strategy |
http://localhost:5500/stopstrategy |
StopStrategyMiddleware | Stop a running strategy |
http://localhost:5500/updatestrategy |
UpdateStrategyMiddleware | Update a running strategy's parameters |
http://localhost:5500/isstrategyrunning |
IsStrategyRunningMiddleware | Check if a strategy is running |
http://localhost:5500/ping |
PingMiddleware | Check if the trade server is running |
http://localhost:5500/notificationhub |
SocketMiddleware | A websocket connection request |
The Startup class adds a long running hosted service StrategyRunnerBackgroundService which inherits the BackgroundService. It is a long running background task for running trade strategies that have been posted to the trade servers runstrategy request pipeline. It contains a reference to the singleton StrategyRunnerActionBlock which has an ActionBlock dataflow that invokes an ActionBlock delegate for each request to run a trade strategy.
strategyRunnerActionBlock.ActionBlock = new ActionBlock<StrategyRunnerActionBlockInput>(async actionBlockInput =>
{
await actionBlockInput.StrategyRunner.RunAsync(actionBlockInput.Strategy,
actionBlockInput.DownloadsPath,
actionBlockInput.CancellationToken)
.ConfigureAwait(false);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = server.MaxDegreeOfParallelism });
The application uses Socket, based on DipSocket, a lightweight publisher / subscriber implementation using WebSockets, for sending and receiving notifications to and from clients and servers. The StrategyNotificationHub inherits the abstract class SocketServer to manage client connections and channels. A client e.g. a running instance of TradeView, establishes a connection to the server with the purpose of running or monitoring a strategy on it. The strategy registers a Socket channel to which multiple client connections can subscribe. The strategy broadcasts notifications (e.g. live trade feed, buy and sell orders etc.) to the client connections. The StrategyNotificationHub overrides the OnClientConnectAsync and ReceiveAsync methods.
public async override Task OnClientConnectAsync(WebSocket websocket, string clientId, string strategyName)
{
var connection = await base.AddWebSocketAsync(websocket).ConfigureAwait(false);
SubscribeToChannel(strategyName, websocket);
var connectionInfo = connection.GetConnectionInfo();
var json = JsonConvert.SerializeObject(connectionInfo);
var message = new Message { MethodName = "OnConnected", SenderConnectionId = "StrategyRunner", Data = json };
await SendMessageAsync(websocket, message).ConfigureAwait(false);
}
public async override Task ReceiveAsync(WebSocket webSocket, Message message)
{
switch (message.MessageType)
{
case MessageType.UnsubscribeFromChannel:
UnsubscribeFromChannel(message.Data, webSocket);
break;
}
}
Batch notifiers inherit abstract class BatchNotification which uses a BlockingCollection for adding notifications to a queue while sending them on on a background thread.
public enum BatchNotificationType
{
StrategyRunnerLogger,
StrategyAccountInfoPublisher,
StrategyCustomNotificationPublisher,
StrategyNotificationPublisher,
StrategyOrderBookPublisher,
StrategyTradePublisher,
StrategyStatisticsPublisher,
StrategyCandlesticksPublisher
}
The StrategyBatchNotificationFactory creates instances of batch notifiers. Batch notifiers use the StrategyNotificationPublisher to publish notifications (trade, order book, account notifications etc) to client connections.
The clients loads the serialised Strategy and strategy assemblies into a MultipartFormDataContent and post a request to the server.
var client = new HttpClient();
var multipartFormDataContent = new MultipartFormDataContent();
var jsonContent = JsonConvert.SerializeObject(strategy);
multipartFormDataContent.Add(new StringContent(jsonContent, Encoding.UTF8, "application/json"), "strategy");
foreach (var file in files)
{
var fileInfo = new FileInfo(file);
var fileStream = File.OpenRead(file);
using (var br = new BinaryReader(fileStream))
{
var byteArrayContent = new ByteArrayContent(br.ReadBytes((int)fileStream.Length));
multipartFormDataContent.Add(byteArrayContent, fileInfo.Name, fileInfo.FullName);
}
}
Task<HttpResponseMessage> response = await client.PostAsync("http://localhost:5500/runstrategy", multipartFormDataContent);
The RunStrategyMiddleware processes the request on the server. It deserialises the strategy and downloads the strategy assemblies into a sub directory under the working directory of the application. The running of the strategy is then passed to the StrategyRunnerActionBlock.
var json = context.Request.Form["strategy"];
var strategy = JsonConvert.DeserializeObject<Strategy>(json);
var downloadsPath = Path.Combine(Directory.GetCurrentDirectory(), "downloads", Guid.NewGuid().ToString());
if (context.Request.HasFormContentType)
{
var form = context.Request.Form;
var downloads = from f
in form.Files
select Download(f, downloadsPath);
await Task.WhenAll(downloads.ToArray());
}
var strategyRunnerActionBlockInput = new StrategyRunnerActionBlockInput
{
StrategyRunner = strategyRunner,
Strategy = strategy,
DownloadsPath = downloadsPath
};
await strategyRunnerActionBlock.RunStrategyAsync(strategyRunnerActionBlockInput).ConfigureAwait(false);
The StrategyRunnerActionBlock, hosted in the StrategyRunnerBackgroundService, has an ActionBlock dataflow that invokes an ActionBlock delegate for each request to run a trade strategy by calling the StrategyRunner.RunAsync method, passing in the strategy and location of the strategy assemblies to run.
strategyRunnerActionBlock.ActionBlock = new ActionBlock<StrategyRunnerActionBlockInput>(async actionBlockInput =>
{
await actionBlockInput.StrategyRunner.RunAsync(actionBlockInput.Strategy,
actionBlockInput.DownloadsPath,
actionBlockInput.CancellationToken)
.ConfigureAwait(false);
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = server.MaxDegreeOfParallelism });
A strategy must implement ITradeStrategy. The StrategyRunner will load the strategy's assembly and all its dependencies into memory and create an instance of the strategy. It will subscribe to strategy events to handle notifications from the strategy to the client connection i.e. the TradeView UI. The strategy is added to TradeStrategyCacheManager and the SubscriptionsCacheManager subscribes to the strategy's feeds i.e. trade, orderbook, account updates etc. Finally, the ITradeStrategy.RunAsync method is called to run the strategy.
internal async Task<Strategy> RunStrategyAsync(Strategy strategy, string localPath)
{
ITradeStrategy tradeStrategy = null;
try
{
var dependencies = GetAssemblies(localPath);
var assemblyLoader = new AssemblyLoader(localPath, dependencies);
var assembly = assemblyLoader.LoadFromMemoryStream(Path.Combine(localPath, strategy.TargetAssembly));
var type = assembly.GetType(strategy.TargetType);
dynamic obj = Activator.CreateInstance(type);
tradeStrategy = (ITradeStrategy)obj;
tradeStrategy.StrategyNotificationEvent += StrategyNotificationEvent;
tradeStrategy.StrategyAccountInfoEvent += StrategyAccountInfoEvent;
tradeStrategy.StrategyOrderBookEvent += StrategyOrderBookEvent;
tradeStrategy.StrategyTradeEvent += StrategyTradeEvent;
tradeStrategy.StrategyStatisticsEvent += StrategyStatisticsEvent;
tradeStrategy.StrategyCandlesticksEvent += StrategyCandlesticksEvent;
tradeStrategy.StrategyCustomNotificationEvent += StrategyCustomNotificationEvent;
strategy.Status = StrategyStatus.Running;
if(tradeStrategyCacheManager.TryAddTradeStrategy(strategy.Name, tradeStrategy))
{
await subscriptionsCacheManager.Subscribe(strategy, tradeStrategy).ConfigureAwait(false);
var result = await tradeStrategy.RunAsync(strategy, cancellationToken).ConfigureAwait(false);
if(!tradeStrategyCacheManager.TryRemoveTradeStrategy(strategy.Name, out ITradeStrategy ts))
{
Notify(NotificationLevel.Error, $"Failed to remove {strategy.Name} from the cache manager.");
}
}
else
{
Notify(NotificationLevel.Error, $"Failed to add {strategy.Name} to the cache manager.");
}
}
finally
{
if(tradeStrategy != null)
{
subscriptionsCacheManager.Unsubscribe(strategy, tradeStrategy);
tradeStrategy.StrategyNotificationEvent -= StrategyNotificationEvent;
tradeStrategy.StrategyAccountInfoEvent -= StrategyAccountInfoEvent;
tradeStrategy.StrategyOrderBookEvent -= StrategyOrderBookEvent;
tradeStrategy.StrategyTradeEvent -= StrategyTradeEvent;
tradeStrategy.StrategyCustomNotificationEvent -= StrategyCustomNotificationEvent;
}
}
return strategy;
}
The TradeStrategyCacheManager caches running instances of strategies and is used to check if a strategy is running, update a running strategy's parameters, and stopping a strategy.
Strategies can subscribe to feeds for one or more symbols across multiple exhanges.
[Flags]
public enum Subscribe
{
None = 0,
AccountInfo = 1,
Trades = 2,
OrderBook = 4,
Candlesticks = 8
}
The SubscriptionsCacheManager manages symbols subscriptions across all exchanges using the ExchangeSubscriptionsCacheFactory which provides an instance of the ExchangeSubscriptionsCache for each exchange.
The IExchangeSubscriptionsCache uses a dictionary for caching symbol subscriptions for the exchange it was created.
public interface IExchangeSubscriptionsCache : IDisposable
{
bool HasSubscriptions { get; }
ConcurrentDictionary<string, ISubscriptionCache> Caches { get; }
Task Subscribe(string strategyName, List<StrategySubscription> strategySubscription, ITradeStrategy tradeStrategy);
void Unsubscribe(string strategyName, List<StrategySubscription> strategySubscription, ITradeStrategy tradeStrategy);
}
The application uses Socket, based on DipSocket, a lightweight publisher / subscriber implementation using WebSockets, for sending and receiving notifications to and from clients and servers.
The SocketClient's StartAsync
method opens WebSocket connection with the SocketServer. The On
method registers an Action to be invoked when receiving a message from the server.
socketClient = new DipSocketClient($"{Strategy.StrategyServerUrl}/notificationhub", strategyAssemblyManager.Id);
socketClient.On("Connected", message =>
{
ViewModelContext.UiDispatcher.Invoke(() =>
{
NotificationsAdd(message);
});
});
socketClient.On("Notification", async (message) =>
{
await ViewModelContext.UiDispatcher.Invoke(async () =>
{
await OnStrategyNotificationAsync(message);
});
});
socketClient.On("Trade", (message) =>
{
ViewModelContext.UiDispatcher.Invoke(async () =>
{
await OnTradeNotificationAsync(message);
});
});
socketClient.On("OrderBook", (message) =>
{
ViewModelContext.UiDispatcher.Invoke(async () =>
{
await OnOrderBookNotificationAsync(message);
});
});
socketClient.On("AccountInfo", (message) =>
{
ViewModelContext.UiDispatcher.Invoke(() =>
{
OnAccountNotification(message);
});
});
socketClient.On("Candlesticks", (message) =>
{
ViewModelContext.UiDispatcher.Invoke(async () =>
{
await OnCandlesticksNotificationAsync(message);
});
});
socketClient.Closed += async (sender, args) =>
{
await ViewModelContext.UiDispatcher.Invoke(async () =>
{
NotificationsAdd(message);
await socketClient.DisposeAsync();
});
};
socketClient.Error += async (sender, args) =>
{
await ViewModelContext.UiDispatcher.Invoke(async () =>
{
NotificationsAdd(message);
await socketClient.DisposeAsync()
});
};
await socketClient.StartAsync(strategy.Name);
The SocketMiddleware processes the request on the server. The StrategyNotificationHub manages client connections.
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var clientId = context.Request.Query["clientId"];
var data = context.Request.Query["data"];
await dipSocketServer.OnClientConnectAsync(webSocket, clientId, data);
await Receive(webSocket);
private async Task Receive(WebSocket webSocket)
{
try
{
var buffer = new byte[1024 * 4];
var messageBuilder = new StringBuilder();
while (webSocket.State.Equals(WebSocketState.Open))
{
WebSocketReceiveResult webSocketReceiveResult;
messageBuilder.Clear();
do
{
webSocketReceiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (webSocketReceiveResult.MessageType.Equals(WebSocketMessageType.Close))
{
await dipSocketServer.OnClientDisonnectAsync(webSocket);
continue;
}
if (webSocketReceiveResult.MessageType.Equals(WebSocketMessageType.Text))
{
messageBuilder.Append(Encoding.UTF8.GetString(buffer, 0, webSocketReceiveResult.Count));
continue;
}
}
while (!webSocketReceiveResult.EndOfMessage);
if (messageBuilder.Length > 0)
{
var json = messageBuilder.ToString();
var message = JsonConvert.DeserializeObject<Message>(json);
await dipSocketServer.ReceiveAsync(webSocket, message);
}
}
}
finally
{
webSocket?.Dispose();
}
}
The StrategyRunnerClient posts a message to the strategy server to update a running strategy's parameters.
var strategyParametersJson = JsonConvert.SerializeObject(strategyParameters, Formatting.Indented);
var strategyRunnerClient = new StrategyRunnerClient();
var response = await strategyRunnerClient.PostAsync($"{Strategy.StrategyServerUrl}/updatestrategy", strategyParametersJson);
The UpdateStrategyMiddleware processes the request on the server.
var json = context.Request.Form["strategyparameters"];
var strategyParameters = JsonConvert.DeserializeObject<StrategyParameters>(json);
if(tradeStrategyCacheManager.TryGetTradeStrategy(strategyParameters.StrategyName, out ITradeStrategy tradeStrategy))
{
await tradeStrategy.TryUpdateStrategyAsync(json);
}
The StrategyRunnerClient posts a message to the strategy server to stop a running strategy.
var strategyParametersJson = JsonConvert.SerializeObject(strategyParameters, Formatting.Indented);
var strategyRunnerClient = new Strategy.StrategyRunnerClient();
var response = await strategyRunnerClient.PostAsync($"{Strategy.StrategyServerUrl}/stopstrategy", strategyParametersJson);
The StopStrategyMiddleware processes the request on the server.
var json = context.Request.Form["strategyparameters"];
var strategyParameters = JsonConvert.DeserializeObject<StrategyParameters>(json);
if (tradeStrategyCacheManager.TryGetTradeStrategy(strategyParameters.StrategyName, out ITradeStrategy tradeStrategy))
{
await tradeStrategy.TryStopStrategy(json);
}
TradeView is intended to trade against multiple exchanges and the following api's are currently supported:
To add a new api create a new .NET Standard project for the API wrapper and create a class that implements IExchangeApi. For example see BinanceExchangeApi.
namespace DevelopmentInProgress.TradeView.Api.Binance
{
public class BinanceExchangeApi : IExchangeApi
{
public async Task<Order> PlaceOrder(User user, ClientOrder clientOrder)
{
var binanceApi = new BinanceApi();
using (var apiUser = new BinanceApiUser(user.ApiKey, user.ApiSecret))
{
var order = OrderHelper.GetOrder(apiUser, clientOrder);
var result = await binanceApi.PlaceAsync(order).ConfigureAwait(false);
return NewOrder(user, result);
}
}
public async Task<string> CancelOrderAsync(User user, string symbol, string orderId, string newClientOrderId = null, long recWindow = 0, CancellationToken cancellationToken = default(CancellationToken))
{
var binanceApi = new BinanceApi();
var id = Convert.ToInt64(orderId);
using (var apiUser = new BinanceApiUser(user.ApiKey, user.ApiSecret))
{
var result = await binanceApi.CancelOrderAsync(apiUser, symbol, id, newClientOrderId, recWindow, cancellationToken).ConfigureAwait(false);
return result;
}
}
Next, add the exchange to the Exchange enum.
public enum Exchange
{
Unknown,
Binance,
Kucoin,
Test
}
Finally, return an instance of the new exchange from the ExchangeApiFactory.
public class ExchangeApiFactory : IExchangeApiFactory
{
public IExchangeApi GetExchangeApi(Exchange exchange)
{
switch(exchange)
{
case Exchange.Binance:
return new BinanceExchangeApi();
case Exchange.Kucoin:
return new KucoinExchangeApi();
default:
throw new NotImplementedException();
}
}
public Dictionary<Exchange, IExchangeApi> GetExchanges()
{
var exchanges = new Dictionary<Exchange, IExchangeApi>();
exchanges.Add(Exchange.Binance, GetExchangeApi(Exchange.Binance));
exchanges.Add(Exchange.Kucoin, GetExchangeApi(Exchange.Kucoin));
return exchanges;
}
}
Data can be persisted to any data source by creating a library with classes that implement the interfaces in DevelopmentInProgress.TradeView.Data.
And register the classes in the TradeView's App.xaml.cs RegisterTypes method.
containerRegistry.Register<ITradeViewConfigurationAccounts, TradeViewConfigurationAccountsFile>();
containerRegistry.Register<ITradeViewConfigurationStrategy, TradeViewConfigurationStrategyFile>();
containerRegistry.Register<ITradeViewConfigurationServer, TradeViewConfigurationServerFile>();