1+ using System . Collections . Concurrent ;
2+ using System . Threading . Channels ;
13using MarketAssistant . Agents . Trading ;
24using MarketAssistant . Infrastructure . Factories ;
35using MarketAssistant . Services . Data ;
911namespace MarketAssistant . Trading ;
1012
1113/// <summary>
12- /// 后台市场监控器,订阅实时价格并根据策略触发交易
14+ /// 后台市场监控器,订阅实时价格并根据策略触发交易。
15+ /// 使用 Channel 缓冲价格更新,确保不丢弃任何 tick。
1316/// </summary>
1417public class MarketMonitor : IDisposable
1518{
@@ -22,7 +25,17 @@ public class MarketMonitor : IDisposable
2225
2326 private CancellationTokenSource ? _cts ;
2427 private bool _isRunning ;
25- private readonly SemaphoreSlim _evaluationLock = new ( 1 , 1 ) ;
28+ private Task ? _consumerTask ;
29+
30+ private readonly Channel < ( string Symbol , decimal Price ) > _priceChannel =
31+ Channel . CreateBounded < ( string , decimal ) > ( new BoundedChannelOptions ( 10000 )
32+ {
33+ FullMode = BoundedChannelFullMode . DropOldest ,
34+ SingleReader = true ,
35+ SingleWriter = false
36+ } ) ;
37+
38+ private readonly ConcurrentDictionary < string , SemaphoreSlim > _strategyLocks = new ( ) ;
2639
2740 public bool IsRunning => _isRunning ;
2841
@@ -70,11 +83,10 @@ public async Task StartAsync()
7083 . ToList ( ) ;
7184
7285 if ( symbols . Count > 0 )
73- {
7486 await _webSocketService . SubscribeAsync ( symbols ) ;
75- }
7687
7788 _webSocketService . PriceUpdated += OnPriceUpdated ;
89+ _consumerTask = Task . Run ( ( ) => ConsumePriceUpdatesAsync ( _cts . Token ) ) ;
7890
7991 _logger . LogInformation ( "MarketMonitor 已启动,监控 {Count} 个交易对" , symbols . Count ) ;
8092 StatusChanged ? . Invoke ( true ) ;
@@ -91,6 +103,12 @@ public async Task StopAsync()
91103 _webSocketService . PriceUpdated -= OnPriceUpdated ;
92104 _cts ? . Cancel ( ) ;
93105
106+ if ( _consumerTask != null )
107+ {
108+ try { await _consumerTask ; }
109+ catch ( OperationCanceledException ) { }
110+ }
111+
94112 await _webSocketService . UnsubscribeAllAsync ( ) ;
95113
96114 _isRunning = false ;
@@ -106,56 +124,77 @@ public async Task RefreshSubscriptionsAsync()
106124 if ( ! _isRunning )
107125 return ;
108126
109- await _webSocketService . UnsubscribeAllAsync ( ) ;
110-
111127 var activeStrategies = await _dataService . GetStrategiesByStatusAsync ( StrategyStatus . Active ) ;
112- var symbols = activeStrategies
128+ var newSymbols = activeStrategies
113129 . Select ( s => s . Symbol . ToLowerInvariant ( ) )
114130 . Distinct ( )
115- . ToList ( ) ;
131+ . ToHashSet ( ) ;
116132
117- if ( symbols . Count > 0 )
118- {
119- await _webSocketService . SubscribeAsync ( symbols ) ;
120- }
133+ await _webSocketService . UnsubscribeAllAsync ( ) ;
121134
122- _logger . LogInformation ( "已刷新监控列表: {Count} 个交易对" , symbols . Count ) ;
135+ if ( newSymbols . Count > 0 )
136+ await _webSocketService . SubscribeAsync ( newSymbols . ToList ( ) ) ;
137+
138+ _logger . LogInformation ( "已刷新监控列表: {Count} 个交易对" , newSymbols . Count ) ;
123139 }
124140
125141 private void OnPriceUpdated ( string symbol , decimal lastPrice , decimal changePercent )
126142 {
127- if ( _cts ? . IsCancellationRequested == true )
128- return ;
129-
130- _ = ProcessPriceUpdateAsync ( symbol , lastPrice ) ;
143+ _priceChannel . Writer . TryWrite ( ( symbol , lastPrice ) ) ;
131144 }
132145
133- private async Task ProcessPriceUpdateAsync ( string symbol , decimal lastPrice )
146+ /// <summary>
147+ /// Channel 消费者:顺序评估策略,异步执行触发的交易(每策略独立锁)
148+ /// </summary>
149+ private async Task ConsumePriceUpdatesAsync ( CancellationToken ct )
134150 {
135- if ( ! await _evaluationLock . WaitAsync ( 0 ) )
136- return ;
137-
138151 try
139152 {
140- var triggered = await _strategyEngine . EvaluateStrategiesAsync (
141- symbol , lastPrice , _cts ? . Token ?? CancellationToken . None ) ;
142-
143- foreach ( var strategy in triggered )
153+ await foreach ( var ( symbol , price ) in _priceChannel . Reader . ReadAllAsync ( ct ) )
144154 {
145- await HandleTriggeredStrategyAsync ( strategy , lastPrice ) ;
155+ try
156+ {
157+ var triggered = await _strategyEngine . EvaluateStrategiesAsync ( symbol , price , ct ) ;
158+ foreach ( var strategy in triggered )
159+ _ = ExecuteWithStrategyLockAsync ( strategy , price , ct ) ;
160+ }
161+ catch ( OperationCanceledException ) { throw ; }
162+ catch ( Exception ex )
163+ {
164+ _logger . LogError ( ex , "价格更新处理异常: {Symbol}" , symbol ) ;
165+ }
146166 }
147167 }
148168 catch ( OperationCanceledException )
149169 {
150- // 正常取消,忽略
170+ _logger . LogDebug ( "价格消费者已取消" ) ;
171+ }
172+ }
173+
174+ /// <summary>
175+ /// 带策略级锁的异步执行,防止同一策略并发触发
176+ /// </summary>
177+ private async Task ExecuteWithStrategyLockAsync (
178+ TradingStrategy strategy , decimal price , CancellationToken ct )
179+ {
180+ var strategyLock = _strategyLocks . GetOrAdd ( strategy . Id , _ => new SemaphoreSlim ( 1 , 1 ) ) ;
181+ if ( ! await strategyLock . WaitAsync ( 0 , ct ) )
182+ {
183+ _logger . LogDebug ( "策略 {Id} 正在执行中,跳过本次触发" , strategy . Id ) ;
184+ return ;
185+ }
186+
187+ try
188+ {
189+ await HandleTriggeredStrategyAsync ( strategy , price ) ;
151190 }
152191 catch ( Exception ex )
153192 {
154- _logger . LogError ( ex , "价格更新处理异常 : {Symbol }" , symbol ) ;
193+ _logger . LogError ( ex , "策略执行异常 : {StrategyId }" , strategy . Id ) ;
155194 }
156195 finally
157196 {
158- _evaluationLock . Release ( ) ;
197+ strategyLock . Release ( ) ;
159198 }
160199 }
161200
@@ -167,54 +206,83 @@ private async Task HandleTriggeredStrategyAsync(TradingStrategy strategy, decima
167206 }
168207 else
169208 {
170- var result = await _tradeExecutor . ExecuteTradeAsync ( strategy , currentPrice , ct : _cts ? . Token ?? default ) ;
209+ var result = await _tradeExecutor . ExecuteTradeAsync (
210+ strategy , currentPrice , ct : _cts ? . Token ?? default ) ;
211+
171212 if ( result . Success && result . Record != null )
172- {
173213 TradeExecuted ? . Invoke ( result . Record ) ;
174- }
175214
176- if ( strategy . MaxExecutions . HasValue &&
177- strategy . ExecutionCount + 1 >= strategy . MaxExecutions . Value )
178- {
179- await _dataService . UpdateStrategyStatusAsync ( strategy . Id , StrategyStatus . Completed ) ;
180- }
215+ await CheckStrategyCompletionAsync ( strategy ) ;
181216 }
182217 }
183218
184219 private async Task HandleAISignalAsync ( TradingStrategy strategy , decimal currentPrice )
185220 {
186221 try
187222 {
223+ TradingContext . CurrentStrategyId = strategy . Id ;
224+
188225 var agent = _agentFactory . CreateAgent ( ) ;
189226 var prompt = $ """
190227 分析交易对 { strategy . Symbol } ,当前价格 { currentPrice } 。
191228 策略配置: { strategy . CustomParams ?? "无" }
192229 请评估是否应该执行 { strategy . Side } 操作,数量 { strategy . Quantity } 。
193230 如果决定交易,请调用 PlaceOrder 工具执行。
231+ 如果决定不交易,请说明理由。
194232 """ ;
195233
196234 var messages = new List < ChatMessage >
197235 {
198236 new ( ChatRole . User , prompt )
199237 } ;
200238
201- await _dataService . UpdateStrategyTriggeredAsync ( strategy . Id ) ;
202-
203239 var response = await agent . RunAsync ( messages , session : null , options : null ,
204240 cancellationToken : _cts ? . Token ?? default ) ;
205241 _logger . LogDebug ( "TradingAgent 响应: {Content}" , response . Text ) ;
242+
243+ // 只在 Agent 实际执行了交易后才更新触发计数
244+ var recentRecords = await _dataService . GetRecordsByStrategyAsync ( strategy . Id ) ;
245+ var hasNewTrade = recentRecords . Any ( r =>
246+ r . CreatedAt > ( strategy . LastTriggeredAt ?? DateTime . MinValue ) ) ;
247+
248+ if ( hasNewTrade )
249+ {
250+ await _dataService . UpdateStrategyTriggeredAsync ( strategy . Id ) ;
251+ await CheckStrategyCompletionAsync ( strategy ) ;
252+ }
206253 }
207254 catch ( Exception ex )
208255 {
209256 _logger . LogError ( ex , "AI 信号策略执行失败: {StrategyId}" , strategy . Id ) ;
210257 }
258+ finally
259+ {
260+ TradingContext . CurrentStrategyId = null ;
261+ }
262+ }
263+
264+ private async Task CheckStrategyCompletionAsync ( TradingStrategy strategy )
265+ {
266+ if ( ! strategy . MaxExecutions . HasValue )
267+ return ;
268+
269+ var updated = await _dataService . GetStrategyAsync ( strategy . Id ) ;
270+ if ( updated != null && updated . ExecutionCount >= updated . MaxExecutions ! . Value )
271+ {
272+ await _dataService . UpdateStrategyStatusAsync ( strategy . Id , StrategyStatus . Completed ) ;
273+ _strategyEngine . ClearPeakPrice ( strategy . Id ) ;
274+ _strategyLocks . TryRemove ( strategy . Id , out _ ) ;
275+ }
211276 }
212277
213278 public void Dispose ( )
214279 {
215280 _cts ? . Cancel ( ) ;
216281 _cts ? . Dispose ( ) ;
217- _evaluationLock . Dispose ( ) ;
282+ _priceChannel . Writer . TryComplete ( ) ;
283+ foreach ( var kvp in _strategyLocks )
284+ kvp . Value . Dispose ( ) ;
285+ _strategyLocks . Clear ( ) ;
218286 GC . SuppressFinalize ( this ) ;
219287 }
220288}
0 commit comments