基于WPF的量化客户端开发教程(001)——环境、配置文件、日志记录和基础框架搭建
Wesky.Quant.Client主程序,目前包含几个简易结构,Domain文件夹下面用来存放策略、通用服务或者业务等内容,Infrastructure文件夹打算用来存放工具箱、底层基础服务等,目前已有一个读取Json配置文件的帮助类;gmsdk是掘金平台的SDK,本项目使用的WPF,采用prism框架来实现MVVM,所以需要安装两个prism的包。包括项目后续使用的引用的nuget包版本等,强
免责声明:1、本系列教程仅用于个人开发技术学习使用,请勿用于商用、投资等除学习以外的任何领域。否则造成的纠纷或者损失,本人不承担任何责任。2、本系列教程不保证任何功能的正确性、完整性和有效性,请谨慎使用。3、本系列教程不保证任何功能的持续性和可用性,作者保留随时修改、中断、终止本系列教程的权利,且不承担任何责任。4、本系列教程中所涉及的任何第三方资料、链接等均不代表本人的观点和立场,本人不承担任何责任。
开源项目地址:
gitee:https://gitee.com/dreamer_j/WeskyQuantClient
github:https://github.com/LittleLittleRobot/WeskyQuantClient/tree/master
代码更新详解:
本文属于第一篇文章,所以内容是从创建开始,新增了一些基础内容。
目前整体项目结构内容如下:

系统开发环境说明:
开发工具:VS2022社区版
开发环境.NET版本:.NET 8
备注:【强烈建议使用同款开发工具和SDK版本进行开发或者打开本项目,防止出现不兼容情况。包括项目后续使用的引用的nuget包版本等,强烈建议使用与教程同款进行搭建,否则可能出现兼容性问题,或者与本教程演示的效果不相符的情况】
操作系统:使用的是Windows 11家庭版。系统版本无特殊要求,win10以上都行。
个人系统简要配置如下:无特殊要求,能跑就行。内存建议32G或以上均可。

基础框架开发详细介绍:
Wesky.Quant.Client是主程序,Wesky.Quant.Client.ExtensionLibrary是拓展程序,主要用于安装外部nuget包。

Wesky.Quant.Client.ExtensionLibrary拓展程序目前安装以上这些nuget包,如果喜欢自己动手实践的,建议采用同版本的包。
gmsdk是掘金平台的SDK,本项目使用的WPF,采用prism框架来实现MVVM,所以需要安装两个prism的包。Serilog用来写入本地日志使用。最下面的OpenTools是我几年前开发的一个简单工具包,里面有EMA计算公式,所以直接引用下来备用。其他几个是系统包,用于操作Json文件、字符编码等使用。

