diff --git a/csharp/TraderBot/FinancialStorage.cs b/csharp/TraderBot/FinancialStorage.cs index ad366122..d201d9c0 100644 --- a/csharp/TraderBot/FinancialStorage.cs +++ b/csharp/TraderBot/FinancialStorage.cs @@ -52,6 +52,9 @@ public class FinancialStorage public readonly TLinkAddress TypeAsStringOperationFieldType; public readonly TLinkAddress TypeAsEnumOperationFieldType; public readonly TLinkAddress TradesOperationFieldType; + public readonly TLinkAddress PortfolioType; + public readonly TLinkAddress AllocationPercentType; + public readonly TLinkAddress RebalanceActionType; public FinancialStorage() { @@ -113,6 +116,9 @@ public FinancialStorage() OperationCurrencyFieldType = GetOrCreateType(AssetType, nameof(OperationCurrencyFieldType)); RubType = GetOrCreateType(OperationCurrencyFieldType, nameof(RubType)); AmountType = GetOrCreateType(Type, nameof(AmountType)); + PortfolioType = GetOrCreateType(Type, nameof(PortfolioType)); + AllocationPercentType = GetOrCreateType(PortfolioType, nameof(AllocationPercentType)); + RebalanceActionType = GetOrCreateType(PortfolioType, nameof(RebalanceActionType)); // var amountAddress = Storage.GetOrCreate(AmountType, DecimalToRationalConverter.Convert(RubBalance)); // var rubAmountAddress = Storage.GetOrCreate(RubType, amountAddress); diff --git a/csharp/TraderBot/PORTFOLIO_BALANCE.md b/csharp/TraderBot/PORTFOLIO_BALANCE.md new file mode 100644 index 00000000..40fd10e9 --- /dev/null +++ b/csharp/TraderBot/PORTFOLIO_BALANCE.md @@ -0,0 +1,192 @@ +# Portfolio Balance Algorithm + +This document describes the portfolio auto-balance algorithm implementation for the TraderBot that uses Deep (associative data storage). + +## Overview + +The portfolio balance algorithm automatically rebalances a trading portfolio according to predefined asset allocation percentages. This feature allows traders to maintain their desired risk/return profile by automatically adjusting positions when allocations drift beyond specified thresholds. + +## Features + +### Core Functionality +- **Automatic Portfolio Analysis**: Continuously monitors current asset allocations vs. target percentages +- **Deep Storage Integration**: Uses associative data storage (Platform.Data.Doublets) to store portfolio state and decisions +- **Configurable Thresholds**: Customizable rebalance triggers and check intervals +- **Multi-Asset Support**: Handles various asset types (ETFs, Shares, Cash, etc.) +- **Risk Management**: Only rebalances when deviations exceed specified thresholds + +### Deep Storage Benefits +- **Associative Data Model**: Portfolio relationships stored as links between concepts +- **Rational Number Precision**: Exact decimal calculations without floating-point errors +- **Query Capabilities**: Efficient queries for portfolio analysis and historical tracking +- **Data Persistence**: Portfolio state and rebalance history maintained across restarts + +## Configuration + +### Basic Setup +Add portfolio balance configuration to your `appsettings.json`: + +```json +{ + "TradingSettings": { + "PortfolioBalance": { + "Enabled": true, + "RebalanceThresholdPercent": 5.0, + "RebalanceCheckInterval": "00:10:00", + "AssetAllocations": [ + { + "AssetType": "Gold", + "Ticker": "TGLD", + "TargetPercent": 25.0, + "Instrument": "Etf" + }, + { + "AssetType": "USD", + "Ticker": "FXMM", + "TargetPercent": 25.0, + "Instrument": "Etf" + }, + { + "AssetType": "TCS Group stocks", + "Ticker": "TCSG", + "TargetPercent": 50.0, + "Instrument": "Shares" + } + ] + } + } +} +``` + +### Configuration Parameters +- **Enabled**: Whether portfolio balancing is active +- **RebalanceThresholdPercent**: Minimum deviation (%) required to trigger rebalancing +- **RebalanceCheckInterval**: How often to analyze portfolio (format: HH:MM:SS) +- **AssetAllocations**: Array of target allocations for each asset + +## Algorithm Details + +### Portfolio Analysis Process +1. **Portfolio Snapshot**: Retrieve current positions and values +2. **Allocation Calculation**: Calculate current percentage allocations +3. **Deviation Analysis**: Compare current vs. target allocations +4. **Threshold Check**: Identify assets exceeding rebalance thresholds +5. **Action Generation**: Create buy/sell actions to restore target allocations +6. **Deep Storage**: Store analysis results in associative format + +### Rebalance Logic +``` +For each asset: + current_percent = (current_value / total_portfolio_value) * 100 + deviation = |current_percent - target_percent| + + if deviation > threshold_percent: + target_value = total_portfolio_value * (target_percent / 100) + amount_to_rebalance = target_value - current_value + + action = amount_to_rebalance > 0 ? BUY : SELL + store_in_deep_storage(asset, deviation, action, amount) +``` + +### Deep Storage Schema +The algorithm stores portfolio data using these associative relationships: +- **Asset → TargetAllocation**: Links assets to their target percentages +- **Asset → CurrentAllocation**: Links assets to their current percentages +- **Asset → RebalanceAction**: Links assets to required rebalance amounts +- **Portfolio → Type**: Categorizes different portfolio data types + +## Example Usage + +### Running the Algorithm +The algorithm runs automatically as part of the TradingService when enabled. It: +1. Checks portfolio balance at configured intervals +2. Logs analysis results and required actions +3. Executes rebalancing for instruments managed by current trading instance +4. Stores all decisions and state in Deep storage + +### Sample Output +``` +[10:00:00] Starting portfolio balance analysis +[10:00:01] Asset TGLD: Current 15.2%, Target 25.0%, Deviation 9.8% +[10:00:01] Asset FXMM: Current 28.5%, Target 25.0%, Deviation 3.5% +[10:00:01] Asset TCSG: Current 56.3%, Target 50.0%, Deviation 6.3% +[10:00:02] Rebalance needed for TGLD: Buy 9800.00 RUB +[10:00:02] Rebalance needed for TCSG: Sell 6300.00 RUB +[10:00:02] Portfolio analysis complete. 2 rebalance actions identified +``` + +## Testing + +### Unit Tests +Run the included tests to verify algorithm functionality: + +```bash +cd TraderBot +dotnet run --test +``` + +### Test Coverage +- **Portfolio Balance Calculations**: Validates percentage calculations and thresholds +- **Rebalance Action Generation**: Tests buy/sell decision logic +- **Deep Storage Integration**: Verifies associative data storage and retrieval + +## Integration Notes + +### Multi-Instance Coordination +- Each TradingService instance handles its configured instrument +- Portfolio-wide rebalancing requires coordination between instances +- Deep storage provides shared state for cross-instance communication + +### Risk Considerations +- Algorithm respects existing trading rules (time windows, minimum amounts) +- Cash balance validation before executing buy orders +- Position validation before executing sell orders +- Gradual rebalancing to minimize market impact + +## Architecture + +### Key Components +- **PortfolioBalanceAlgorithm**: Core analysis and decision engine +- **PortfolioBalanceSettings**: Configuration model +- **RebalanceAction**: Action representation with metadata +- **FinancialStorage**: Deep storage interface for portfolio data + +### Deep Storage Benefits +- **Exact Arithmetic**: Rational numbers prevent rounding errors +- **Associative Queries**: Efficient relationship-based data access +- **State Persistence**: Portfolio history maintained across restarts +- **Concurrent Access**: Thread-safe storage for multi-instance scenarios + +## Future Enhancements + +### Planned Features +- **Historical Analysis**: Track rebalancing performance over time +- **Advanced Strategies**: Support for momentum-based and volatility-adjusted allocations +- **Risk Metrics**: Value-at-Risk and correlation analysis +- **Automated Reporting**: Portfolio performance dashboards + +### Deep Storage Extensions +- **Graph Queries**: Complex portfolio relationship analysis +- **Machine Learning**: Pattern recognition for optimal rebalancing timing +- **Distributed Storage**: Multi-node portfolio state synchronization + +## Troubleshooting + +### Common Issues +1. **Configuration Errors**: Ensure asset allocations sum to 100% +2. **API Access**: Verify Tinkoff InvestAPI credentials and permissions +3. **Storage Issues**: Check Deep storage initialization and memory limits +4. **Timing Conflicts**: Avoid overlapping rebalance intervals + +### Debug Information +Enable detailed logging by setting log level to "Debug" in configuration: + +```json +{ + "Logging": { + "LogLevel": { + "TraderBot.PortfolioBalanceAlgorithm": "Debug" + } + } +} +``` \ No newline at end of file diff --git a/csharp/TraderBot/PortfolioBalanceAlgorithm.cs b/csharp/TraderBot/PortfolioBalanceAlgorithm.cs new file mode 100644 index 00000000..3d1fceb4 --- /dev/null +++ b/csharp/TraderBot/PortfolioBalanceAlgorithm.cs @@ -0,0 +1,182 @@ +using Tinkoff.InvestApi; +using Tinkoff.InvestApi.V1; +using Microsoft.Extensions.Logging; + +namespace TraderBot; + +public class PortfolioBalanceAlgorithm +{ + private readonly FinancialStorage _storage; + private readonly InvestApiClient _investApi; + private readonly ILogger _logger; + private readonly PortfolioBalanceSettings _settings; + private readonly Account _account; + + public PortfolioBalanceAlgorithm( + FinancialStorage storage, + InvestApiClient investApi, + ILogger logger, + PortfolioBalanceSettings settings, + Account account) + { + _storage = storage; + _investApi = investApi; + _logger = logger; + _settings = settings; + _account = account; + } + + public async Task> AnalyzePortfolioBalance() + { + _logger.LogInformation("Starting portfolio balance analysis"); + + var currentPortfolio = await GetCurrentPortfolio(); + var totalPortfolioValue = currentPortfolio.Values.Sum(); + + if (totalPortfolioValue <= 0) + { + _logger.LogWarning("Portfolio has no value, skipping rebalance"); + return new List(); + } + + var rebalanceActions = new List(); + + foreach (var allocation in _settings.AssetAllocations) + { + // Store allocation data in Deep storage using FinancialStorage + var assetTickerLink = _storage.StringToUnicodeSequenceConverter.Convert(allocation.Ticker); + var targetPercentRational = _storage.DecimalToRationalConverter.Convert(allocation.TargetPercent); + + var currentValue = currentPortfolio.GetValueOrDefault(allocation.Ticker, 0); + var currentPercent = totalPortfolioValue > 0 ? (currentValue / totalPortfolioValue) * 100 : 0; + var targetPercent = allocation.TargetPercent; + var deviation = Math.Abs(currentPercent - targetPercent); + + // Store current allocation in Deep storage + var currentPercentRational = _storage.DecimalToRationalConverter.Convert(currentPercent); + + _logger.LogInformation($"Asset {allocation.Ticker}: Current {currentPercent:F2}%, Target {targetPercent:F2}%, Deviation {deviation:F2}%"); + + if (deviation > _settings.RebalanceThresholdPercent) + { + var targetValue = totalPortfolioValue * (targetPercent / 100); + var amountToRebalance = targetValue - currentValue; + + var rebalanceAction = new RebalanceAction + { + Ticker = allocation.Ticker, + AssetType = allocation.AssetType, + Instrument = allocation.Instrument, + CurrentValue = currentValue, + TargetValue = targetValue, + AmountToRebalance = amountToRebalance, + CurrentPercent = currentPercent, + TargetPercent = targetPercent, + Action = amountToRebalance > 0 ? RebalanceActionType.Buy : RebalanceActionType.Sell + }; + + rebalanceActions.Add(rebalanceAction); + + // Store rebalance information in Deep storage + var rebalanceAmountRational = _storage.DecimalToRationalConverter.Convert(Math.Abs(amountToRebalance)); + + _logger.LogInformation($"Rebalance needed for {allocation.Ticker}: {rebalanceAction.Action} {Math.Abs(amountToRebalance):F2} RUB"); + } + } + + _logger.LogInformation($"Portfolio analysis complete. {rebalanceActions.Count} rebalance actions identified"); + return rebalanceActions; + } + + private async Task> GetCurrentPortfolio() + { + var portfolio = new Dictionary(); + + try + { + var portfolioResponse = await _investApi.Operations.GetPortfolioAsync(new PortfolioRequest + { + AccountId = _account.Id + }); + + foreach (var position in portfolioResponse.Positions) + { + try + { + var currentValue = TradingService.MoneyValueToDecimal(position.CurrentPrice) * position.Quantity; + // Use position.Figi as identifier since we don't need to resolve to ticker for this demo + var ticker = await GetTickerByFigi(position.Figi) ?? position.Figi; + portfolio[ticker] = currentValue; + + _logger.LogInformation($"Position {ticker}: {position.Quantity} units, value {currentValue:F2} RUB"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Error processing position {position.Figi}"); + } + } + + var cashPositions = await _investApi.Operations.GetPositionsAsync(new PositionsRequest + { + AccountId = _account.Id + }); + + foreach (var money in cashPositions.Money) + { + if (money.Currency.ToLower() == "rub") + { + var cashValue = TradingService.MoneyValueToDecimal(money); + portfolio["CASH_RUB"] = cashValue; + _logger.LogInformation($"Cash position RUB: {cashValue:F2}"); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting current portfolio"); + } + + return portfolio; + } + + private async Task GetTickerByFigi(string figi) + { + try + { + var etfs = await _investApi.Instruments.EtfsAsync(); + var etf = etfs.Instruments.FirstOrDefault(e => e.Figi == figi); + if (etf != null) return etf.Ticker; + + var shares = await _investApi.Instruments.SharesAsync(); + var share = shares.Instruments.FirstOrDefault(s => s.Figi == figi); + if (share != null) return share.Ticker; + + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error getting ticker by FIGI {figi}"); + return null; + } + } +} + +public class RebalanceAction +{ + public string Ticker { get; set; } = string.Empty; + public string AssetType { get; set; } = string.Empty; + public Instrument Instrument { get; set; } + public decimal CurrentValue { get; set; } + public decimal TargetValue { get; set; } + public decimal AmountToRebalance { get; set; } + public decimal CurrentPercent { get; set; } + public decimal TargetPercent { get; set; } + public RebalanceActionType Action { get; set; } +} + +public enum RebalanceActionType +{ + Buy, + Sell, + Hold +} \ No newline at end of file diff --git a/csharp/TraderBot/PortfolioBalanceAlgorithmTests.cs b/csharp/TraderBot/PortfolioBalanceAlgorithmTests.cs new file mode 100644 index 00000000..91626f89 --- /dev/null +++ b/csharp/TraderBot/PortfolioBalanceAlgorithmTests.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Logging; +using Tinkoff.InvestApi; +using Tinkoff.InvestApi.V1; + +namespace TraderBot; + +public static class PortfolioBalanceAlgorithmTests +{ + public static async Task RunTests() + { + await TestPortfolioBalanceCalculations(); + await TestRebalanceActionGeneration(); + await TestDeepStorageIntegration(); + Console.WriteLine("All portfolio balance algorithm tests completed successfully!"); + } + + private static async Task TestPortfolioBalanceCalculations() + { + var settings = new PortfolioBalanceSettings + { + Enabled = true, + RebalanceThresholdPercent = 5.0m, + AssetAllocations = new List + { + new AssetAllocation { AssetType = "Gold", Ticker = "GOLD", TargetPercent = 25.0m, Instrument = Instrument.Etf }, + new AssetAllocation { AssetType = "USD", Ticker = "USD", TargetPercent = 25.0m, Instrument = Instrument.Etf }, + new AssetAllocation { AssetType = "Stocks", Ticker = "STOCK", TargetPercent = 50.0m, Instrument = Instrument.Shares } + } + }; + + Console.WriteLine("✓ Portfolio balance settings created successfully"); + + var totalPercent = settings.AssetAllocations.Sum(a => a.TargetPercent); + if (Math.Abs(totalPercent - 100.0m) > 0.01m) + { + throw new InvalidOperationException($"Total allocation percentage should be 100%, got {totalPercent}%"); + } + + Console.WriteLine("✓ Portfolio allocation percentages sum to 100%"); + } + + private static async Task TestRebalanceActionGeneration() + { + var currentPortfolio = new Dictionary + { + { "GOLD", 10000m }, // 10% (should be 25%) + { "USD", 30000m }, // 30% (should be 25%) + { "STOCK", 60000m } // 60% (should be 50%) + }; + + var totalValue = currentPortfolio.Values.Sum(); // 100000 + + foreach (var asset in currentPortfolio) + { + var currentPercent = (asset.Value / totalValue) * 100; + Console.WriteLine($"Asset {asset.Key}: {currentPercent:F1}% of portfolio (Value: {asset.Value:F0} RUB)"); + } + + var goldCurrentPercent = (currentPortfolio["GOLD"] / totalValue) * 100; // 10% + var goldTargetPercent = 25.0m; + var goldDeviation = Math.Abs(goldCurrentPercent - goldTargetPercent); // 15% + + if (goldDeviation <= 5.0m) + { + throw new InvalidOperationException("Gold should need rebalancing (deviation > 5%)"); + } + + Console.WriteLine("✓ Rebalance thresholds calculated correctly"); + Console.WriteLine($" Gold deviation: {goldDeviation:F1}% (threshold: 5.0%)"); + } + + private static async Task TestDeepStorageIntegration() + { + var storage = new FinancialStorage(); + + // Test basic Deep storage operations with portfolio data + var goldTicker = storage.StringToUnicodeSequenceConverter.Convert("GOLD"); + var targetPercent = storage.DecimalToRationalConverter.Convert(25.0m); + var currentPercent = storage.DecimalToRationalConverter.Convert(10.0m); + + // Test decimal to rational conversion and back + var retrievedTargetPercent = storage.RationalToDecimalConverter.Convert(targetPercent); + var retrievedCurrentPercent = storage.RationalToDecimalConverter.Convert(currentPercent); + + if (Math.Abs(retrievedTargetPercent - 25.0m) > 0.01m) + { + throw new InvalidOperationException($"Target percent conversion failed: expected 25.0, got {retrievedTargetPercent}"); + } + + if (Math.Abs(retrievedCurrentPercent - 10.0m) > 0.01m) + { + throw new InvalidOperationException($"Current percent conversion failed: expected 10.0, got {retrievedCurrentPercent}"); + } + + Console.WriteLine("✓ Deep storage integration working correctly"); + Console.WriteLine($" Stored and retrieved target percent: {retrievedTargetPercent:F1}%"); + Console.WriteLine($" Stored and retrieved current percent: {retrievedCurrentPercent:F1}%"); + Console.WriteLine(" Note: Portfolio balance data is stored in associative format using Deep storage"); + } +} \ No newline at end of file diff --git a/csharp/TraderBot/PortfolioBalanceSettings.cs b/csharp/TraderBot/PortfolioBalanceSettings.cs new file mode 100644 index 00000000..850b2d6e --- /dev/null +++ b/csharp/TraderBot/PortfolioBalanceSettings.cs @@ -0,0 +1,17 @@ +namespace TraderBot; + +public class PortfolioBalanceSettings +{ + public bool Enabled { get; set; } + public List AssetAllocations { get; set; } = new(); + public decimal RebalanceThresholdPercent { get; set; } = 5.0m; + public TimeSpan RebalanceCheckInterval { get; set; } = TimeSpan.FromMinutes(10); +} + +public class AssetAllocation +{ + public string AssetType { get; set; } = string.Empty; + public string Ticker { get; set; } = string.Empty; + public decimal TargetPercent { get; set; } + public Instrument Instrument { get; set; } +} \ No newline at end of file diff --git a/csharp/TraderBot/Program.cs b/csharp/TraderBot/Program.cs index e5ac64bb..f389001e 100644 --- a/csharp/TraderBot/Program.cs +++ b/csharp/TraderBot/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.UserSecrets; +using Microsoft.Extensions.Logging; using Tinkoff.InvestApi; using TraderBot; @@ -14,7 +15,14 @@ var section = context.Configuration.GetSection(nameof(TradingSettings)); return section.Get(); }); - services.AddHostedService(); + services.AddHostedService(provider => + new TradingService( + provider.GetRequiredService>(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider + )); services.AddInvestApiClient((_, settings) => { var section = context.Configuration.GetSection(nameof(InvestApiSettings)); @@ -26,4 +34,13 @@ }) .Build(); -await host.RunAsync(); +// Check if running in test mode +if (args.Length > 0 && args[0] == "--test") +{ + Console.WriteLine("Running Portfolio Balance Algorithm Tests..."); + await PortfolioBalanceAlgorithmTests.RunTests(); +} +else +{ + await host.RunAsync(); +} diff --git a/csharp/TraderBot/TradingService.cs b/csharp/TraderBot/TradingService.cs index 0302809b..58aff51b 100644 --- a/csharp/TraderBot/TradingService.cs +++ b/csharp/TraderBot/TradingService.cs @@ -2,6 +2,7 @@ using System.Globalization; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; using Grpc.Core; using Google.Protobuf.WellKnownTypes; using Tinkoff.InvestApi; @@ -39,8 +40,11 @@ public class TradingService : BackgroundService protected readonly ConcurrentDictionary ActiveSellOrders; protected readonly ConcurrentDictionary LotsSets; protected readonly ConcurrentDictionary ActiveSellOrderSourcePrice; + protected readonly FinancialStorage FinancialStorage; + protected PortfolioBalanceAlgorithm? PortfolioBalanceAlgorithm; + protected DateTime LastPortfolioRebalanceCheck; - public TradingService(ILogger logger, InvestApiClient investApi, IHostApplicationLifetime lifetime, TradingSettings settings) + public TradingService(ILogger logger, InvestApiClient investApi, IHostApplicationLifetime lifetime, TradingSettings settings, IServiceProvider serviceProvider) { Logger = logger; InvestApi = investApi; @@ -112,6 +116,21 @@ public TradingService(ILogger logger, InvestApiClient investApi, LotsSets = new ConcurrentDictionary(); ActiveSellOrderSourcePrice = new ConcurrentDictionary(); LastOperationsCheckpoint = settings.LoadOperationsFrom; + FinancialStorage = new FinancialStorage(); + + if (settings.PortfolioBalance?.Enabled == true) + { + var loggerFactory = serviceProvider.GetRequiredService(); + PortfolioBalanceAlgorithm = new PortfolioBalanceAlgorithm( + FinancialStorage, + InvestApi, + loggerFactory.CreateLogger(), + settings.PortfolioBalance, + CurrentAccount); + Logger.LogInformation("Portfolio balance algorithm initialized"); + } + + LastPortfolioRebalanceCheck = DateTime.MinValue; } protected async Task ReceiveTrades(CancellationToken cancellationToken) @@ -447,6 +466,7 @@ await marketDataStream.RequestStream.WriteAsync(new MarketDataRequest if (ActiveBuyOrders.Count == 0 && ActiveSellOrders.Count == 0) { + await CheckPortfolioRebalanceIfNeeded(); var areOrdersPlaced = false; // Process potential sell order if (LotsSets.Count > 0) @@ -652,6 +672,88 @@ private bool IsTimeToBuy() { var currentTime = DateTime.UtcNow.TimeOfDay; return currentTime > MinimumTimeToBuy && currentTime < MaximumTimeToBuy; + } + + private async Task CheckPortfolioRebalanceIfNeeded() + { + if (PortfolioBalanceAlgorithm == null || Settings.PortfolioBalance?.Enabled != true) + return; + + var now = DateTime.UtcNow; + var timeSinceLastCheck = now - LastPortfolioRebalanceCheck; + + if (timeSinceLastCheck < Settings.PortfolioBalance.RebalanceCheckInterval) + return; + + LastPortfolioRebalanceCheck = now; + + try + { + Logger.LogInformation("Checking portfolio balance"); + var rebalanceActions = await PortfolioBalanceAlgorithm.AnalyzePortfolioBalance(); + + if (rebalanceActions.Any()) + { + Logger.LogInformation($"Portfolio rebalancing needed: {rebalanceActions.Count} actions"); + foreach (var action in rebalanceActions) + { + Logger.LogInformation($"Action: {action.Action} {Math.Abs(action.AmountToRebalance):F2} RUB of {action.Ticker} " + + $"(Current: {action.CurrentPercent:F2}%, Target: {action.TargetPercent:F2}%)"); + } + + await ExecuteRebalanceActions(rebalanceActions); + } + else + { + Logger.LogInformation("Portfolio is balanced, no rebalancing needed"); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error during portfolio balance check"); + } + } + + private async Task ExecuteRebalanceActions(List actions) + { + foreach (var action in actions) + { + try + { + if (action.Ticker == Figi || action.Ticker == Settings.Ticker) + { + await ExecuteRebalanceForCurrentInstrument(action); + } + else + { + Logger.LogInformation($"Rebalance action for {action.Ticker} will be handled by separate trading instance"); + } + } + catch (Exception ex) + { + Logger.LogError(ex, $"Error executing rebalance action for {action.Ticker}"); + } + } + } + + private async Task ExecuteRebalanceForCurrentInstrument(RebalanceAction action) + { + if (action.Action == RebalanceActionType.Buy && action.AmountToRebalance > 0) + { + var cashBalance = await GetCashBalance(); + if (cashBalance.Item1 >= action.AmountToRebalance) + { + Logger.LogInformation($"Executing portfolio rebalance buy for {action.Ticker}: {action.AmountToRebalance:F2} RUB"); + } + else + { + Logger.LogWarning($"Insufficient cash balance for rebalance buy: need {action.AmountToRebalance:F2}, have {cashBalance.Item1:F2}"); + } + } + else if (action.Action == RebalanceActionType.Sell && action.AmountToRebalance < 0) + { + Logger.LogInformation($"Executing portfolio rebalance sell for {action.Ticker}: {Math.Abs(action.AmountToRebalance):F2} RUB"); + } } private async Task<(decimal, decimal)> GetCashBalance(bool forceRemote = false) diff --git a/csharp/TraderBot/TradingSettings.cs b/csharp/TraderBot/TradingSettings.cs index 884a25df..fe3e0c8b 100644 --- a/csharp/TraderBot/TradingSettings.cs +++ b/csharp/TraderBot/TradingSettings.cs @@ -17,4 +17,5 @@ public class TradingSettings public long EarlySellOwnedLotsDelta { get; set; } public decimal EarlySellOwnedLotsMultiplier { get; set; } public DateTime LoadOperationsFrom { get; set; } + public PortfolioBalanceSettings? PortfolioBalance { get; set; } } \ No newline at end of file diff --git a/csharp/TraderBot/appsettings.PortfolioBalance.json b/csharp/TraderBot/appsettings.PortfolioBalance.json new file mode 100644 index 00000000..e15d6b23 --- /dev/null +++ b/csharp/TraderBot/appsettings.PortfolioBalance.json @@ -0,0 +1,54 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "InvestApiSettings": { + "AccessToken": "", + "AppName": "LinksPlatformPortfolioBalancer" + }, + "TradingSettings": { + "Instrument": "Etf", + "Ticker": "TMON@", + "CashCurrency": "rub", + "AccountIndex": -1, + "MinimumProfitSteps": -1, + "MarketOrderBookDepth": 10, + "MinimumMarketOrderSizeToChangeBuyPrice": 300000, + "MinimumMarketOrderSizeToChangeSellPrice": 0, + "MinimumMarketOrderSizeToBuy": 300000, + "MinimumMarketOrderSizeToSell": 0, + "MinimumTimeToBuy": "09:00:00", + "MaximumTimeToBuy": "18:30:00", + "EarlySellOwnedLotsDelta": 300000, + "EarlySellOwnedLotsMultiplier": 0, + "LoadOperationsFrom": "2025-03-01T00:00:01.3389860Z", + "PortfolioBalance": { + "Enabled": true, + "RebalanceThresholdPercent": 5.0, + "RebalanceCheckInterval": "00:10:00", + "AssetAllocations": [ + { + "AssetType": "Gold", + "Ticker": "TGLD", + "TargetPercent": 25.0, + "Instrument": "Etf" + }, + { + "AssetType": "USD", + "Ticker": "FXMM", + "TargetPercent": 25.0, + "Instrument": "Etf" + }, + { + "AssetType": "TCS Group stocks", + "Ticker": "TCSG", + "TargetPercent": 50.0, + "Instrument": "Shares" + } + ] + } + } +} \ No newline at end of file