using XFEEDToolBox.NativeSupport; using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using XFEEDToolBox.Services; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEEDToolBox.Core.Presentation; using XFEEDToolBox.Core.Session; namespace XFEEDToolBox.ViewModels; public partial class ToolboxViewModel { private CancellationTokenSource? automaticTradeStop; private string? automaticTradeConfigurationPath; private AutomaticTradeConfiguration? automaticTradeParameters; [ObservableProperty] public partial StationLiveRow? AutomaticDestination { get; set; } [ObservableProperty,NotifyPropertyChangedFor(nameof(AutomaticCgVisibility))] public partial int AutomaticModeIndex { get; set; } [ObservableProperty] public partial string AutomaticCommodities { get; set; }=""; [ObservableProperty] public partial bool AutomaticReturnTrade { get; set; }=true; [ObservableProperty,NotifyPropertyChangedFor(nameof(AutomaticUpdateVisibility))] public partial bool AutomaticPluginUpdateRequired { get; set; } public Microsoft.UI.Xaml.Visibility AutomaticUpdateVisibility=>AutomaticPluginUpdateRequired?Microsoft.UI.Xaml.Visibility.Visible:Microsoft.UI.Xaml.Visibility.Collapsed; public Microsoft.UI.Xaml.Visibility AutomaticCgVisibility=>AutomaticModeIndex==1?Microsoft.UI.Xaml.Visibility.Visible:Microsoft.UI.Xaml.Visibility.Collapsed; public string AutomaticShipSummary=>$"当前飞船 {CurrentPlayer.Ship} · 实际货舱 {CurrentPlayer.CargoCapacity:N0}t · 重购费 {CurrentPlayer.Rebuy:N0} Cr;启动时重新核对。"; partial void OnAutomaticDestinationChanged(StationLiveRow? value){NoteManualTradeConfiguration();InvalidateTradeCandidates();NotifyTradePlanningSummary();RefreshTradePlanCommands();} partial void OnAutomaticModeIndexChanged(int value){NoteManualTradeConfiguration();TradePlanningModeChanged(value);RefreshTradePlanCommands();} private string AutomaticRouteFile=>Path.Combine(runtime.DataDirectory,"automatic-trade-route.json"); private void ApplyAutomaticParameters(AutomaticTradeConfiguration value) { var rows=Stations.Select(s=>s.Record).Append(value.Source).Append(value.Destination).DistinctBy(s=>s.MarketId).ToArray(); Stations.Apply(rows);SelectedStart=Stations.First(s=>s.MarketId==value.Source.MarketId); AutomaticDestination=Stations.First(s=>s.MarketId==value.Destination.MarketId); AutomaticModeIndex=(int)value.Mode;AutomaticCommodities=string.Join(", ",value.Commodities);AutomaticReturnTrade=value.ReturnTrade; automaticTradeParameters=value; } private async Task InitializeAutomaticTradeAsync() { if(!File.Exists(AutomaticRouteFile))return; try { var value=await Task.Run(()=>JsonSerializer.Deserialize(File.ReadAllText(AutomaticRouteFile),ToolboxSettings.JsonOptions),lifetime.Token); if(value is not null){value.Validate();ApplyAutomaticParameters(value);} } catch(Exception error){runtime.Log("跑商路线",error.Message);} } private AutomaticTradeConfiguration SelectedAutomaticParameters(string[] names) { if(SelectedStart is null||AutomaticDestination is null)throw new InvalidOperationException("请选择起点和目的站点。"); if(!investmentFollowsBalance&&(!double.IsFinite(TradeInvestment)||TradeInvestment<=0))throw new InvalidOperationException("请填写大于零的投入资金。"); if(names.Any(n=>!NativeTradeBatchPolicy.AllowsFormal(n)))throw new InvalidOperationException("商品名称未被识别,请使用行情列表中的名称。"); if(AutomaticModeIndex==1&&names.Length==0)throw new InvalidOperationException("请根据CG活动说明填写接收的商品。"); var value=new AutomaticTradeConfiguration{Source=SelectedStart.Record,Destination=AutomaticDestination.Record, WorkingDirectory=automaticTradeParameters?.WorkingDirectory??Path.Combine(runtime.DataDirectory,"automatic-trading","active"), CargoCapacity=0,ReserveCredits=Math.Max(runtime.Player.Rebuy,Math.Max(0,(decimal)Reserve)), InvestmentCredits=investmentFollowsBalance?null:(decimal)TradeInvestment,Mode=(AutomaticTradeMode)AutomaticModeIndex,Commodities=names, CommunityGoalId=automaticTradeParameters?.Destination.MarketId==AutomaticDestination.MarketId?automaticTradeParameters.CommunityGoalId:null, ReturnTrade=AutomaticReturnTrade,EmptyValidationVisits=0,ValidateMenuReentry=false, SourcePlanning=BuildRequest(),SourceCenterFollowsPlayer=TradeRegionCenterText.Trim().Length==0}; value.Validate();return value; } private async Task RunSelectedAutomaticTradeAsync() { var text=AutomaticCommodities; var names=await Task.Run(()=>text.Split([',',',',';',';'],StringSplitOptions.TrimEntries|StringSplitOptions.RemoveEmptyEntries) .Select(CommodityCatalog.SelectionKey).Distinct(StringComparer.Ordinal).ToArray(),lifetime.Token); var value=SelectedAutomaticParameters(names); Directory.CreateDirectory(Path.GetDirectoryName(AutomaticRouteFile)!); await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token); await RunAutomaticParametersAsync(value,AutomaticRouteFile); } [RelayCommand(CanExecute=nameof(CanExecuteTradePlan))] private async Task UpdateAndStartAutomaticTradeAsync() { var text=AutomaticCommodities; var names=await Task.Run(()=>text.Split([',',',',';',';'],StringSplitOptions.TrimEntries|StringSplitOptions.RemoveEmptyEntries) .Select(CommodityCatalog.SelectionKey).Distinct(StringComparer.Ordinal).ToArray(),lifetime.Token); var value=SelectedAutomaticParameters(names);var succeeded=false; if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商或恢复流程。"); using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;HasError=false; try { var executable=PluginGameExecutable;var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,executable,NativeSupportPackage.Directory,Path.Combine(value.WorkingDirectory,"operations")); bootstrap.Progress+=message=>dispatcher.TryEnqueue(()=>{RunStatus="更新并恢复";RunMessage=message;runtime.Log("跑商更新",message);}); await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(stop.Token),stop.Token);succeeded=true;AutomaticPluginUpdateRequired=false; } catch(OperationCanceledException)when(stop.IsCancellationRequested){RunStatus="已取消";RunMessage="更新或恢复已取消,已完成步骤保留。";} catch(Exception error){RunStatus="需要处理";RunMessage=error.Message;ShowError(error.Message);} finally{automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();} if(succeeded) { await ReadPluginStatusAsync(); await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token); await RunAutomaticParametersAsync(value,AutomaticRouteFile); } } internal async Task UpdateAndRunAutomaticTradeAsync(string configurationPath) { var value=await Task.Run(()=>JsonSerializer.Deserialize(File.ReadAllText(configurationPath),ToolboxSettings.JsonOptions),lifetime.Token) ??throw new InvalidDataException("跑商参数为空。"); value.Validate();ApplyAutomaticParameters(value);await UpdateAndStartAutomaticTradeAsync(); } internal async Task UpgradeNativeSupportOnlyAsync(string configurationPath,string reportPath) { if(RunActive||automaticTradeStop is not null||runtime.FlightAutomation.IsActive||NavigationActive||SystemFlightActive)throw new InvalidOperationException("请先结束当前驾驶流程再更新。"); var value=JsonSerializer.Deserialize(await File.ReadAllTextAsync(configurationPath,lifetime.Token),ToolboxSettings.JsonOptions) ??throw new InvalidDataException("维护参数为空。");value.Validate(); var before=runtime.Player;RunActive=true; try { var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(value.WorkingDirectory,"operations")); bootstrap.Progress+=message=>dispatcher.TryEnqueue(()=>{RunStatus="更新底层支持";RunMessage=message;runtime.Log("底层维护",message);}); await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(lifetime.Token),lifetime.Token); var after=runtime.Player; if(before.Docked&&(!after.Docked||before.MarketId!=after.MarketId))throw new InvalidDataException("更新后的停靠地点未与原行程一致,未继续任何驾驶。"); await ReadPluginStatusAsync();RunStatus="已更新";RunMessage="底层支持已更新,保留当前行程;未启动跑商。"; await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{passed=true,at=DateTimeOffset.UtcNow,automaticTradeStarted=false, before=new{before.CommanderId,before.CurrentShipId,before.MarketId,before.GameMode,before.CargoCount}, after=new{after.CommanderId,after.CurrentShipId,after.MarketId,after.GameMode,after.CargoCount}},ToolboxSettings.JsonOptions),lifetime.Token); } catch(Exception error) { await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{passed=false,at=DateTimeOffset.UtcNow,automaticTradeStarted=false,error=error.Message},ToolboxSettings.JsonOptions));throw; } finally{RunActive=false;RefreshTradePlanCommands();} } internal async Task RunAutomaticTradeAsync(string configurationPath,Action? beforeStart=null) { var value=await Task.Run(()=>JsonSerializer.Deserialize(File.ReadAllText(configurationPath),ToolboxSettings.JsonOptions),lifetime.Token) ??throw new InvalidDataException("跑商参数为空。"); value.Validate();ApplyAutomaticParameters(value); await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token); // Configuration preparation can yield after an upstream authorization // check. Reconfirm immediately before the new run owns any control; // its long-lived token still belongs to this viewmodel, not the caller. lifetime.Token.ThrowIfCancellationRequested();beforeStart?.Invoke(); await RunAutomaticParametersAsync(value,configurationPath); } private async Task RunAutomaticParametersAsync(AutomaticTradeConfiguration parameters,string path) { if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商流程。"); automaticTradeConfigurationPath=Path.GetFullPath(path);automaticTradeParameters=parameters; AutomaticSupplySummary=""; using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;HasError=false; NativeTradeCommandDriver? driver=null; AutomaticTradeRunSession? financialSession=null; int? financialRoundsBaseline=null; DateTimeOffset lastProtectionRecovery=default; var sceneRecoveries=0; async Task EnterWithConnectionSupervisionAsync(WindowsTradingBootstrap bootstrap) { var expected=runtime.Player; await StartupConnectionRecovery.SuperviseAsync(cancellation=>Task.Run(()=>bootstrap.UpgradeAndEnterAsync(cancellation),cancellation), async(failure,count,cancellation)=> { var current=runtime.Player; if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId ||current.CurrentShipId is {} ship&&ship!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode) throw new InvalidDataException("等待连接恢复期间指挥官、飞船或原模式改变,已停止自动跑商。"); var delay=TimeSpan.FromMinutes(Math.Min(10,count*2)); RunStatus="等待连接恢复";RunMessage=$"原版连接失败已完成本轮有界恢复,将在 {delay.TotalMinutes:N0} 分钟后核对并续接原行程;原订单、回执和累计收益保留,可随时取消。";HasError=false; runtime.Log("连接恢复等待",$"第 {count} 轮:{failure.Message}"); await Task.Delay(delay,cancellation); current=runtime.Player; if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId ||current.CurrentShipId is {} nextShip&&nextShip!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode) throw new InvalidDataException("等待连接恢复期间身份改变,已保留客户端和原订单。"); },stop.Token); } bool ProtectionRecoveryPending() { // Navigation may reject the new Status before the health poll's // next tick. Consume that same fresh witness before handling failure. shipProtection?.ObserveInterdiction(runtime.Player,DateTimeOffset.UtcNow); return shipProtection?.LastTrigger is{} trigger&&(trigger.Reason is ShipProtectionReason.Interdicted or ShipProtectionReason.ShieldsLost or ShipProtectionReason.HullBelowHalf) &&trigger.At>lastProtectionRecovery; } try { while(!stop.IsCancellationRequested) { try { var existingSession=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(parameters.WorkingDirectory,"operations")){ResumeExistingCargo=true}; existingSession.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("跑商会话恢复",text);}); await EnterWithConnectionSupervisionAsync(existingSession); await ReadPluginStatusAsync(); while(!runtime.Player.InGame||runtime.Player.CurrentShipId is null||runtime.Player.CargoCapacity<=0||runtime.Player.Rebuy<=0||!runtime.Player.CargoReliable||!runtime.Player.CreditsReliable) { RunStatus="等待游戏就绪";RunMessage="正在等待游戏载入及当前飞船、货舱和余额就绪,完成后自动继续。";HasError=false; await Task.Delay(1000,stop.Token); } var current=runtime.Player; var reserve=Math.Max(parameters.ReserveCredits,current.Rebuy); RunStatus="正在启动";RunMessage="正在按当前飞船核对接口、货舱和保留资金。"; PlanProfit="正在读取实时货单";PlanHourly="货单就绪后估算";PlanDuration="正在估算航程";ProfitLabel="本次跑商已确认净利润"; driver=await CreateNativeTradeDriverAsync(new(),stop.Token,new(current.CargoCapacity,reserve),Path.Combine(parameters.WorkingDirectory,"operations")); var flight=(NativeFlightControlDriver)runtime.FlightDriver!;var pid=flight.ProcessId; async Task Recover(CancellationToken token) { await WindowsFlightRecovery.RecoverAsync(pid,NativeRelogChannel.SupportedGameSha256,()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory, ReleaseProtectionForControlledExitAsync,text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("跑商会话恢复",text);}),token); var refreshed=await new WindowsGameSessionDriver(()=>runtime.Player,()=>PluginGameExecutable).FindTargetAsync(token); if(refreshed.ProcessId!=pid)throw new SavingSceneRecoveryRequiredException(new IOException("已保护重启客户端,原订单等待在新实例接续。")); } async Task ValidateFlight(CancellationToken token) { var capability=await new NativeCruiseControlChannel(pid,NativeRelogChannel.SupportedGameSha256).PrepareAsync(Path.Combine(NativeSupportPackage.Directory,"CruiseControl"),token); if(parameters.RequireSupercruiseOvercharge&&!capability.Available) throw new NotSupportedException("当前飞船的原生SCO温度、燃油和启停接口尚未就绪,未采购或起飞。"); } var engine=new AutomaticTradeEngine(parameters,driver,new NativeCommerceChannel(pid),()=>runtime.Player, token=>EnsureNativeFlightProtectionAsync(pid,NativeRelogChannel.SupportedGameSha256,token),runtime.Settings.JournalDirectory,Recover, (station,rows,token)=>runtime.Markets.SaveSnapshotAsync(new(station,rows,DateTimeOffset.UtcNow,DataSource.Journal),token),ValidateFlight,SelectAutomaticSupplyAsync,ProtectionRecoveryPending); engine.Progress+=value=> { financialSession??=AutomaticTradeRunSession.Open(parameters.WorkingDirectory,value.NetProfit); if(financialRoundsBaseline is null) { try{financialRoundsBaseline=AutomaticRoundsBaseline(parameters.WorkingDirectory,financialSession,value.CompletedRounds);} catch(Exception error)when(error is IOException or UnauthorizedAccessException or JsonException){financialRoundsBaseline=value.CompletedRounds;runtime.Log("跑商统计",error.Message);} } var sessionProfit=financialSession.Profit(value.NetProfit); dispatcher.TryEnqueue(()=>{ tradeExecutionStartedAt=financialSession.StartedAt;CompletedTradeRounds=Math.Max(0,value.CompletedRounds-financialRoundsBaseline.Value); TradeConfirmedProfit=$"{sessionProfit:N0} Cr";RefreshTradeElapsed(); RunStatus=value.Phase switch{"requires-recovery"=>"需要处理","stopped"=>"已取消","safety-pause"=>"等待安全停靠","recovering-safety"=>"安全维修恢复中",_=>"自动跑商中"}; RunMessage=value.Message;NextDestination=value.NextDestination;ActualProfit=$"{sessionProfit:N0} Cr";ProfitLabel="本次跑商已确认净利润"; if(value.SupplySelection is{} supply)AutomaticSupplySummary=$"CG 货源:{supply.Source.DisplayName} · {supply.Order.Name} {supply.Order.Quantity:N0}/{supply.FreeCapacity:N0} t · 库存 {supply.Supply:N0} t · 预计 {supply.CreditsPerHour:N0} Cr/h · 风险 {supply.Delivery.RiskDisplay}"; ProfitDetails=$"自 {financialSession.StartedAt.ToLocalTime():MM-dd HH:mm} 起累计,自动重连和修复重启延续本次累计。"; if(value.Estimate is{} expected){PlanProfit=$"{expected.NetProfit:N0} Cr";PlanHourly=$"{expected.Hourly:N0} Cr/h";PlanDuration=$"约 {expected.Minutes:F1} 分钟";PlanWarnings=expected.Basis;} runtime.Log("自动跑商",value.Message); }); }; // This task and its leases are owned by the application, not by the caller or agent. await Task.Run(()=>engine.RunAsync(stop.Token),stop.Token); break; } catch(Exception)when(!stop.IsCancellationRequested&&ProtectionRecoveryPending()) { var protection=shipProtection!;lastProtectionRecovery=protection.LastTrigger!.At; if(driver is not null){try{await driver.ReleaseAllAsync();}catch(Exception error)when(error is IOException or InvalidOperationException or OperationCanceledException){}driver=null;} RunStatus="飞船保护恢复中";RunMessage="飞船保护已退出客户端,正在通过官方启动器恢复原模式和原订单;健康保护继续生效。";HasError=false; await protection.Completion.WaitAsync(stop.Token); if(protection.RestartTarget is null)throw new IOException("拦截已保护退出,但尚未确认官方启动器的新游戏实例。"); // The next iteration enters the original mode in the verified // replacement process and resumes its durable cargo checkpoint. } catch(Exception error)when(!stop.IsCancellationRequested&&FlightConnectionRecoveryRequiredException.Find(error) is not null) { var failure=FlightConnectionRecoveryRequiredException.Find(error)!;var binding=failure.Binding; FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token); if(driver is not null){await driver.ReleaseAllAsync();driver=null;} var lifecycle=new WindowsGameSessionDriver(()=>runtime.Player,()=>PluginGameExecutable); var failedClient=await lifecycle.FindLaunchTargetAsync(stop.Token); FlightConnectionRecovery.RequireOriginalClient(binding,failedClient,PluginGameExecutable); if(binding.GameSha256!=NativeRelogChannel.SupportedGameSha256)throw new InvalidDataException("原飞行连接故障的构建身份不匹配。"); FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token); var operations=Path.Combine(parameters.WorkingDirectory,"operations");Directory.CreateDirectory(operations); await File.WriteAllTextAsync(Path.Combine(operations,$"flight-connection-recovery-{binding.ProcessId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.json"), JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,binding,client=failedClient,session=failure.Menu,navigation=failure.Navigation, originalOrdersUnchanged=true,confirmedReceiptsPreserved=true,originalFailure=error.ToString()},ToolboxSettings.JsonOptions),stop.Token); FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token); RunStatus="正在恢复网络连接";RunMessage="原版已持续确认连接失败,正在按原模式有界恢复;先核对原订单,再续接原航程。";HasError=false; var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,operations) {ResumeExistingCargo=true,ExpectedConnectionFailureClient=failedClient,ExpectedFlightConnection=binding}; bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("飞行连接恢复",text);}); await EnterWithConnectionSupervisionAsync(bootstrap); await ReadPluginStatusAsync(); // The next engine instance reconciles durable intent/receipts // before it can issue anything. Keep this run's financial base. } catch(Exception error)when(NativeOrderReconciliationRequiredException.Present(error)&&!stop.IsCancellationRequested) { if(driver is not null){await driver.ReleaseAllAsync();driver=null;} RunStatus="正在核对原订单";RunMessage="原始请求返回服务器错误,正在通过官方会话重新核对货舱和余额。";HasError=false; var recoveryCommander=runtime.Player.CommanderId;var recoveryShip=runtime.Player.CurrentShipId; while(!runtime.Player.InGame||!runtime.Player.Docked||!runtime.Player.CargoReliable) { RunMessage="正在等待当前航程停靠,随后自动恢复官方会话并核对原订单;取消按钮仍可使用。"; await Task.Delay(1000,stop.Token); } if(runtime.Player.CommanderId!=recoveryCommander||runtime.Player.CurrentShipId!=recoveryShip)throw new InvalidDataException("等待订单核对期间指挥官或飞船已改变。"); var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(parameters.WorkingDirectory,"operations")){ReconcileOriginalOrders=true}; bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("交易会话核对",text);}); await EnterWithConnectionSupervisionAsync(bootstrap); await ReadPluginStatusAsync(); } catch(Exception error)when(!stop.IsCancellationRequested&&SavingSceneRecoveryRequiredException.Present(error)&&FlightJourneyFailure.CanRecoverScene(error)&&sceneRecoveries<2) { sceneRecoveries++; if(driver is not null){await driver.ReleaseAllAsync();driver=null;} RunStatus="正在恢复游戏场景";RunMessage="已保留原订单,正在核对原版保存状态;未重复提交交易。";HasError=false; // The next iteration observes the exact original transition for // 90 seconds, then verifies the replacement session and cargo. } } } catch(OperationCanceledException)when(stop.IsCancellationRequested){RunStatus="已取消";RunMessage="已停止新增操作,原始订单与恢复记录保留。";} catch(Exception error){AutomaticPluginUpdateRequired=error is NativeTradeSdkUpgradeRequiredException;RunStatus="需要处理";RunMessage=error.Message;ShowError(error.Message);runtime.Log("自动跑商",error.ToString());} finally { if(driver is not null)try{await driver.ReleaseAllAsync();}catch(Exception error){ShowError(error.Message);} automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();CalculateCommand.NotifyCanExecuteChanged(); } } }