Skip to content

Commit ff2b685

Browse files
committed
Refactor trading execution tools and enhance risk management
- Updated CryptoTradingExecutionTools to delegate order execution to TradeExecutor, streamlining the trading process. - Improved MarketMonitor to utilize a Channel for price updates, ensuring no tick is dropped and enhancing performance. - Enhanced RiskManager to calculate total asset value in USDT asynchronously, improving accuracy in risk assessments. - Introduced a new method in TradingDataService to compute the average entry price for better PnL estimation. - Updated various models and services to support new features and improve overall code clarity and maintainability.
1 parent 3c532b9 commit ff2b685

8 files changed

Lines changed: 268 additions & 141 deletions

File tree

src/Agents/Tools/Crypto/CryptoTradingExecutionTools.cs

Lines changed: 10 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,24 @@
1010
namespace MarketAssistant.Agents.Tools.Crypto;
1111

1212
/// <summary>
13-
/// 虚拟币交易执行工具实现,供 TradingAgent 使用
13+
/// 虚拟币交易执行工具实现,供 TradingAgent 使用。下单委托给 TradeExecutor 统一入口。
1414
/// </summary>
1515
public class CryptoTradingExecutionTools : ITradingExecutionTools
1616
{
1717
private readonly BinanceAccountService _accountService;
1818
private readonly BinanceMarketDataService _marketDataService;
19-
private readonly RiskManager _riskManager;
20-
private readonly TradingDataService _dataService;
19+
private readonly TradeExecutor _tradeExecutor;
2120
private readonly ILogger<CryptoTradingExecutionTools> _logger;
2221

2322
public CryptoTradingExecutionTools(
2423
BinanceAccountService accountService,
2524
BinanceMarketDataService marketDataService,
26-
RiskManager riskManager,
27-
TradingDataService dataService,
25+
TradeExecutor tradeExecutor,
2826
ILogger<CryptoTradingExecutionTools> logger)
2927
{
3028
_accountService = accountService;
3129
_marketDataService = marketDataService;
32-
_riskManager = riskManager;
33-
_dataService = dataService;
30+
_tradeExecutor = tradeExecutor;
3431
_logger = logger;
3532
}
3633

@@ -137,51 +134,13 @@ public async Task<TradeResult> PlaceOrderAsync(
137134
}
138135

139136
if (effectivePrice <= 0)
140-
{
141-
return new TradeResult
142-
{
143-
Success = false,
144-
ErrorMessage = $"无法确定 {symbol} 的有效价格,拒绝下单"
145-
};
146-
}
147-
148-
var riskCheck = await _riskManager.ValidateOrderAsync(symbol, side, quantity, effectivePrice);
149-
if (!riskCheck.Passed)
150-
{
151-
_logger.LogWarning("风控拒绝: {Reason}", riskCheck.Reason);
152-
return new TradeResult { Success = false, ErrorMessage = $"风控拒绝: {riskCheck.Reason}" };
153-
}
154-
155-
try
156-
{
157-
var response = await _accountService.PlaceOrderAsync(
158-
symbol, side.ToString().ToUpper(), type.ToString().ToUpper(), quantity, price);
137+
return new TradeResult { Success = false, ErrorMessage = $"无法确定 {symbol} 的有效价格,拒绝下单" };
159138

160-
var record = new TradeRecord
161-
{
162-
StrategyId = "manual",
163-
Symbol = symbol,
164-
Side = side,
165-
OrderType = type,
166-
RequestedQty = quantity,
167-
ExecutedQty = decimal.TryParse(response.ExecutedQty, out var eq) ? eq : 0,
168-
RequestedPrice = price,
169-
ExecutedPrice = decimal.TryParse(response.Price, out var ep) ? ep : effectivePrice,
170-
Status = MapOrderStatus(response.Status),
171-
BinanceOrderId = response.OrderId,
172-
CompletedAt = response.Status == "FILLED" ? DateTime.UtcNow : null
173-
};
174-
175-
await _dataService.SaveTradeRecordAsync(record);
176-
await _dataService.UpdateDailyStatsAsync(0, 0);
177-
178-
return new TradeResult { Success = true, Record = record };
179-
}
180-
catch (Exception ex)
181-
{
182-
_logger.LogError(ex, "下单失败: {Symbol} {Side}", symbol, side);
183-
return new TradeResult { Success = false, ErrorMessage = ex.Message };
184-
}
139+
var strategyId = TradingContext.CurrentStrategyId ?? "manual";
140+
return await _tradeExecutor.ExecuteOrderAsync(
141+
symbol, side, type, quantity, effectivePrice,
142+
type == OrderType.Limit ? price : null,
143+
strategyId: strategyId);
185144
}
186145

187146
[Description("查询指定订单的状态")]
@@ -225,13 +184,4 @@ public IEnumerable<AIFunction> GetFunctions()
225184
yield return AIFunctionFactory.Create(GetOrderStatusAsync);
226185
yield return AIFunctionFactory.Create(CancelOrderAsync);
227186
}
228-
229-
private static TradeRecordStatus MapOrderStatus(string binanceStatus) => binanceStatus switch
230-
{
231-
"FILLED" => TradeRecordStatus.Filled,
232-
"PARTIALLY_FILLED" => TradeRecordStatus.PartiallyFilled,
233-
"CANCELED" or "CANCELLED" => TradeRecordStatus.Cancelled,
234-
"REJECTED" or "EXPIRED" => TradeRecordStatus.Failed,
235-
_ => TradeRecordStatus.Pending
236-
};
237187
}

src/Trading/MarketMonitor.cs

Lines changed: 108 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Concurrent;
2+
using System.Threading.Channels;
13
using MarketAssistant.Agents.Trading;
24
using MarketAssistant.Infrastructure.Factories;
35
using MarketAssistant.Services.Data;
@@ -9,7 +11,8 @@
911
namespace MarketAssistant.Trading;
1012

1113
/// <summary>
12-
/// 后台市场监控器,订阅实时价格并根据策略触发交易
14+
/// 后台市场监控器,订阅实时价格并根据策略触发交易。
15+
/// 使用 Channel 缓冲价格更新,确保不丢弃任何 tick。
1316
/// </summary>
1417
public 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
}

src/Trading/Models/TradingModels.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,27 @@ public class OrderStatusInfo
141141
public class RiskCheckResult
142142
{
143143
public bool Passed { get; set; }
144+
public bool NeedsConfirmation { get; set; }
144145
public string? Reason { get; set; }
145146

146147
public static RiskCheckResult Pass() => new() { Passed = true };
147148
public static RiskCheckResult Reject(string reason) => new() { Passed = false, Reason = reason };
149+
public static RiskCheckResult RequireConfirmation(string reason) =>
150+
new() { Passed = false, NeedsConfirmation = true, Reason = reason };
151+
}
152+
153+
/// <summary>
154+
/// 交易上下文,用于在 Agent 工具调用链中传递当前策略 ID
155+
/// </summary>
156+
public static class TradingContext
157+
{
158+
private static readonly AsyncLocal<string?> _strategyId = new();
159+
160+
public static string? CurrentStrategyId
161+
{
162+
get => _strategyId.Value;
163+
set => _strategyId.Value = value;
164+
}
148165
}
149166

150167
#endregion

0 commit comments

Comments
 (0)