Wesky.Quant.Client主程序,目前包含几个简易结构,Domain文件夹下面用来存放策略、通用服务或者业务等内容,Infrastructure文件夹打算用来存放工具箱、底层基础服务等,目前已有一个读取Json配置文件的帮助类;QuantExtensionServices文件夹会用来存放拓展服务类,一般是启动程序时候进行加载的拓展服务类,例如日志配置服务等;ViewModels和Views是Prism框架的一个开发规范,默认客户端页面(View)和VM业务层(ViewModel)分别存放这两个文件夹下面,prism框架会自动识别。App.xmal是启动项,上面可以进行一些配置,类似webapi程序的startup启动项。
具体展开说明: App.xmal.cs里面,预设了全局异常捕获事件、客户端多开限制、服务注册入口等。具体可参考以下代码:
public partial class App : PrismApplication { public App() { Startup += AppStartup; Exit += AppExit; } private void AppExit(object sender, ExitEventArgs e) { // } private void AppStartup(object sender, StartupEventArgs e) { //UI线程未捕获异常处理事件 DispatcherUnhandledException += App_DispatcherUnhandledException; ; //Task线程内未捕获异常处理事件 TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; ; //非UI线程未捕获异常处理事件 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; ; } private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { try { string msg; if (e.ExceptionObject is Exception ex) { msg = ExceptionToString(ex, "非UI线程"); } else { msg = $"发生了一个错误!信息:{e.ExceptionObject}"; } Log.Error(msg); } catch (Exception ex) { string msg = ExceptionToString(ex, "非UI线程 处理函数"); Log.Error(msg); } } private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { try { string msg = ExceptionToString(e.Exception, "Task线程"); Log.Error(msg); e.SetObserved(); //设置该异常已察觉(这样处理后就不会引起程序崩溃) } catch (Exception ex) { string msg = ExceptionToString(ex, "Task线程 处理函数"); Log.Error(msg); } } private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { try { e.Handled = true; string msg = ExceptionToString(e.Exception, "UI线程"); Log.Error(msg); } catch (Exception ex) { string msg = ExceptionToString(ex, "UI线程 处理函数"); Log.Error(msg); } } /// <summary> /// 提取异常信息 /// </summary> private static string ExceptionToString(Exception ex, string info) { StringBuilder str = new StringBuilder($"{DateTime.Now}, {info}发生了一个错误!{Environment.NewLine}"); if (ex.InnerException == null) { str.Append($"【对象名称】:{ex.Source}{Environment.NewLine}"); str.Append($"【异常类型】:{ex.GetType().Name}{Environment.NewLine}"); str.Append($"【详细信息】:{ex.Message}{Environment.NewLine}"); str.Append($"【堆栈调用】:{ex.StackTrace}"); } else { str.Append($"【对象名称】:{ex.InnerException.Source}{Environment.NewLine}"); str.Append($"【异常类型】:{ex.InnerException.GetType().Name}{Environment.NewLine}"); str.Append($"【详细信息】:{ex.InnerException.Message}{Environment.NewLine}"); str.Append($"【堆栈调用】:{ex.InnerException.StackTrace}"); } return str.ToString(); } static volatile int currentMainThreadID = Thread.CurrentThread.ManagedThreadId; //这个属性表示当前执行线程是否在主线程中运行 public static bool IsRunInMainThread { get { return Thread.CurrentThread.ManagedThreadId == currentMainThreadID; } } protected override Window CreateShell() { return Container.Resolve<QuantMainView>(); } private static Mutex instance; protected override void InitializeShell(Window shell) { instance = new Mutex(true, "WeskyQuantClient", out bool createdNew); // 防止同一台电脑上多开 if (createdNew) { instance.ReleaseMutex(); base.InitializeShell(shell); } else { MessageBox.Show("客户端已经启动,请勿重复启动"); System.Windows.Application.Current?.Shutdown(); } } protected override void RegisterTypes(IContainerRegistry containerRegistry) { /// 全局对象注册 containerRegistry.Register<Dispatcher>(() => System.Windows.Application.Current.Dispatcher); // 注册弹出窗 containerRegistry.RegisterDialogWindow<DialogWindow>(); // 注册系统服务 var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new ReadJsonHelper()); serviceCollection.AddSerilogConfiguration(); // 初始化日志配置信息(写入本地文件) serviceCollection.AddHttpClient(); var provide = serviceCollection.BuildServiceProvider(); QuantServiceProviderStatic.QuantServiceProvider = provide; // 注册编码 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } }
日志扩展服务,用于提供通用日志模板,例如每天都新建一个日志文件,并且最多存储200天的日志,超出部分会被自动删除:
public static class SerilogExtensionService{ public static void AddSerilogConfiguration(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } // 初始化本地日志文件写入格式 Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .WriteTo.Async(a => a.File(System.Environment.CurrentDirectory + $"\\Logs\\.log", rollingInterval: RollingInterval.Hour, outputTemplate: "{NewLine}DateTime:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}LogLevel:{Level}{NewLine}Message:{Message}{NewLine}{Exception}", retainedFileCountLimit: 200)) .CreateLogger(); }}
Json文件读取帮助服务,用于提供一个简易版Json数据读取功能,包括key-value读取方式、key-泛型对象读取方式两种:
public class ReadJsonHelper { private static IConfiguration _config; public ReadJsonHelper() { string contentPath = $"{AppDomain.CurrentDomain.SetupInformation.ApplicationBase}"; _config = new ConfigurationBuilder() .SetBasePath(contentPath) .Add(new JsonConfigurationSource { Path = "weskyquant.json", Optional = false, ReloadOnChange = true })//直接读目录里的json文件 .Build(); } /// <summary> /// 读取指定节点的字符串 /// </summary> /// <param name="sessions"></param> /// <returns></returns> public static string Read(params string[] sessions) { try { if (sessions.Any()) { return _config[string.Join(":", sessions)]; } } catch { return ""; } return ""; } public static object ReadObject(string sessions) { try { if (sessions.Any()) { return _config[sessions]; } } catch { return ""; } return ""; } /// <summary> /// 读取实体信息 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="session"></param> /// <returns></returns> public static List<T> Read<T>(params string[] session) { List<T> list = new List<T>(); _config.Bind(string.Join(":", session), list); return list; } public static string GetGenerateId() { long i = 1; foreach (byte b in Guid.NewGuid().ToByteArray()) { i *= ((int)b + 1); } return string.Format("{0:x}", i - DateTime.Now.Ticks); } }
由于一些系统服务,或者基于IServiceCollection进行拓展的服务,使用prism不方便注册,所以提供一个自定义的全局容器,用于方便获取系统服务或者拓展服务的实例:
public class QuantServiceProviderStatic { public static IServiceProvider QuantServiceProvider { get; set; } }
如果以上一些都准备完整,运行程序,会打开一个空白的客户端页面。第一篇序章完成~

如果需要该项目代码的答疑,可上星球进行提问~

如果需要源码,请根据文章开头提示自行fork或者下载,我使用的Apache-2.0开源协议许可,大家感兴趣可以自行学习使用。如果对这方面开发技术有疑问(纯新手不建议),可以扫上面的星球加入我的星球,提供技术答疑服务。
也欢迎关注博主公众号:Dotnet Dancer ,后续新增文章也会公开到该公众号上进行同步。该系列文章仅用于学习交流使用,大家请勿玩飘了就行~
更多推荐


所有评论(0)