diff --git a/.cursor/rules/fullstack.mdc b/.cursor/rules/fullstack.mdc index 0e18b99..e080172 100644 --- a/.cursor/rules/fullstack.mdc +++ b/.cursor/rules/fullstack.mdc @@ -103,7 +103,7 @@ Key Principles - Before creating new object or new method/function check if there a code that can be called - Most the time you will need to update multiple layer of code files. Make sure to reference all the method that you created when required - When you think its necessary update all the code from the database to the front end - - Do not update ManagingApi.ts, user will always do it with nswag + - Do not update ManagingApi.ts, once you made a change on the backend endpoint, execute the command to regenerate ManagingApi.ts on the frontend; cd src/Managing.Nswag && dotnet build - Do not reference new react library if a component already exist in mollecules or atoms Follow the official Microsoft documentation and ASP.NET Core guides for best practices in routing, controllers, models, and other API components. diff --git a/.github/workflows/caprover.yml b/.github/workflows/caprover.yml index bc90abf..c5e914d 100644 --- a/.github/workflows/caprover.yml +++ b/.github/workflows/caprover.yml @@ -24,13 +24,13 @@ jobs: - name: Preset Image Name run: echo "IMAGE_URL=$(echo ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:$(echo ${{ github.sha }} | cut -c1-7) | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - - name: Build and push Docker Image - uses: docker/build-push-action@v5 - with: - context: ./src/Managing.WebApp - file: ./src/Managing.WebApp/Dockerfile-web-ui-dev - push: true - tags: ${{ env.IMAGE_URL }} + # - name: Build and push Docker Image + # uses: docker/build-push-action@v5 + # with: + # context: ./src/Managing.WebApp + # file: ./src/Managing.WebApp/Dockerfile-web-ui-dev + # push: true + # tags: ${{ env.IMAGE_URL }} # - name: Create deploy.tar diff --git a/README-API.md b/README-API.md new file mode 100644 index 0000000..9a5ec55 --- /dev/null +++ b/README-API.md @@ -0,0 +1,355 @@ +# API Features + +## User Management (`UserController`) + +- **JWT Authentication**: Secure user authentication using Web3 signatures +- **Profile Management**: Update agent name, avatar URL, and Telegram channel +- **Current User Information**: Retrieve authenticated user details + +### Authentication Flow +1. Sign message with Web3 wallet +2. Submit signed message to receive JWT token +3. Use JWT token for all subsequent API calls + +### Endpoints +- `POST /User` - Create JWT token with Web3 signature +- `GET /User` - Get current user information +- `PUT /User/agent-name` - Update agent name +- `PUT /User/avatar` - Update avatar URL +- `PUT /User/telegram-channel` - Update Telegram channel + +## Account Management (`AccountController`) + +- **Account Creation**: Create trading accounts linked to user profiles +- **Account Retrieval**: Get all accounts or specific account by name +- **Balance Monitoring**: Retrieve real-time account balances +- **GMX Integration**: Get claimable fees summary from GMX contracts +- **Account Deletion**: Remove accounts from user profile + +### Endpoints +- `POST /Account` - Create new account +- `GET /Account/accounts` - Get all user accounts +- `GET /Account/balances` - Get account balances +- `GET /Account` - Get specific account by name +- `GET /Account/{name}/gmx-claimable-summary` - Get GMX claimable fees +- `DELETE /Account` - Delete account by name + +## Money Management (`MoneyManagementController`) + +- **Strategy Creation**: Define risk management parameters (StopLoss, TakeProfit, position sizing) +- **Strategy Updates**: Modify existing money management configurations +- **Strategy Retrieval**: Access all strategies or specific strategy by name +- **Strategy Deletion**: Remove unused money management configurations + +### Endpoints +- `POST /MoneyManagement` - Create or update money management strategy +- `GET /MoneyManagement/moneymanagements` - Get all money management strategies +- `GET /MoneyManagement` - Get specific money management by name +- `DELETE /MoneyManagement` - Delete money management strategy + +## Scenarios & Indicators (`ScenarioController`) + +### Scenarios +- **Build Scenarios**: Combine multiple indicators into trading strategies +- **Update Scenarios**: Modify existing scenario configurations +- **Scenario Management**: Create, retrieve, update, and delete scenarios +- **Loopback Period**: Configure historical data lookback for scenario evaluation + +### Indicators +- **Indicator Creation**: Build custom technical indicators with specific parameters +- **Indicator Updates**: Modify existing indicator configurations +- **Comprehensive Parameters**: Support for period, multiplier, fast/slow periods, signal periods, etc. +- **Indicator Types**: Support for various technical indicators (MACD, RSI, EMA, SuperTrend, etc.) + +### Scenario Endpoints +- `GET /Scenario` - Get all scenarios for user +- `POST /Scenario` - Create new scenario +- `PUT /Scenario` - Update existing scenario +- `DELETE /Scenario` - Delete scenario by name + +### Indicator Endpoints +- `GET /Scenario/indicator` - Get all indicators for user +- `POST /Scenario/indicator` - Create new indicator +- `PUT /Scenario/indicator` - Update existing indicator +- `DELETE /Scenario/indicator` - Delete indicator by name + +### Available Indicators + +| Indicator | Type | Description | Parameters | +|----------------------|------|------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------| +| RsiDivergence | Signal | Detects RSI divergence patterns where price and RSI move in opposite directions | Period (recommended: 4-6) | +| RsiDivergenceConfirm | Signal | Enhanced RSI divergence that waits for confirmation before triggering signals | Period (recommended: 4-6) | +| MacdCross | Signal | Triggers signals when MACD histogram crosses zero line | FastPeriods: 12, SlowPeriods: 26, SignalPeriods: 9 | +| EmaCross | Signal | Triggers signals when price crosses EMA line | Period (recommended: 200) | +| EmaTrend | Trend | Returns trend signals based on price position relative to EMA | Period (recommended: 200) | +| SuperTrend | Signal | Triggers signals when price crosses SuperTrend indicator | Period: 10, Multiplier: 3 | +| ChandelierExit | Signal | Exit strategy based on highest/lowest values over a period with ATR multiplier | Period: 22, Multiplier: 3 | +| StochRsiTrend | Trend | Trend signals based on Stochastic RSI levels (>80% = Short, <20% = Long) | Period (recommended: 22) | +| Stc | Signal | Schaff Trend Cycle - signals when crossing 75% (Short) or 25% (Long) levels | CyclePeriods, FastPeriods, SlowPeriods | +| ThreeWhiteSoldiers | Signal | Candlestick pattern recognition for bullish reversal signals | Lookback Period: 3 | +| LaggingStc | Signal | Enhanced STC with lagging mechanism for reduced false signals | CyclePeriods, FastPeriods, SlowPeriods | +| SuperTrendCrossEma | Signal | Combined indicator using both SuperTrend and EMA crossovers | SuperTrend Period/Multiplier + EMA Period | +| DualEmaCross | Signal | Dual EMA crossover system using fast and slow EMAs | FastPeriods, SlowPeriods | + +#### Signal Types +- **Signal**: Generates entry/exit signals for specific trading actions +- **Trend**: Provides directional trend information for position bias +- **Context**: Offers additional market context for decision making + +#### Parameter Guidelines +- **Period**: Number of candles for calculation (higher = smoother, lower = more responsive) +- **Multiplier**: ATR multiplier for volatility-based indicators (higher = wider bands) +- **Fast/Slow Periods**: For dual-line indicators, fast reacts quickly, slow provides stability +- **Cycle Periods**: For oscillators, defines the cycle length for calculations + +## Bot Management (`BotController`) + +### Core Bot Operations +- **Start Bots**: Deploy bots with comprehensive configuration +- **Stop/Restart Bots**: Individual or bulk bot control +- **Delete Bots**: Remove bots and their associated data +- **Configuration Updates**: Real-time bot parameter updates without restart + +### Advanced Bot Features +- **Manual Trading**: Open/close positions manually +- **Watch Mode**: Signal-only mode without actual trading +- **User Ownership**: Security validation ensuring users only control their own bots +- **Real-time Updates**: Live bot status and performance monitoring + +### Flexible Configuration Options + +**Scenario Configuration:** +- **Saved Scenarios**: Use `ScenarioName` to reference scenarios saved in database +- **Dynamic Scenarios**: Pass complete `Scenario` object directly without saving to database +- Dynamic scenarios are useful for testing custom configurations or one-time strategies + +**Money Management Configuration:** +- **Saved Strategies**: Use `MoneyManagementName` to reference saved money management strategies +- **Dynamic Strategies**: Pass complete `MoneyManagement` object directly without saving +- Allows for custom risk parameters without cluttering the saved strategies list + +### Endpoints +- `POST /Bot/Start` - Start a new bot +- `GET /Bot/Stop` - Stop specific bot +- `GET /Bot/Restart` - Restart specific bot +- `DELETE /Bot/Delete` - Delete bot +- `POST /Bot/stop-all` - Stop all user bots +- `POST /Bot/restart-all` - Restart all user bots +- `GET /Bot/ToggleIsForWatching` - Toggle watch mode +- `GET /Bot` - Get all active bots +- `POST /Bot/OpenPosition` - Manually open position +- `POST /Bot/ClosePosition` - Manually close position +- `PUT /Bot/UpdateConfig` - Update bot configuration + +### TradingBotConfig Documentation + +The `TradingBotConfig` class defines all configuration parameters for trading bots. Below are the available properties: + +#### Required Properties + +| Property | Type | Description | +|----------|------|-------------| +| `AccountName` | `string` | Name of the trading account to use | +| `MoneyManagement` | `MoneyManagement` | Risk management strategy configuration | +| `Ticker` | `Ticker` | Trading pair symbol (e.g., BTCUSDT) | +| `Timeframe` | `Timeframe` | Candle timeframe for analysis | +| `IsForWatchingOnly` | `bool` | If true, bot only sends signals without trading | +| `BotTradingBalance` | `decimal` | Initial trading balance for the bot | +| `BotType` | `BotType` | Type of trading bot behavior | +| `IsForBacktest` | `bool` | Whether this config is for backtesting | +| `CooldownPeriod` | `int` | Number of candles to wait before opening new position in same direction | +| `MaxLossStreak` | `int` | Maximum consecutive losses before requiring opposite direction signal (0 = no limit) | +| `FlipPosition` | `bool` | Whether the bot can flip positions | +| `Name` | `string` | Unique identifier/name for the bot | +| `FlipOnlyWhenInProfit` | `bool` | Only flip positions when current position is profitable (default: true) | + +#### Optional Properties + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `Scenario` | `Scenario` | Scenario object with strategies (takes precedence over ScenarioName) | null | +| `ScenarioName` | `string` | Name of scenario to load from database | null | +| `MaxPositionTimeHours` | `decimal?` | Maximum hours a position can stay open before auto-close | null | +| `CloseEarlyWhenProfitable` | `bool` | Close position early when profitable (requires MaxPositionTimeHours) | false | +| `UseSynthApi` | `bool` | Enable Synth API for probabilistic price forecasts | false | +| `UseForPositionSizing` | `bool` | Use Synth predictions for position sizing adjustments | true | +| `UseForSignalFiltering` | `bool` | Use Synth predictions for signal filtering | true | +| `UseForDynamicStopLoss` | `bool` | Use Synth predictions for dynamic stop-loss/take-profit | true | + +#### Advanced Configuration Details + +**Time-Based Position Management:** +- When `MaxPositionTimeHours` is set, positions are automatically closed after the specified time +- Only closes when position is in profit or at breakeven (never closes at a loss due to time) +- `CloseEarlyWhenProfitable` allows immediate closure when profitable instead of waiting full duration + +**Profit-Controlled Flipping:** +- `FlipOnlyWhenInProfit` ensures safer trading by only flipping profitable positions +- Helps prevent cascading losses in volatile markets + +**Synth API Integration:** +- `UseSynthApi` enables probabilistic price forecasting and risk assessment +- Sub-properties control specific Synth features (position sizing, signal filtering, dynamic stops) +- Provides AI-enhanced trading decisions when enabled + +**Scenario Configuration:** +- Either provide a `Scenario` object directly or use `ScenarioName` to load from database +- Direct `Scenario` objects are useful for testing custom configurations without saving +- `ScenarioName` is typically used for live trading with saved scenarios + +**Money Management Configuration:** +- Either provide a `MoneyManagement` object directly or use `MoneyManagementName` in the request +- Direct objects allow for one-time custom risk parameters +- Saved strategies via name reference are ideal for reusable configurations + +### Bot Configuration Parameters + +| Parameter | Description | Default | +|--------------------------|-------------------------------------------------------------------------------------------------------|---------| +| MaxPositionTimeHours | Maximum time (in hours) a position can stay open. Closes only when in profit/breakeven. Null = disabled | null | +| FlipOnlyWhenInProfit | Only flip positions when current position is profitable | true | +| CooldownPeriod | Number of candles to wait before opening new position in same direction | 10 | +| MaxLossStreak | Maximum consecutive losses before requiring opposite direction signal. 0 = no limit | 0 | +| CloseEarlyWhenProfitable | Close positions early when profitable (requires MaxPositionTimeHours) | false | +| BotTradingBalance | Initial trading balance for the bot | Required | + +### Bot Types + +The `BotType` enum in `TradingBotConfig` defines the following trading bot behaviors: + +| Type | Description | +|-------------|----------------------------------------------------------------------------------------| +| SimpleBot | Basic bot implementation for simple trading strategies | +| ScalpingBot | Opens positions and waits for cooldown period before opening new ones in same direction | +| FlippingBot | Advanced bot that can flip positions when opposite signals are triggered | + +#### Flipping Mode Configuration + +The flipping behavior is controlled by several `TradingBotConfig` properties: + +- **`BotType`**: Set to `FlippingBot` to enable position flipping capabilities +- **`FlipPosition`**: Boolean flag that enables/disables position flipping (automatically set based on BotType) +- **`FlipOnlyWhenInProfit`**: Safety feature that only allows flipping when current position is profitable (default: true) + +#### How Flipping Works + +**FlippingBot Behavior:** +1. Opens initial position based on scenario signals +2. Monitors for opposite direction signals from the same scenario +3. When opposite signal occurs: + - If `FlipOnlyWhenInProfit` is true: Only flips if current position is profitable + - If `FlipOnlyWhenInProfit` is false: Flips regardless of profit status +4. Closes current position and immediately opens new position in opposite direction +5. Continues this cycle for the duration of the bot's operation + +**ScalpingBot vs FlippingBot:** +- **ScalpingBot**: Opens position → Waits for exit signal → Closes → Cooldown → Opens new position +- **FlippingBot**: Opens position → Monitors for opposite signals → Flips immediately (no cooldown between flips) + +This configuration allows for more aggressive trading strategies while maintaining risk management through the profit-controlled flipping mechanism. + +## Backtesting (`BacktestController`) + +### Backtest Operations +- **Run Backtests**: Execute historical strategy testing with comprehensive parameters +- **Retrieve Results**: Access backtest results with complete configuration details +- **Delete Backtests**: Remove old backtest data +- **Save/Load**: Optional saving of backtest results for future reference + +### Enhanced Backtest Features +- **Complete Configuration**: Backtests include full `TradingBotConfig` for easy bot deployment +- **Advanced Parameters**: Support for all bot configuration parameters in backtests +- **Date Range Testing**: Flexible start/end date selection +- **Money Management Integration**: Test with specific money management strategies +- **Watch Mode**: Run backtests without executing trades (signal analysis only) + +### Flexible Configuration Options + +**Scenario Configuration for Backtests:** +- **Saved Scenarios**: Use `ScenarioName` to reference scenarios saved in database +- **Dynamic Scenarios**: Pass complete `Scenario` object directly in the backtest request +- Dynamic scenarios are perfect for testing custom indicator combinations without saving them +- Allows rapid experimentation with different strategy configurations + +**Money Management Configuration for Backtests:** +- **Saved Strategies**: Use `MoneyManagementName` to reference saved money management strategies +- **Dynamic Strategies**: Pass complete `MoneyManagement` object directly in the request +- Enables testing various risk parameters without cluttering saved strategies +- Ideal for optimization testing with different stop-loss/take-profit configurations + +### Endpoints +- `GET /Backtest` - Get all backtests for user +- `GET /Backtest/{id}` - Get specific backtest by ID +- `DELETE /Backtest` - Delete backtest by ID +- `POST /Backtest/Run` - Run new backtest + +### Backtest Parameters +- **Exchange**: Target exchange for historical data +- **Ticker**: Trading pair symbol +- **Timeframe**: Candle timeframe (1m, 5m, 15m, 1h, 4h, 1d, etc.) +- **Date Range**: Historical period for testing +- **Initial Balance**: Starting capital for backtest +- **Advanced Config**: All bot parameters (time limits, profit control, etc.) + +### RunBacktestRequest Structure + +The backtest request supports both saved and dynamic configurations: + +```json +{ + "Config": { + "ScenarioName": "MySavedScenario", // OR pass Scenario object directly + "Scenario": { /* full scenario object */ }, + "AccountName": "TestAccount", + "Ticker": "BTCUSDT", + "Timeframe": "OneHour", + // ... other TradingBotConfig properties + }, + "MoneyManagementName": "Conservative", // OR pass MoneyManagement object + "MoneyManagement": { /* full money management object */ }, + "StartDate": "2024-01-01T00:00:00Z", + "EndDate": "2024-02-01T00:00:00Z", + "Balance": 10000, + "Save": true +} +``` + +This flexibility allows for comprehensive strategy testing without requiring database saves for experimental configurations. + +## Market Data & Analytics (`DataController`) + +### Market Data +- **Ticker Information**: Available trading pairs with logos and metadata +- **Candle Data**: Historical OHLCV data for any timeframe +- **Real-time Updates**: Live price feeds via SignalR + +### Platform Analytics +- **Strategies Statistics**: Platform-wide bot performance metrics +- **Top Strategies**: Best performing strategies by ROI +- **User Analytics**: Individual user strategy performance +- **Platform Summary**: Comprehensive platform statistics with time filters +- **Agent Balances**: Historical balance tracking for users +- **Best Agents**: Leaderboard of top performing users + +### Endpoints +- `POST /Data/GetTickers` - Get available trading pairs with metadata +- `GET /Data/Spotlight` - Get market spotlight overview +- `GET /Data/GetCandles` - Get historical candle data +- `GET /Data/GetStrategiesStatistics` - Get platform-wide strategy statistics +- `GET /Data/GetTopStrategies` - Get top performing strategies +- `GET /Data/GetUserStrategies` - Get user's strategy details +- `GET /Data/GetUserStrategy` - Get specific user strategy +- `GET /Data/GetPlatformSummary` - Get comprehensive platform summary +- `GET /Data/GetAgentBalances` - Get agent balance history +- `GET /Data/GetBestAgents` - Get best performing agents leaderboard + +### Analytics Features +- **Time Filters**: 24H, 3D, 1W, 1M, 1Y, Total +- **Performance Metrics**: ROI, PnL, win rates, volume traded +- **Caching**: Optimized response times with intelligent caching +- **Real-time Updates**: Live performance tracking + +### Spotlight Data +- **Market Overview**: Comprehensive market analysis +- **Strategy Performance**: Cross-strategy performance comparison +- **Volume Analysis**: Trading volume statistics diff --git a/README.md b/README.md index 824d959..de0cc02 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ It contains bot management, backtesting, scenario management and money managemen ## Back-end -- .NET 7 +- .NET 8 - [SignalR](https://dotnet.microsoft.com/en-us/apps/aspnet/signalr) - [Discord.Net](https://github.com/discord-net/Discord.Net) - [CryptoExchange.Net](https://github.com/JKorf/CryptoExchange.Net) @@ -88,79 +88,209 @@ It contains bot management, backtesting, scenario management and money managemen --- -# Features +# API Features -## Privy +## User Management (`UserController`) -Front-end required: -- Sign message to get jwt -- Delegate embedded address -- Sign delegation -- Send >10 USDc and 5$ of ETH for txn fees -- Trigger to init address +- **JWT Authentication**: Secure user authentication using Web3 signatures +- **Profile Management**: Update agent name, avatar URL, and Telegram channel +- **Current User Information**: Retrieve authenticated user details -Backend actions: -- Approve GMX contracts addresses -- Approve USDc contract address +### Authentication Flow +1. Sign message with Web3 wallet +2. Submit signed message to receive JWT token +3. Use JWT token for all subsequent API calls +### Endpoints +- `POST /User` - Create JWT token with Web3 signature +- `GET /User` - Get current user information +- `PUT /User/agent-name` - Update agent name +- `PUT /User/avatar` - Update avatar URL +- `PUT /User/telegram-channel` - Update Telegram channel -## Money Management +## Account Management (`AccountController`) -- Create a defined money management for a given timeframe (StopLoss, TakeProfit, Amount to risk) -- Edit a money management configuration -- Delete a configuration +- **Account Creation**: Create trading accounts linked to user profiles +- **Account Retrieval**: Get all accounts or specific account by name +- **Balance Monitoring**: Retrieve real-time account balances +- **GMX Integration**: Get claimable fees summary from GMX contracts +- **Account Deletion**: Remove accounts from user profile -## Strategies +### Endpoints +- `POST /Account` - Create new account +- `GET /Account/accounts` - Get all user accounts +- `GET /Account/balances` - Get account balances +- `GET /Account` - Get specific account by name +- `GET /Account/{name}/gmx-claimable-summary` - Get GMX claimable fees +- `DELETE /Account` - Delete account by name -- Build a strategy -- Delete strategy +## Money Management (`MoneyManagementController`) -Strategies availables : +- **Strategy Creation**: Define risk management parameters (StopLoss, TakeProfit, position sizing) +- **Strategy Updates**: Modify existing money management configurations +- **Strategy Retrieval**: Access all strategies or specific strategy by name +- **Strategy Deletion**: Remove unused money management configurations -| Strategy | Description | Recommended values | -|----------------------|------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------| -| ChandelierExit | Triggers a SHORT signal when the previous candle is above the ChandelierExit, and the last candle closes below the ChandelierExit. | Period: 22, Multiplier: 3 | -| EMACross | Triggers a signal when the last candle crosses the EMA. | Period: 200 | -| EMATrend | Returns a Trend signal SHORT when the last candle is below the EMA, and a Trend LONG signal when StochRSI < 20%. | Period: 200 | -| MACDCross | Triggers a signal when EMAs cross. | FastPeriod: 12, SlowPeriods: 26, SignalPeriods: 9 | -| RSIDivergenceConfirm | First, detects a divergence and then triggers a signal when the divergence is confirmed. | Period: 4 for 6 | -| RSIDivergence | Triggers a signal when a divergence occurs on the period. | Period: 4 for 6 | -| STC | Returns a signal SHORT when the previous STC > 75% and the current STC <= 75%. | Period: 22 | -| StochRsiTrend | Returns a Trend signal SHORT when Stoch RSI > 80% and a Trend LONG signal when StochRSI < 20%. | Period: 22 | -| SuperTrend | Triggers a SHORT signal when the previous candle is above the super trend, and the last candle closes below the super trend. | Period: 10, Multiplier: 3 | -| ThreeWhiteSoldiers | Triggers a LONG signal when the Three White Soldiers pattern is identified. | Lookback Period: 3 | +### Endpoints +- `POST /MoneyManagement` - Create or update money management strategy +- `GET /MoneyManagement/moneymanagements` - Get all money management strategies +- `GET /MoneyManagement` - Get specific money management by name +- `DELETE /MoneyManagement` - Delete money management strategy -## Scenarios +## Scenarios & Indicators (`ScenarioController`) -- Build a scenario with multiple strategies -- Delete a scenario +### Scenarios +- **Build Scenarios**: Combine multiple indicators into trading strategies +- **Update Scenarios**: Modify existing scenario configurations +- **Scenario Management**: Create, retrieve, update, and delete scenarios +- **Loopback Period**: Configure historical data lookback for scenario evaluation -## Backtests +### Indicators +- **Indicator Creation**: Build custom technical indicators with specific parameters +- **Indicator Updates**: Modify existing indicator configurations +- **Comprehensive Parameters**: Support for period, multiplier, fast/slow periods, signal periods, etc. +- **Indicator Types**: Support for various technical indicators (MACD, RSI, EMA, SuperTrend, etc.) -The backtest system works with multiple required parameters : +### Scenario Endpoints +- `GET /Scenario` - Get all scenarios for user +- `POST /Scenario` - Create new scenario +- `PUT /Scenario` - Update existing scenario +- `DELETE /Scenario` - Delete scenario by name -- Exchange (Binance, Kraken, FTX) -- Ticker (ADAUSDT, BTCUSDT, etc..) -- Days : Since when did you want to start backtest. Should be a negative value -- ScenarioName -- Timeframe (OneDay, FifteenMinutes, etc..) -- BotType (ScalpingBot or FlippingBot) -- Initial balance -- **Advanced parameters**: All bot configuration parameters (time limits, profit-controlled flipping, etc.) -- **Smart bot deployment**: Deploy successful backtests as live bots with optimized settings -- **Enhanced UI**: Wider modals with organized 2-column parameter layouts +### Indicator Endpoints +- `GET /Scenario/indicator` - Get all indicators for user +- `POST /Scenario/indicator` - Create new indicator +- `PUT /Scenario/indicator` - Update existing indicator +- `DELETE /Scenario/indicator` - Delete indicator by name -## Bots +### Available Indicators -- Create and run a bot -- Stop / Restart a bot -- Delete a bot -- Stop all bots -- Set bot to watch only (send signal to discord instead of opening a new position) -- **Time-based position management**: Automatically close positions after maximum time limit (only when in profit/breakeven) -- **Advanced position flipping**: Control whether positions flip only when current position is profitable -- **Real-time configuration updates**: Update bot settings without restarting -- **Enhanced money management**: Smart money management selection with optimized settings from backtests +| Indicator | Type | Description | Parameters | +|----------------------|------|------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------| +| RsiDivergence | Signal | Detects RSI divergence patterns where price and RSI move in opposite directions | Period (recommended: 4-6) | +| RsiDivergenceConfirm | Signal | Enhanced RSI divergence that waits for confirmation before triggering signals | Period (recommended: 4-6) | +| MacdCross | Signal | Triggers signals when MACD histogram crosses zero line | FastPeriods: 12, SlowPeriods: 26, SignalPeriods: 9 | +| EmaCross | Signal | Triggers signals when price crosses EMA line | Period (recommended: 200) | +| EmaTrend | Trend | Returns trend signals based on price position relative to EMA | Period (recommended: 200) | +| SuperTrend | Signal | Triggers signals when price crosses SuperTrend indicator | Period: 10, Multiplier: 3 | +| ChandelierExit | Signal | Exit strategy based on highest/lowest values over a period with ATR multiplier | Period: 22, Multiplier: 3 | +| StochRsiTrend | Trend | Trend signals based on Stochastic RSI levels (>80% = Short, <20% = Long) | Period (recommended: 22) | +| Stc | Signal | Schaff Trend Cycle - signals when crossing 75% (Short) or 25% (Long) levels | CyclePeriods, FastPeriods, SlowPeriods | +| ThreeWhiteSoldiers | Signal | Candlestick pattern recognition for bullish reversal signals | Lookback Period: 3 | +| LaggingStc | Signal | Enhanced STC with lagging mechanism for reduced false signals | CyclePeriods, FastPeriods, SlowPeriods | +| SuperTrendCrossEma | Signal | Combined indicator using both SuperTrend and EMA crossovers | SuperTrend Period/Multiplier + EMA Period | +| DualEmaCross | Signal | Dual EMA crossover system using fast and slow EMAs | FastPeriods, SlowPeriods | + +#### Signal Types +- **Signal**: Generates entry/exit signals for specific trading actions +- **Trend**: Provides directional trend information for position bias +- **Context**: Offers additional market context for decision making + +#### Parameter Guidelines +- **Period**: Number of candles for calculation (higher = smoother, lower = more responsive) +- **Multiplier**: ATR multiplier for volatility-based indicators (higher = wider bands) +- **Fast/Slow Periods**: For dual-line indicators, fast reacts quickly, slow provides stability +- **Cycle Periods**: For oscillators, defines the cycle length for calculations + +## Bot Management (`BotController`) + +### Core Bot Operations +- **Start Bots**: Deploy bots with comprehensive configuration +- **Stop/Restart Bots**: Individual or bulk bot control +- **Delete Bots**: Remove bots and their associated data +- **Configuration Updates**: Real-time bot parameter updates without restart + +### Advanced Bot Features +- **Manual Trading**: Open/close positions manually +- **Watch Mode**: Signal-only mode without actual trading +- **User Ownership**: Security validation ensuring users only control their own bots +- **Real-time Updates**: Live bot status and performance monitoring + +### Flexible Configuration Options + +**Scenario Configuration:** +- **Saved Scenarios**: Use `ScenarioName` to reference scenarios saved in database +- **Dynamic Scenarios**: Pass complete `Scenario` object directly without saving to database +- Dynamic scenarios are useful for testing custom configurations or one-time strategies + +**Money Management Configuration:** +- **Saved Strategies**: Use `MoneyManagementName` to reference saved money management strategies +- **Dynamic Strategies**: Pass complete `MoneyManagement` object directly without saving +- Allows for custom risk parameters without cluttering the saved strategies list + +### Endpoints +- `POST /Bot/Start` - Start a new bot +- `GET /Bot/Stop` - Stop specific bot +- `GET /Bot/Restart` - Restart specific bot +- `DELETE /Bot/Delete` - Delete bot +- `POST /Bot/stop-all` - Stop all user bots +- `POST /Bot/restart-all` - Restart all user bots +- `GET /Bot/ToggleIsForWatching` - Toggle watch mode +- `GET /Bot` - Get all active bots +- `POST /Bot/OpenPosition` - Manually open position +- `POST /Bot/ClosePosition` - Manually close position +- `PUT /Bot/UpdateConfig` - Update bot configuration + +### TradingBotConfig Documentation + +The `TradingBotConfig` class defines all configuration parameters for trading bots. Below are the available properties: + +#### Required Properties + +| Property | Type | Description | +|----------|------|-------------| +| `AccountName` | `string` | Name of the trading account to use | +| `MoneyManagement` | `MoneyManagement` | Risk management strategy configuration | +| `Ticker` | `Ticker` | Trading pair symbol (e.g., BTCUSDT) | +| `Timeframe` | `Timeframe` | Candle timeframe for analysis | +| `IsForWatchingOnly` | `bool` | If true, bot only sends signals without trading | +| `BotTradingBalance` | `decimal` | Initial trading balance for the bot | +| `BotType` | `BotType` | Type of trading bot behavior | +| `IsForBacktest` | `bool` | Whether this config is for backtesting | +| `CooldownPeriod` | `int` | Number of candles to wait before opening new position in same direction | +| `MaxLossStreak` | `int` | Maximum consecutive losses before requiring opposite direction signal (0 = no limit) | +| `FlipPosition` | `bool` | Whether the bot can flip positions | +| `Name` | `string` | Unique identifier/name for the bot | +| `FlipOnlyWhenInProfit` | `bool` | Only flip positions when current position is profitable (default: true) | + +#### Optional Properties + +| Property | Type | Description | Default | +|----------|------|-------------|---------| +| `Scenario` | `Scenario` | Scenario object with strategies (takes precedence over ScenarioName) | null | +| `ScenarioName` | `string` | Name of scenario to load from database | null | +| `MaxPositionTimeHours` | `decimal?` | Maximum hours a position can stay open before auto-close | null | +| `CloseEarlyWhenProfitable` | `bool` | Close position early when profitable (requires MaxPositionTimeHours) | false | +| `UseSynthApi` | `bool` | Enable Synth API for probabilistic price forecasts | false | +| `UseForPositionSizing` | `bool` | Use Synth predictions for position sizing adjustments | true | +| `UseForSignalFiltering` | `bool` | Use Synth predictions for signal filtering | true | +| `UseForDynamicStopLoss` | `bool` | Use Synth predictions for dynamic stop-loss/take-profit | true | + +#### Advanced Configuration Details + +**Time-Based Position Management:** +- When `MaxPositionTimeHours` is set, positions are automatically closed after the specified time +- Only closes when position is in profit or at breakeven (never closes at a loss due to time) +- `CloseEarlyWhenProfitable` allows immediate closure when profitable instead of waiting full duration + +**Profit-Controlled Flipping:** +- `FlipOnlyWhenInProfit` ensures safer trading by only flipping profitable positions +- Helps prevent cascading losses in volatile markets + +**Synth API Integration:** +- `UseSynthApi` enables probabilistic price forecasting and risk assessment +- Sub-properties control specific Synth features (position sizing, signal filtering, dynamic stops) +- Provides AI-enhanced trading decisions when enabled + +**Scenario Configuration:** +- Either provide a `Scenario` object directly or use `ScenarioName` to load from database +- Direct `Scenario` objects are useful for testing custom configurations without saving +- `ScenarioName` is typically used for live trading with saved scenarios + +**Money Management Configuration:** +- Either provide a `MoneyManagement` object directly or use `MoneyManagementName` in the request +- Direct objects allow for one-time custom risk parameters +- Saved strategies via name reference are ideal for reusable configurations ### Bot Configuration Parameters @@ -170,13 +300,149 @@ The backtest system works with multiple required parameters : | FlipOnlyWhenInProfit | Only flip positions when current position is profitable | true | | CooldownPeriod | Number of candles to wait before opening new position in same direction | 10 | | MaxLossStreak | Maximum consecutive losses before requiring opposite direction signal. 0 = no limit | 0 | +| CloseEarlyWhenProfitable | Close positions early when profitable (requires MaxPositionTimeHours) | false | +| BotTradingBalance | Initial trading balance for the bot | Required | -Bot types availables : +### Bot Types + +The `BotType` enum in `TradingBotConfig` defines the following trading bot behaviors: | Type | Description | |-------------|----------------------------------------------------------------------------------------| -| ScalpingBot | This bot will open position and wait before opening a new one | -| FlippingBot | The flipping bot flipping the position only when a strategy trigger an opposite signal | +| SimpleBot | Basic bot implementation for simple trading strategies | +| ScalpingBot | Opens positions and waits for cooldown period before opening new ones in same direction | +| FlippingBot | Advanced bot that can flip positions when opposite signals are triggered | + +#### Flipping Mode Configuration + +The flipping behavior is controlled by several `TradingBotConfig` properties: + +- **`BotType`**: Set to `FlippingBot` to enable position flipping capabilities +- **`FlipPosition`**: Boolean flag that enables/disables position flipping (automatically set based on BotType) +- **`FlipOnlyWhenInProfit`**: Safety feature that only allows flipping when current position is profitable (default: true) + +#### How Flipping Works + +**FlippingBot Behavior:** +1. Opens initial position based on scenario signals +2. Monitors for opposite direction signals from the same scenario +3. When opposite signal occurs: + - If `FlipOnlyWhenInProfit` is true: Only flips if current position is profitable + - If `FlipOnlyWhenInProfit` is false: Flips regardless of profit status +4. Closes current position and immediately opens new position in opposite direction +5. Continues this cycle for the duration of the bot's operation + +**ScalpingBot vs FlippingBot:** +- **ScalpingBot**: Opens position → Waits for exit signal → Closes → Cooldown → Opens new position +- **FlippingBot**: Opens position → Monitors for opposite signals → Flips immediately (no cooldown between flips) + +This configuration allows for more aggressive trading strategies while maintaining risk management through the profit-controlled flipping mechanism. + +## Backtesting (`BacktestController`) + +### Backtest Operations +- **Run Backtests**: Execute historical strategy testing with comprehensive parameters +- **Retrieve Results**: Access backtest results with complete configuration details +- **Delete Backtests**: Remove old backtest data +- **Save/Load**: Optional saving of backtest results for future reference + +### Enhanced Backtest Features +- **Complete Configuration**: Backtests include full `TradingBotConfig` for easy bot deployment +- **Advanced Parameters**: Support for all bot configuration parameters in backtests +- **Date Range Testing**: Flexible start/end date selection +- **Money Management Integration**: Test with specific money management strategies +- **Watch Mode**: Run backtests without executing trades (signal analysis only) + +### Flexible Configuration Options + +**Scenario Configuration for Backtests:** +- **Saved Scenarios**: Use `ScenarioName` to reference scenarios saved in database +- **Dynamic Scenarios**: Pass complete `Scenario` object directly in the backtest request +- Dynamic scenarios are perfect for testing custom indicator combinations without saving them +- Allows rapid experimentation with different strategy configurations + +**Money Management Configuration for Backtests:** +- **Saved Strategies**: Use `MoneyManagementName` to reference saved money management strategies +- **Dynamic Strategies**: Pass complete `MoneyManagement` object directly in the request +- Enables testing various risk parameters without cluttering saved strategies +- Ideal for optimization testing with different stop-loss/take-profit configurations + +### Endpoints +- `GET /Backtest` - Get all backtests for user +- `GET /Backtest/{id}` - Get specific backtest by ID +- `DELETE /Backtest` - Delete backtest by ID +- `POST /Backtest/Run` - Run new backtest + +### Backtest Parameters +- **Exchange**: Target exchange for historical data +- **Ticker**: Trading pair symbol +- **Timeframe**: Candle timeframe (1m, 5m, 15m, 1h, 4h, 1d, etc.) +- **Date Range**: Historical period for testing +- **Initial Balance**: Starting capital for backtest +- **Advanced Config**: All bot parameters (time limits, profit control, etc.) + +### RunBacktestRequest Structure + +The backtest request supports both saved and dynamic configurations: + +```json +{ + "Config": { + "ScenarioName": "MySavedScenario", // OR pass Scenario object directly + "Scenario": { /* full scenario object */ }, + "AccountName": "TestAccount", + "Ticker": "BTCUSDT", + "Timeframe": "OneHour", + // ... other TradingBotConfig properties + }, + "MoneyManagementName": "Conservative", // OR pass MoneyManagement object + "MoneyManagement": { /* full money management object */ }, + "StartDate": "2024-01-01T00:00:00Z", + "EndDate": "2024-02-01T00:00:00Z", + "Balance": 10000, + "Save": true +} +``` + +This flexibility allows for comprehensive strategy testing without requiring database saves for experimental configurations. + +## Market Data & Analytics (`DataController`) + +### Market Data +- **Ticker Information**: Available trading pairs with logos and metadata +- **Candle Data**: Historical OHLCV data for any timeframe +- **Real-time Updates**: Live price feeds via SignalR + +### Platform Analytics +- **Strategies Statistics**: Platform-wide bot performance metrics +- **Top Strategies**: Best performing strategies by ROI +- **User Analytics**: Individual user strategy performance +- **Platform Summary**: Comprehensive platform statistics with time filters +- **Agent Balances**: Historical balance tracking for users +- **Best Agents**: Leaderboard of top performing users + +### Endpoints +- `POST /Data/GetTickers` - Get available trading pairs with metadata +- `GET /Data/Spotlight` - Get market spotlight overview +- `GET /Data/GetCandles` - Get historical candle data +- `GET /Data/GetStrategiesStatistics` - Get platform-wide strategy statistics +- `GET /Data/GetTopStrategies` - Get top performing strategies +- `GET /Data/GetUserStrategies` - Get user's strategy details +- `GET /Data/GetUserStrategy` - Get specific user strategy +- `GET /Data/GetPlatformSummary` - Get comprehensive platform summary +- `GET /Data/GetAgentBalances` - Get agent balance history +- `GET /Data/GetBestAgents` - Get best performing agents leaderboard + +### Analytics Features +- **Time Filters**: 24H, 3D, 1W, 1M, 1Y, Total +- **Performance Metrics**: ROI, PnL, win rates, volume traded +- **Caching**: Optimized response times with intelligent caching +- **Real-time Updates**: Live performance tracking + +### Spotlight Data +- **Market Overview**: Comprehensive market analysis +- **Strategy Performance**: Cross-strategy performance comparison +- **Volume Analysis**: Trading volume statistics ## Privy Integration diff --git a/SYNTH_API_INTEGRATION.md b/SYNTH_API_INTEGRATION.md new file mode 100644 index 0000000..c951ca9 --- /dev/null +++ b/SYNTH_API_INTEGRATION.md @@ -0,0 +1,494 @@ +# Synth API Integration - Technical Documentation + +## Overview + +The Synth API integration provides probabilistic price forecasting capabilities for trading signal validation using AI-powered predictions from Mode Network's Synth Subnet. The system employs a sophisticated **probability-weighted confidence calculation** that prioritizes actual trading probabilities over theoretical position sizing metrics. + +## Implementation Architecture + +### Service Overview + +The Synth API integration is implemented through the `SynthPredictionService` class, which serves as the central orchestrator for all probabilistic forecasting operations. The service provides MongoDB-backed caching, comprehensive signal validation, and real-time risk assessment capabilities. + +#### Key Dependencies +- **ISynthApiClient**: Handles communication with Mode Network's Synth Subnet API +- **ISynthRepository**: Manages MongoDB persistence for prediction caching and leaderboard data +- **ILogger**: Provides detailed logging for debugging and monitoring + +#### Supported Assets +Currently supports BTC and ETH with extensible architecture for additional cryptocurrencies. + +### Timeframe-Specific Configuration + +The system automatically optimizes API calls based on trading timeframes due to Synth API limitations (only supports 5-minute and 24-hour increments): + +**Short timeframes (1m, 5m, 15m, 30m):** +- Uses 5-minute time increments with 4-hour prediction horizons +- Updates cache every 2 minutes for rapid market changes + +**Medium timeframes (1h):** +- Uses 5-minute time increments with 12-hour prediction horizons +- Updates cache every 5 minutes for balanced responsiveness + +**Long timeframes (4h, 1d):** +- Uses 24-hour time increments with 48-hour prediction horizons +- Updates cache every 15 minutes for stability + +### Caching Strategy + +#### Two-Tier MongoDB Caching System + +**1. Leaderboard Caching** +- Caches top miner rankings to avoid repeated API calls +- Separate caching for live vs backtest scenarios +- Uses structured cache keys incorporating asset, time increment, and scenario type + +**2. Individual Prediction Caching** +- Stores detailed price path predictions per miner +- Enables partial cache hits (fetch only missing miners) +- Automatic cache invalidation based on timeframe-specific durations + +#### Intelligent Cache Management +The system optimizes performance by identifying which miner predictions are already cached and fetching only the missing data, then combining cached and fresh predictions for complete analysis. + +### Signal Validation Workflow + +#### Primary Validation Method: `ValidateSignalAsync` + +**Input Processing:** +1. Extracts money management settings (SL/TP percentages) +2. Calculates dynamic price thresholds based on position direction +3. Determines appropriate time horizon (24 hours for signal validation) + +**Price Threshold Calculation:** +For LONG positions, the system calculates stop loss prices below current price and take profit prices above. For SHORT positions, this is reversed - stop loss prices above current price and take profit prices below. + +**Probability Analysis:** +- Retrieves cached or fresh predictions from top 10 miners +- Calculates probability of reaching each price threshold +- Analyzes directional movement based on position type + +**Comprehensive Scoring:** +- Calculates Expected Monetary Value (EMV) +- Applies Kelly Criterion analysis +- Computes Expected Utility Theory metrics +- Generates confidence score using multi-factor algorithm + +### Multi-Factor Confidence Scoring + +#### Three-Component Scoring System + +**1. Configuration-Aware Scoring (50% weight)** +- Adverse probability pressure analysis +- Kelly fraction alignment with risk tolerance +- Risk aversion compatibility assessment +- Configuration consistency bonuses + +**2. Threshold Alignment Scoring (30% weight)** +- Kelly minimum/maximum threshold compliance +- Favorable probability threshold achievement +- Kelly multiplier philosophy alignment + +**3. Probability Scoring (20% weight)** +- Take Profit probability assessment +- Stop Loss risk evaluation +- TP/SL ratio analysis +- Win/Loss ratio consideration +- Probability dominance bonuses + +#### Conditional Pass-Through System + +For signals exceeding adverse probability thresholds, the system evaluates **8 redeeming qualities**: + +1. Significant Kelly edge (>2x minimum threshold AND >5%) +2. Meaningful Expected Monetary Value (>$100) +3. Excellent TP/SL ratio (≥2.0) +4. Positive Expected Utility (>1.0) +5. TP probability dominance (TP > SL × 1.5) +6. Moderate threshold breach (SL/Threshold ≤ 1.25) +7. Strong Kelly assessment indicators +8. Favorable Win/Loss ratio (≥2.0) + +**Pass-through requires 75% of factors to be positive**, resulting in constrained LOW confidence. + +### Risk Assessment Implementation + +#### Position Risk Assessment: `AssessPositionRiskAsync` + +**Pre-Position Risk Check:** +- Estimates liquidation price based on money management +- Calculates 24-hour liquidation probability +- Blocks positions exceeding `MaxLiquidationProbability` (default 10%) + +#### Ongoing Position Monitoring: `MonitorPositionRiskAsync` + +**Real-time Risk Monitoring:** +- 6-hour liquidation probability assessment +- Warning threshold at 20% risk +- Auto-close threshold at 50% risk (if dynamic SL enabled) +- Generates actionable risk notifications + +### Backtest vs Live Trading Support + +#### Historical Data Integration +For backtesting scenarios, the system retrieves historical leaderboard data from 30 minutes before the signal date and fetches corresponding historical predictions. This ensures accurate simulation of what information would have been available at the time of the original signal. + +#### Cache Separation +- Separate cache keys for backtest vs live scenarios +- Historical leaderboard and prediction storage +- Automatic fallback to current data if historical unavailable + +### Probability Calculation Engine + +#### Path Aggregation Algorithm +The probability calculation engine aggregates simulation paths from all selected miners and analyzes each path for target price crossings within the specified time horizon. The system performs directional analysis based on position type (LONG/SHORT) and calculates the final probability as the percentage of paths that cross the target price. + +**Process:** +1. Aggregates simulation paths from all selected miners +2. Analyzes each path for target price crossings within time horizon +3. Directional analysis based on position type (LONG/SHORT) +4. Calculates probability as: (Paths Crossing Target) / (Total Paths) + +### Performance Optimization Features + +#### Intelligent API Usage +- Partial cache hits reduce API calls by up to 90% +- Timeframe-specific configurations minimize unnecessary data +- Batch processing for multiple threshold calculations + +#### Memory Management +- MongoDB persistence prevents RAM saturation +- Automatic cleanup of expired cache data +- Efficient data structures for large prediction datasets + +#### Error Handling & Resilience +- Graceful degradation on API failures +- Fallback confidence scoring when predictions unavailable +- Comprehensive error logging with context preservation + +## Core Calculation Methodology + +### Confidence Score Calculation + +The system uses a weighted composite scoring approach: + +**Standard Signals (within adverse threshold):** +``` +Confidence Score = (ConfigurationScore × 50%) + (ThresholdAlignmentScore × 30%) + (ProbabilityScore × 20%) +``` + +**Over-Threshold Signals (conditional pass-through):** +``` +Constrained Score = (KellyScore × 25%) + (TpSlScore × 25%) + (EMVScore × 20%) + (UtilityScore × 15%) + (RiskPenalty × 15%) +Final Score = min(Constrained Score × 0.75, LowThreshold + 0.05) +``` + +### Sigmoid Scoring Functions + +All scoring components use continuous sigmoid functions for smooth transitions: + +**MapToScore (higher is better):** +``` +Score = 1 / (1 + e^(-steepness × (value - midpoint))) +``` + +**MapToInverseScore (lower is better):** +``` +Score = 1 / (1 + e^(steepness × (value - midpoint))) +``` + +## Risk Management Parameters + +### Core Signal Filtering Parameters + +| Parameter | Range | Default | Purpose | +|-----------|-------|---------|---------| +| `AdverseProbabilityThreshold` | 5% - 50% | 20% | Maximum acceptable Stop Loss probability | +| `FavorableProbabilityThreshold` | 10% - 70% | 30% | Minimum required Take Profit probability | +| `SignalValidationTimeHorizonHours` | 1 - 168 | 24 | Time horizon for probability calculations | + +### Expected Utility Theory Parameters + +| Parameter | Range | Default | Purpose | +|-----------|-------|---------|---------| +| `RiskAversion` | 0.1 - 5.0 | 1.0 | Portfolio utility risk aversion coefficient | +| `UseExpectedUtility` | boolean | true | Enable Expected Utility calculations | + +### Kelly Criterion Parameters + +| Parameter | Range | Default | Purpose | +|-----------|-------|---------|---------| +| `KellyMinimumThreshold` | 0.5% - 10% | 1% | Minimum Kelly fraction for favorable signals | +| `KellyMaximumCap` | 5% - 50% | 25% | Maximum Kelly fraction (safety cap) | +| `KellyFractionalMultiplier` | 10% - 100% | 100% | Kelly sizing multiplier (fractional Kelly) | +| `UseKellyCriterion` | boolean | true | Enable Kelly position sizing recommendations | + +### Position Management Parameters + +| Parameter | Range | Default | Purpose | +|-----------|-------|---------|---------| +| `MaxLiquidationProbability` | 5% - 30% | 10% | Maximum acceptable liquidation risk | +| `PositionMonitoringTimeHorizonHours` | 1 - 48 | 6 | Monitoring frequency for open positions | +| `PositionWarningThreshold` | 10% - 40% | 20% | Risk level for position warnings | +| `PositionAutoCloseThreshold` | 30% - 80% | 50% | Risk level for automatic position closure | + +## Risk Tolerance Profiles + +### Conservative Profile +``` +AdverseProbabilityThreshold = 15% // Stricter blocking threshold +FavorableProbabilityThreshold = 40% // Higher TP requirements +RiskAversion = 2.0 // More risk-averse utility calculation +KellyMinimumThreshold = 2% // Higher Kelly minimum +KellyMaximumCap = 15% // Lower Kelly maximum +KellyFractionalMultiplier = 50% // Half-Kelly sizing +MaxLiquidationProbability = 8% // Stricter position risk +PositionWarningThreshold = 15% // Earlier warnings +PositionAutoCloseThreshold = 35% // Earlier auto-close +``` + +**Confidence Thresholds:** High ≥80%, Medium ≥60%, Low ≥40% + +### Moderate Profile (Default) +``` +AdverseProbabilityThreshold = 20% // Balanced blocking threshold +FavorableProbabilityThreshold = 30% // Reasonable TP requirements +RiskAversion = 1.0 // Neutral utility calculation +KellyMinimumThreshold = 1% // Standard Kelly minimum +KellyMaximumCap = 25% // Standard Kelly maximum +KellyFractionalMultiplier = 100% // Full Kelly sizing +MaxLiquidationProbability = 10% // Standard position risk +PositionWarningThreshold = 20% // Standard warnings +PositionAutoCloseThreshold = 50% // Standard auto-close +``` + +**Confidence Thresholds:** High ≥75%, Medium ≥55%, Low ≥35% + +### Aggressive Profile +``` +AdverseProbabilityThreshold = 30% // Permissive blocking threshold +FavorableProbabilityThreshold = 25% // Lower TP requirements +RiskAversion = 0.5 // Risk-seeking utility calculation +KellyMinimumThreshold = 0.5% // Lower Kelly minimum +KellyMaximumCap = 40% // Higher Kelly maximum +KellyFractionalMultiplier = 100% // Full Kelly sizing +MaxLiquidationProbability = 15% // Higher position risk tolerance +PositionWarningThreshold = 30% // Later warnings +PositionAutoCloseThreshold = 70% // Later auto-close +``` + +**Confidence Thresholds:** High ≥70%, Medium ≥45%, Low ≥25% + +## Detailed Scoring Algorithms + +### 1. Configuration-Aware Scoring (50% Weight) + +**Adverse Probability Pressure (30% of ConfigScore):** +- Conservative: Penalizes at 40% of threshold, steepness 6.0 +- Moderate: Penalizes at 60% of threshold, steepness 4.0 +- Aggressive: Penalizes at 80% of threshold, steepness 3.0 + +**Kelly Alignment Scoring (25% of ConfigScore):** +- Conservative: Smooth curves for Kelly 2%-8%, gradual penalty >15% +- Moderate: Broad scoring range, penalty for <1% or >15% +- Aggressive: Rewards higher Kelly, less penalty for extremes + +**Risk Aversion Alignment (25% of ConfigScore):** +- Conservative: Lenient utility requirements (midpoint -0.2, steepness 3.0) +- Moderate: Balanced utility assessment (midpoint 0.0, steepness 5.0) +- Aggressive: Higher utility demands (midpoint 0.2, steepness 8.0) + +**Configuration Consistency Bonus (20% of ConfigScore):** +- Conservative: Bonus for SL <12%, Kelly <6%, TP/SL >1.5 +- Moderate: Bonus for SL <16%, Kelly >3%, TP/SL >1.5 +- Aggressive: Bonus for Kelly >8%, TP/SL >2.0, positive EMV + +### 2. Threshold Alignment Scoring (30% Weight) + +**Kelly Threshold Compliance (50% of ThresholdScore):** +- Kelly Minimum Score: Sigmoid scoring if below minimum threshold +- Kelly Maximum Score: Inverse sigmoid penalty if above maximum cap + +**Favorable Probability Threshold (30% of ThresholdScore):** +- Direct comparison to FavorableProbabilityThreshold +- Sigmoid scoring for partial compliance + +**Kelly Multiplier Philosophy Alignment (20% of ThresholdScore):** +- Fractional Kelly (<100%): Rewards lower SL probability (midpoint 15%, steepness 8.0) +- Full Kelly (≥100%): Rewards higher TP/SL ratio (midpoint 1.8, steepness 5.0) + +### 3. Probability Scoring (20% Weight) + +**Take Profit Probability (30% of ProbScore):** +- Midpoint: 60% TP probability +- Steepness: 4.0 for sensitivity around realistic ranges + +**Stop Loss Risk Assessment (30% of ProbScore):** +- Uses 75% of adverse threshold as reference point +- Inverse scoring (lower SL probability = higher score) +- Steepness: 8.0 for sharp penalty above threshold + +**TP/SL Ratio Assessment (25% of ProbScore):** +- Midpoint: 1.5 (realistic good ratio) +- Steepness: 3.0 for balanced sensitivity + +**Win/Loss Ratio (10% of ProbScore):** +- Midpoint: 1.5 (favorable ratio) +- Steepness: 3.0 + +**Probability Dominance Bonus (5% of ProbScore):** +- TP/SL ratio scaled by factor of 2 +- Binary scoring for extreme cases + +## Conditional Pass-Through System + +### Redeeming Qualities Assessment (8 Factors) + +Signals exceeding the adverse probability threshold can still receive LOW confidence if meeting **75% of these criteria:** + +1. **Significant Kelly Edge:** Kelly > (2 × KellyMinimumThreshold) AND Kelly > 5% +2. **Meaningful Expected Value:** EMV > $100 +3. **Excellent TP/SL Ratio:** TP/SL ≥ 2.0 +4. **Positive Expected Utility:** ExpectedUtility > 1.0 +5. **TP Probability Dominance:** TP > (SL × 1.5) +6. **Moderate Threshold Breach:** SL/Threshold ≤ 1.25 +7. **Strong Kelly Assessment:** Contains "Strong", "Exceptional", or "Extraordinary" +8. **Favorable Win/Loss Ratio:** Win/Loss ≥ 2.0 + +### Constrained Scoring for Pass-Through + +**Component Weights:** +- Kelly Score: 25% (capped at 0.8) +- TP/SL Score: 25% (capped at 0.7) +- EMV Score: 20% (binary: >0 = 0.6, ≤0 = 0.2) +- Utility Score: 15% (binary: >0 = 0.5, ≤0 = 0.2) +- Risk Penalty: 15% (inverse sigmoid on threshold overage) + +**Final Score Calculation:** +``` +Constrained Score × 0.75 penalty, capped at (LowThreshold + 0.05) +``` + +## API Time Horizon Configuration + +### Timeframe-Based Settings + +**Short Timeframes (1m, 5m, 15m, 30m):** +- Time Increment: 300 seconds (5 minutes) +- Default Horizon: 14,400 seconds (4 hours) +- Cache Duration: 2 minutes + +**Medium Timeframes (1h):** +- Time Increment: 300 seconds (5 minutes) +- Default Horizon: 43,200 seconds (12 hours) +- Cache Duration: 5 minutes + +**Long Timeframes (4h, 1d):** +- Time Increment: 86,400 seconds (24 hours) +- Default Horizon: 172,800 seconds (48 hours) +- Cache Duration: 15 minutes + +### Price Threshold Calculations + +**LONG Positions:** +``` +Stop Loss Price = Current Price × (1 - StopLossPercentage) +Take Profit Price = Current Price × (1 + TakeProfitPercentage) +``` + +**SHORT Positions:** +``` +Stop Loss Price = Current Price × (1 + StopLossPercentage) +Take Profit Price = Current Price × (1 - TakeProfitPercentage) +``` + +### Probability Calculation Method + +The system aggregates simulation paths from top-performing miners and calculates the percentage of paths that cross the target price within the specified time horizon: + +``` +Probability = (Paths Crossing Target) / (Total Simulation Paths) +``` + +**Path Analysis:** +- Uses top 10 miners by default (configurable) +- Aggregates all simulation paths from selected miners +- Directional analysis based on position type (LONG/SHORT) +- Time-bounded evaluation within specified horizon + +## Parameter Optimization Guidelines + +### For Different Market Conditions + +**High Volatility Markets:** +- Increase AdverseProbabilityThreshold (25-35%) +- Decrease FavorableProbabilityThreshold (20-25%) +- Lower KellyMaximumCap (15-20%) +- Shorter SignalValidationTimeHorizonHours (12-18) + +**Low Volatility Markets:** +- Decrease AdverseProbabilityThreshold (15-20%) +- Increase FavorableProbabilityThreshold (35-45%) +- Higher KellyMaximumCap (25-35%) +- Longer SignalValidationTimeHorizonHours (24-48) + +**Trending Markets:** +- Use Aggressive profile +- Lower RiskAversion (0.5-0.8) +- Higher KellyFractionalMultiplier (85-100%) + +**Ranging Markets:** +- Use Conservative profile +- Higher RiskAversion (1.5-2.5) +- Lower KellyFractionalMultiplier (50-75%) + +### Money Management Integration Impact + +**Tight SL/TP Ranges (< 2%):** +- Use Aggressive thresholds (30-35% adverse) +- Higher probability granularity +- More frequent MEDIUM/LOW confidence levels + +**Standard SL/TP Ranges (2-5%):** +- Use Moderate thresholds (20-25% adverse) +- Balanced confidence distribution +- Optimal for most trading strategies + +**Wide SL/TP Ranges (> 5%):** +- Use Conservative thresholds (15-20% adverse) +- Lower probability values +- More reliable confidence signals + +## Performance Monitoring + +### Key Metrics to Track + +**Confidence Distribution:** +- Target: 30% High, 40% Medium, 25% Low, 5% None +- Monitor for uniform distribution (indicates poor calibration) + +**Kelly Capping Frequency:** +- Track percentage of signals with capped Kelly fractions +- High capping (>50%) may indicate overly aggressive signals + +**Expected Utility Trends:** +- Monitor portfolio-level utility accumulation +- Negative trends indicate poor risk management + +**Pass-Through Analysis:** +- Track conditional pass-through frequency +- Monitor success rate of LOW confidence signals + +### Calibration Validation + +**Probability Accuracy:** +- Compare predicted vs. actual outcome frequencies +- Calibrate thresholds based on historical performance + +**Confidence Effectiveness:** +- Track win rates by confidence level +- Validate that High > Medium > Low > None in performance + +**Risk Assessment Accuracy:** +- Monitor false positive/negative rates for liquidation predictions +- Adjust MaxLiquidationProbability based on actual risk events \ No newline at end of file diff --git a/src/Managing.Api/Controllers/BacktestController.cs b/src/Managing.Api/Controllers/BacktestController.cs index 8138f6c..04cbc7f 100644 --- a/src/Managing.Api/Controllers/BacktestController.cs +++ b/src/Managing.Api/Controllers/BacktestController.cs @@ -1,9 +1,12 @@ -using Managing.Application.Abstractions; +using Managing.Api.Models.Requests; +using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Hubs; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; +using Managing.Domain.Scenarios; +using Managing.Domain.Strategies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -123,7 +126,7 @@ public class BacktestController : BaseController return BadRequest("Either scenario name or scenario object is required"); } - if (string.IsNullOrEmpty(request.MoneyManagementName) && request.MoneyManagement == null) + if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) { return BadRequest("Either money management name or money management object is required"); } @@ -136,26 +139,58 @@ public class BacktestController : BaseController // Get money management MoneyManagement moneyManagement; - if (!string.IsNullOrEmpty(request.MoneyManagementName)) + if (!string.IsNullOrEmpty(request.Config.MoneyManagementName)) { - moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); + moneyManagement = + await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName); if (moneyManagement == null) return BadRequest("Money management not found"); } else { - moneyManagement = request.MoneyManagement; + moneyManagement = Map(request.Config.MoneyManagement); moneyManagement?.FormatPercentage(); } - // Update config with money management - TradingBot will handle scenario loading + // Handle scenario - either from ScenarioRequest or ScenarioName + Scenario scenario = null; + if (request.Config.Scenario != null) + { + // Convert ScenarioRequest to Scenario domain object + scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod) + { + User = user + }; + + // Convert IndicatorRequest objects to Indicator domain objects + foreach (var indicatorRequest in request.Config.Scenario.Indicators) + { + var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) + { + SignalType = indicatorRequest.SignalType, + MinimumHistory = indicatorRequest.MinimumHistory, + Period = indicatorRequest.Period, + FastPeriods = indicatorRequest.FastPeriods, + SlowPeriods = indicatorRequest.SlowPeriods, + SignalPeriods = indicatorRequest.SignalPeriods, + Multiplier = indicatorRequest.Multiplier, + SmoothPeriods = indicatorRequest.SmoothPeriods, + StochPeriods = indicatorRequest.StochPeriods, + CyclePeriods = indicatorRequest.CyclePeriods, + User = user + }; + scenario.AddIndicator(indicator); + } + } + + // Convert TradingBotConfigRequest to TradingBotConfig for backtest var backtestConfig = new TradingBotConfig { AccountName = request.Config.AccountName, MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, ScenarioName = request.Config.ScenarioName, - Scenario = request.Config.Scenario, + Scenario = scenario, // Use the converted scenario object Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.WatchOnly, BotTradingBalance = request.Balance, @@ -165,10 +200,14 @@ public class BacktestController : BaseController MaxLossStreak = request.Config.MaxLossStreak, MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, - FlipPosition = request.Config.FlipPosition, + FlipPosition = request.Config.BotType == BotType.FlippingBot, // Computed based on BotType Name = request.Config.Name ?? $"Backtest-{request.Config.ScenarioName ?? request.Config.Scenario?.Name ?? "Custom"}-{DateTime.UtcNow:yyyyMMdd-HHmmss}", CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, + UseSynthApi = request.Config.UseSynthApi, + UseForPositionSizing = request.Config.UseForPositionSizing, + UseForSignalFiltering = request.Config.UseForSignalFiltering, + UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss }; switch (request.Config.BotType) @@ -208,6 +247,18 @@ public class BacktestController : BaseController await _hubContext.Clients.All.SendAsync("BacktestsSubscription", backtesting); } } + + public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest) + { + return new MoneyManagement + { + Name = moneyManagementRequest.Name, + StopLoss = moneyManagementRequest.StopLoss, + TakeProfit = moneyManagementRequest.TakeProfit, + Leverage = moneyManagementRequest.Leverage, + Timeframe = moneyManagementRequest.Timeframe + }; + } } /// @@ -216,9 +267,9 @@ public class BacktestController : BaseController public class RunBacktestRequest { /// - /// The trading bot configuration to use for the backtest + /// The trading bot configuration request to use for the backtest /// - public TradingBotConfig Config { get; set; } + public TradingBotConfigRequest Config { get; set; } /// /// The start date for the backtest @@ -244,14 +295,4 @@ public class RunBacktestRequest /// Whether to save the backtest results /// public bool Save { get; set; } = false; - - /// - /// The name of the money management to use (optional if MoneyManagement is provided) - /// - public string? MoneyManagementName { get; set; } - - /// - /// The money management details (optional if MoneyManagementName is provided) - /// - public MoneyManagement? MoneyManagement { get; set; } } \ No newline at end of file diff --git a/src/Managing.Api/Controllers/BaseController.cs b/src/Managing.Api/Controllers/BaseController.cs index 2421609..47891cb 100644 --- a/src/Managing.Api/Controllers/BaseController.cs +++ b/src/Managing.Api/Controllers/BaseController.cs @@ -30,6 +30,9 @@ public abstract class BaseController : ControllerBase throw new Exception("User not found for this token"); } + throw new Exception("Not identity assigned to this token"); } -} + + +} \ No newline at end of file diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 27aa177..4d5b157 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using Managing.Api.Models.Requests; using Managing.Api.Models.Responses; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; @@ -7,6 +7,8 @@ using Managing.Application.ManageBot.Commands; using Managing.Common; using Managing.Domain.Bots; using Managing.Domain.MoneyManagements; +using Managing.Domain.Scenarios; +using Managing.Domain.Strategies; using Managing.Domain.Trades; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -117,24 +119,37 @@ public class BotController : BaseController return Forbid("You don't have permission to start a bot with this account"); } - // Trigger error if money management is not provided - if (string.IsNullOrEmpty(request.MoneyManagementName) && request.Config.MoneyManagement == null) + // Validate that either money management name or object is provided + if (string.IsNullOrEmpty(request.Config.MoneyManagementName) && request.Config.MoneyManagement == null) { - return BadRequest("Money management name or money management object is required"); + return BadRequest("Either money management name or money management object is required"); } var user = await GetUser(); - - // Get money management if name is provided - MoneyManagement moneyManagement = request.Config.MoneyManagement; - if (!string.IsNullOrEmpty(request.MoneyManagementName)) + + // Get money management - either by name lookup or use provided object + MoneyManagement moneyManagement; + if (!string.IsNullOrEmpty(request.Config.MoneyManagementName)) { - moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); + moneyManagement = + await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagementName); if (moneyManagement == null) { return BadRequest("Money management not found"); } } + else + { + moneyManagement = Map(request.Config.MoneyManagement); + // Format percentage values if using custom money management + moneyManagement?.FormatPercentage(); + + // Ensure user is set for custom money management + if (moneyManagement != null) + { + moneyManagement.User = user; + } + } // Validate initialTradingBalance if (request.Config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount) @@ -167,13 +182,45 @@ public class BotController : BaseController return BadRequest("CloseEarlyWhenProfitable can only be enabled when MaxPositionTimeHours is set"); } - // Update the config with final money management + // Handle scenario - either from ScenarioRequest or ScenarioName + Scenario scenario = null; + if (request.Config.Scenario != null) + { + // Convert ScenarioRequest to Scenario domain object + scenario = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod) + { + User = user + }; + + // Convert IndicatorRequest objects to Indicator domain objects + foreach (var indicatorRequest in request.Config.Scenario.Indicators) + { + var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) + { + SignalType = indicatorRequest.SignalType, + MinimumHistory = indicatorRequest.MinimumHistory, + Period = indicatorRequest.Period, + FastPeriods = indicatorRequest.FastPeriods, + SlowPeriods = indicatorRequest.SlowPeriods, + SignalPeriods = indicatorRequest.SignalPeriods, + Multiplier = indicatorRequest.Multiplier, + SmoothPeriods = indicatorRequest.SmoothPeriods, + StochPeriods = indicatorRequest.StochPeriods, + CyclePeriods = indicatorRequest.CyclePeriods, + User = user + }; + scenario.AddIndicator(indicator); + } + } + + // Map the request to the full TradingBotConfig var config = new TradingBotConfig { AccountName = request.Config.AccountName, MoneyManagement = moneyManagement, Ticker = request.Config.Ticker, - ScenarioName = request.Config.ScenarioName, + Scenario = scenario, // Use the converted scenario object + ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.Config.IsForWatchingOnly, BotTradingBalance = request.Config.BotTradingBalance, @@ -182,10 +229,15 @@ public class BotController : BaseController MaxLossStreak = request.Config.MaxLossStreak, MaxPositionTimeHours = request.Config.MaxPositionTimeHours, FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, + CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, + UseSynthApi = request.Config.UseSynthApi, + UseForPositionSizing = request.Config.UseForPositionSizing, + UseForSignalFiltering = request.Config.UseForSignalFiltering, + UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, + // Set computed/default properties IsForBacktest = false, FlipPosition = request.Config.BotType == BotType.FlippingBot, - Name = request.Config.Name, - CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable + Name = request.Config.Name }; var result = await _mediator.Send(new StartBotCommand(config, request.Config.Name, user)); @@ -200,6 +252,7 @@ public class BotController : BaseController } } + /// /// Stops a bot specified by type and name. /// @@ -614,7 +667,7 @@ public class BotController : BaseController // Get the existing bot to ensure it exists and get current config var bots = _botService.GetActiveBots(); var existingBot = bots.FirstOrDefault(b => b.Identifier == request.Identifier); - + if (existingBot == null) { return NotFound($"Bot with identifier '{request.Identifier}' not found"); @@ -630,9 +683,9 @@ public class BotController : BaseController } // If the bot name is being changed, check for conflicts - var isNameChanging = !string.IsNullOrEmpty(request.Config.Name) && + var isNameChanging = !string.IsNullOrEmpty(request.Config.Name) && request.Config.Name != request.Identifier; - + if (isNameChanging) { // Check if new name already exists @@ -643,31 +696,36 @@ public class BotController : BaseController } } - // Validate the money management if provided - if (request.Config.MoneyManagement != null) + // Validate and get the money management + MoneyManagement moneyManagement = null; + if (!string.IsNullOrEmpty(request.MoneyManagementName)) { - // Check if the money management belongs to the user - var userMoneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.Config.MoneyManagement.Name); - if (userMoneyManagement != null && userMoneyManagement.User?.Name != user.Name) - { - return Forbid("You don't have permission to use this money management"); - } - } - else if (!string.IsNullOrEmpty(request.MoneyManagementName)) - { - // If MoneyManagement is null but MoneyManagementName is provided, load it - var moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); + // Load money management by name + moneyManagement = await _moneyManagementService.GetMoneyMangement(user, request.MoneyManagementName); if (moneyManagement == null) { return BadRequest($"Money management '{request.MoneyManagementName}' not found"); } - + if (moneyManagement.User?.Name != user.Name) { return Forbid("You don't have permission to use this money management"); } - - request.Config.MoneyManagement = moneyManagement; + } + else if (request.MoneyManagement != null) + { + // Use provided money management object + moneyManagement = request.MoneyManagement; + // Format percentage values if using custom money management + moneyManagement.FormatPercentage(); + + // Ensure user is set for custom money management + moneyManagement.User = user; + } + else + { + // Use existing bot's money management if no new one is provided + moneyManagement = existingBot.Config.MoneyManagement; } // Validate CloseEarlyWhenProfitable requires MaxPositionTimeHours @@ -676,27 +734,85 @@ public class BotController : BaseController return BadRequest("CloseEarlyWhenProfitable requires MaxPositionTimeHours to be set"); } + // Handle scenario - either from ScenarioRequest or ScenarioName + Scenario scenarioForUpdate = null; + if (request.Config.Scenario != null) + { + // Convert ScenarioRequest to Scenario domain object + scenarioForUpdate = new Scenario(request.Config.Scenario.Name, request.Config.Scenario.LoopbackPeriod) + { + User = user + }; + + // Convert IndicatorRequest objects to Indicator domain objects + foreach (var indicatorRequest in request.Config.Scenario.Indicators) + { + var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type) + { + SignalType = indicatorRequest.SignalType, + MinimumHistory = indicatorRequest.MinimumHistory, + Period = indicatorRequest.Period, + FastPeriods = indicatorRequest.FastPeriods, + SlowPeriods = indicatorRequest.SlowPeriods, + SignalPeriods = indicatorRequest.SignalPeriods, + Multiplier = indicatorRequest.Multiplier, + SmoothPeriods = indicatorRequest.SmoothPeriods, + StochPeriods = indicatorRequest.StochPeriods, + CyclePeriods = indicatorRequest.CyclePeriods, + User = user + }; + scenarioForUpdate.AddIndicator(indicator); + } + } + + // Map the request to the full TradingBotConfig + var updatedConfig = new TradingBotConfig + { + AccountName = request.Config.AccountName, + MoneyManagement = moneyManagement, + Ticker = request.Config.Ticker, + Scenario = scenarioForUpdate, // Use the converted scenario object + ScenarioName = request.Config.ScenarioName, // Fallback to scenario name if scenario object not provided + Timeframe = request.Config.Timeframe, + IsForWatchingOnly = request.Config.IsForWatchingOnly, + BotTradingBalance = request.Config.BotTradingBalance, + BotType = request.Config.BotType, + CooldownPeriod = request.Config.CooldownPeriod, + MaxLossStreak = request.Config.MaxLossStreak, + MaxPositionTimeHours = request.Config.MaxPositionTimeHours, + FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, + CloseEarlyWhenProfitable = request.Config.CloseEarlyWhenProfitable, + UseSynthApi = request.Config.UseSynthApi, + UseForPositionSizing = request.Config.UseForPositionSizing, + UseForSignalFiltering = request.Config.UseForSignalFiltering, + UseForDynamicStopLoss = request.Config.UseForDynamicStopLoss, + // Set computed/default properties + IsForBacktest = false, + FlipPosition = request.Config.BotType == BotType.FlippingBot, + Name = request.Config.Name + }; + // Update the bot configuration using the enhanced method - var success = await _botService.UpdateBotConfiguration(request.Identifier, request.Config); - + var success = await _botService.UpdateBotConfiguration(request.Identifier, updatedConfig); + if (success) { var finalBotName = isNameChanging ? request.Config.Name : request.Identifier; - + await _hubContext.Clients.All.SendAsync("SendNotification", $"Bot {finalBotName} configuration updated successfully by {user.Name}." + (isNameChanging ? $" (renamed from {request.Identifier})" : ""), "Info"); - + await NotifyBotSubscriberAsync(); - - return Ok(isNameChanging + + return Ok(isNameChanging ? $"Bot configuration updated successfully and renamed to '{request.Config.Name}'" : "Bot configuration updated successfully"); } else { return BadRequest("Failed to update bot configuration. " + - (isNameChanging ? "The new name might already be in use." : "")); + (isNameChanging ? "The new name might already be in use." : "")); } } catch (Exception ex) @@ -705,6 +821,18 @@ public class BotController : BaseController return StatusCode(500, $"Error updating bot configuration: {ex.Message}"); } } + + public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest) + { + return new MoneyManagement + { + Name = moneyManagementRequest.Name, + StopLoss = moneyManagementRequest.StopLoss, + TakeProfit = moneyManagementRequest.TakeProfit, + Leverage = moneyManagementRequest.Leverage, + Timeframe = moneyManagementRequest.Timeframe + }; + } } /// @@ -745,35 +873,7 @@ public class ClosePositionRequest public class StartBotRequest { /// - /// The trading bot configuration + /// The trading bot configuration request with primary properties /// - public TradingBotConfig Config { get; set; } - - /// - /// Optional money management name (if not included in Config.MoneyManagement) - /// - public string? MoneyManagementName { get; set; } -} - -/// -/// Request model for updating bot configuration -/// -public class UpdateBotConfigRequest -{ - /// - /// The unique identifier of the bot to update - /// - [Required] - public string Identifier { get; set; } - - /// - /// The new trading bot configuration - /// - [Required] - public TradingBotConfig Config { get; set; } - - /// - /// Optional: Money management name to load if Config.MoneyManagement is null - /// - public string? MoneyManagementName { get; set; } + public TradingBotConfigRequest Config { get; set; } } \ No newline at end of file diff --git a/src/Managing.Api/Controllers/ScenarioController.cs b/src/Managing.Api/Controllers/ScenarioController.cs index 0aa569c..5f00c1e 100644 --- a/src/Managing.Api/Controllers/ScenarioController.cs +++ b/src/Managing.Api/Controllers/ScenarioController.cs @@ -1,4 +1,5 @@ -using Managing.Application.Abstractions; +using Managing.Api.Models.Responses; +using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; @@ -39,10 +40,12 @@ public class ScenarioController : BaseController /// /// A list of scenarios. [HttpGet] - public async Task>> GetScenarios() + public async Task>> GetScenarios() { var user = await GetUser(); - return Ok(_scenarioService.GetScenariosByUser(user)); + var scenarios = _scenarioService.GetScenariosByUser(user); + var scenarioViewModels = scenarios.Select(MapToScenarioViewModel); + return Ok(scenarioViewModels); } /// @@ -52,11 +55,13 @@ public class ScenarioController : BaseController /// A list of strategy names to include in the scenario. /// The created scenario. [HttpPost] - public async Task> CreateScenario(string name, List strategies, + public async Task> CreateScenario(string name, List strategies, int? loopbackPeriod = null) { var user = await GetUser(); - return Ok(_scenarioService.CreateScenarioForUser(user, name, strategies, loopbackPeriod)); + var scenario = _scenarioService.CreateScenarioForUser(user, name, strategies, loopbackPeriod); + var scenarioViewModel = MapToScenarioViewModel(scenario); + return Ok(scenarioViewModel); } /// @@ -85,10 +90,12 @@ public class ScenarioController : BaseController /// A list of strategies. [HttpGet] [Route("indicator")] - public async Task>> GetIndicators() + public async Task>> GetIndicators() { var user = await GetUser(); - return Ok(_scenarioService.GetIndicatorsByUser(user)); + var indicators = _scenarioService.GetIndicatorsByUser(user); + var indicatorViewModels = indicators.Select(MapToIndicatorViewModel); + return Ok(indicatorViewModels); } /// @@ -107,7 +114,7 @@ public class ScenarioController : BaseController /// The created indicator. [HttpPost] [Route("indicator")] - public async Task> CreateIndicator( + public async Task> CreateIndicator( IndicatorType indicatorType, string name, int? period = null, @@ -120,7 +127,7 @@ public class ScenarioController : BaseController int? cyclePeriods = null) { var user = await GetUser(); - return Ok(_scenarioService.CreateIndicatorForUser( + var indicator = _scenarioService.CreateIndicatorForUser( user, indicatorType, name, @@ -131,7 +138,9 @@ public class ScenarioController : BaseController multiplier, stochPeriods, smoothPeriods, - cyclePeriods)); + cyclePeriods); + var indicatorViewModel = MapToIndicatorViewModel(indicator); + return Ok(indicatorViewModel); } /// @@ -176,4 +185,35 @@ public class ScenarioController : BaseController smoothPeriods, cyclePeriods)); } + + private static ScenarioViewModel MapToScenarioViewModel(Scenario scenario) + { + return new ScenarioViewModel + { + Name = scenario.Name, + LoopbackPeriod = scenario.LoopbackPeriod, + UserName = scenario.User?.Name, + Indicators = scenario.Indicators?.Select(MapToIndicatorViewModel).ToList() ?? new List() + }; + } + + private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator) + { + return new IndicatorViewModel + { + Name = indicator.Name, + Type = indicator.Type, + SignalType = indicator.SignalType, + MinimumHistory = indicator.MinimumHistory, + Period = indicator.Period, + FastPeriods = indicator.FastPeriods, + SlowPeriods = indicator.SlowPeriods, + SignalPeriods = indicator.SignalPeriods, + Multiplier = indicator.Multiplier, + SmoothPeriods = indicator.SmoothPeriods, + StochPeriods = indicator.StochPeriods, + CyclePeriods = indicator.CyclePeriods, + UserName = indicator.User?.Name + }; + } } \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/IndicatorRequest.cs b/src/Managing.Api/Models/Requests/IndicatorRequest.cs new file mode 100644 index 0000000..1a11600 --- /dev/null +++ b/src/Managing.Api/Models/Requests/IndicatorRequest.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Requests; + +/// +/// Request model for indicator configuration without user information +/// +public class IndicatorRequest +{ + /// + /// The name of the indicator + /// + [Required] + public string Name { get; set; } + + /// + /// The type of indicator + /// + [Required] + public IndicatorType Type { get; set; } + + /// + /// The signal type for this indicator + /// + [Required] + public SignalType SignalType { get; set; } + + /// + /// Minimum history required for this indicator + /// + public int MinimumHistory { get; set; } + + /// + /// Period parameter for the indicator + /// + public int? Period { get; set; } + + /// + /// Fast periods parameter for indicators like MACD + /// + public int? FastPeriods { get; set; } + + /// + /// Slow periods parameter for indicators like MACD + /// + public int? SlowPeriods { get; set; } + + /// + /// Signal periods parameter for indicators like MACD + /// + public int? SignalPeriods { get; set; } + + /// + /// Multiplier parameter for indicators like SuperTrend + /// + public double? Multiplier { get; set; } + + /// + /// Smooth periods parameter + /// + public int? SmoothPeriods { get; set; } + + /// + /// Stochastic periods parameter + /// + public int? StochPeriods { get; set; } + + /// + /// Cycle periods parameter + /// + public int? CyclePeriods { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/MoneyManagementRequest.cs b/src/Managing.Api/Models/Requests/MoneyManagementRequest.cs new file mode 100644 index 0000000..eef3162 --- /dev/null +++ b/src/Managing.Api/Models/Requests/MoneyManagementRequest.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; +using Managing.Common; + +namespace Managing.Api.Models.Requests; + +public class MoneyManagementRequest +{ + [Required] public string Name { get; set; } + [Required] public Enums.Timeframe Timeframe { get; set; } + [Required] public decimal StopLoss { get; set; } + [Required] public decimal TakeProfit { get; set; } + [Required] public decimal Leverage { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/RunBacktestRequest.cs b/src/Managing.Api/Models/Requests/RunBacktestRequest.cs index f9b6cb3..80892d5 100644 --- a/src/Managing.Api/Models/Requests/RunBacktestRequest.cs +++ b/src/Managing.Api/Models/Requests/RunBacktestRequest.cs @@ -1,15 +1,49 @@ -using static Managing.Common.Enums; +using Managing.Domain.MoneyManagements; -namespace Managing.Api.Models.Requests +namespace Managing.Api.Models.Requests; + +/// +/// Request model for running a backtest +/// +public class RunBacktestRequest { - public class RunBacktestRequest - { - public TradingExchanges Exchange { get; set; } - public BotType BotType { get; set; } - public Ticker Ticker { get; set; } - public Timeframe Timeframe { get; set; } - public RiskLevel RiskLevel { get; set; } - public bool WatchOnly { get; set; } - public int Days { get; set; } - } + /// + /// The trading bot configuration request to use for the backtest + /// + public TradingBotConfigRequest Config { get; set; } + + /// + /// The start date for the backtest + /// + public DateTime StartDate { get; set; } + + /// + /// The end date for the backtest + /// + public DateTime EndDate { get; set; } + + /// + /// The starting balance for the backtest + /// + public decimal Balance { get; set; } + + /// + /// Whether to only watch the backtest without executing trades + /// + public bool WatchOnly { get; set; } = false; + + /// + /// Whether to save the backtest results + /// + public bool Save { get; set; } = false; + + /// + /// The name of the money management to use (optional if MoneyManagement is provided) + /// + public string? MoneyManagementName { get; set; } + + /// + /// The money management details (optional if MoneyManagementName is provided) + /// + public MoneyManagement? MoneyManagement { get; set; } } diff --git a/src/Managing.Api/Models/Requests/ScenarioRequest.cs b/src/Managing.Api/Models/Requests/ScenarioRequest.cs new file mode 100644 index 0000000..40d3fc3 --- /dev/null +++ b/src/Managing.Api/Models/Requests/ScenarioRequest.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace Managing.Api.Models.Requests; + +/// +/// Request model for scenario configuration without user information +/// +public class ScenarioRequest +{ + /// + /// The name of the scenario + /// + [Required] + public string Name { get; set; } + + /// + /// List of indicator configurations for this scenario + /// + [Required] + public List Indicators { get; set; } = new(); + + /// + /// The loopback period for the scenario + /// + public int? LoopbackPeriod { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/StartBotRequest.cs b/src/Managing.Api/Models/Requests/StartBotRequest.cs index ed714f1..42bedc8 100644 --- a/src/Managing.Api/Models/Requests/StartBotRequest.cs +++ b/src/Managing.Api/Models/Requests/StartBotRequest.cs @@ -1,31 +1,16 @@ using System.ComponentModel.DataAnnotations; -using static Managing.Common.Enums; namespace Managing.Api.Models.Requests { + /// + /// Request model for starting a bot + /// public class StartBotRequest { - [Required] public BotType BotType { get; set; } - [Required] public string BotName { get; set; } - [Required] public Ticker Ticker { get; set; } - [Required] public Timeframe Timeframe { get; set; } - [Required] public bool IsForWatchOnly { get; set; } - [Required] public string Scenario { get; set; } - [Required] public string AccountName { get; set; } - [Required] public string MoneyManagementName { get; set; } - /// - /// Initial trading balance in USD for the bot + /// The trading bot configuration request with primary properties /// [Required] - [Range(10.00, double.MaxValue, ErrorMessage = "Initial trading balance must be greater than ten")] - public decimal InitialTradingBalance { get; set; } - - /// - /// Cooldown period in minutes between trades - /// - [Required] - [Range(1, 1440, ErrorMessage = "Cooldown period must be between 1 and 1440 minutes (24 hours)")] - public decimal CooldownPeriod { get; set; } = 1; // Default to 1 minute if not specified + public TradingBotConfigRequest Config { get; set; } } } \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs b/src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs new file mode 100644 index 0000000..1bf60c4 --- /dev/null +++ b/src/Managing.Api/Models/Requests/TradingBotConfigRequest.cs @@ -0,0 +1,119 @@ +using System.ComponentModel.DataAnnotations; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Requests; + +/// +/// Simplified trading bot configuration request with only primary properties +/// +public class TradingBotConfigRequest +{ + /// + /// The account name to use for trading + /// + [Required] + public string AccountName { get; set; } + + /// + /// The ticker/symbol to trade + /// + [Required] + public Ticker Ticker { get; set; } + + /// + /// The timeframe for trading decisions + /// + [Required] + public Timeframe Timeframe { get; set; } + + /// + /// Whether this bot is for watching only (no actual trading) + /// + [Required] + public bool IsForWatchingOnly { get; set; } + + /// + /// The initial trading balance for the bot + /// + [Required] + public decimal BotTradingBalance { get; set; } + + /// + /// The type of bot (SimpleBot, ScalpingBot, FlippingBot) + /// + [Required] + public BotType BotType { get; set; } + + /// + /// The name/identifier for this bot + /// + [Required] + public string Name { get; set; } + + /// + /// Cooldown period between trades (in candles) + /// + [Required] + public int CooldownPeriod { get; set; } + + /// + /// Maximum consecutive losses before stopping the bot + /// + [Required] + public int MaxLossStreak { get; set; } + + /// + /// The scenario configuration (takes precedence over ScenarioName) + /// + public ScenarioRequest? Scenario { get; set; } + + /// + /// The scenario name to load from database (only used when Scenario is not provided) + /// + public string? ScenarioName { get; set; } + + /// + /// The money management name to load from database (only used when MoneyManagement is not provided) + /// + public string? MoneyManagementName { get; set; } + + /// + /// The money management object to use for the bot + /// + public MoneyManagementRequest? MoneyManagement { get; set; } + + /// + /// Maximum time in hours that a position can remain open before being automatically closed + /// + public decimal? MaxPositionTimeHours { get; set; } + + /// + /// Whether to close positions early when they become profitable + /// + public bool CloseEarlyWhenProfitable { get; set; } = false; + + /// + /// Whether to only flip positions when the current position is in profit + /// + public bool FlipOnlyWhenInProfit { get; set; } = true; + + /// + /// Whether to use Synth API for predictions and risk assessment + /// + public bool UseSynthApi { get; set; } = false; + + /// + /// Whether to use Synth predictions for position sizing adjustments + /// + public bool UseForPositionSizing { get; set; } = true; + + /// + /// Whether to use Synth predictions for signal filtering + /// + public bool UseForSignalFiltering { get; set; } = true; + + /// + /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments + /// + public bool UseForDynamicStopLoss { get; set; } = true; +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs b/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs new file mode 100644 index 0000000..26bc075 --- /dev/null +++ b/src/Managing.Api/Models/Requests/UpdateBotConfigRequest.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using Managing.Domain.MoneyManagements; + +namespace Managing.Api.Models.Requests; + +/// +/// Request model for updating bot configuration +/// +public class UpdateBotConfigRequest +{ + /// + /// The unique identifier of the bot to update + /// + [Required] + public string Identifier { get; set; } + + /// + /// The new trading bot configuration request + /// + [Required] + public TradingBotConfigRequest Config { get; set; } + + /// + /// Optional: Money management name to load from database (if MoneyManagement object is not provided) + /// + public string? MoneyManagementName { get; set; } + + /// + /// Optional: Money management object for custom configurations (takes precedence over MoneyManagementName) + /// + public MoneyManagement? MoneyManagement { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Responses/IndicatorViewModel.cs b/src/Managing.Api/Models/Responses/IndicatorViewModel.cs new file mode 100644 index 0000000..31950d0 --- /dev/null +++ b/src/Managing.Api/Models/Responses/IndicatorViewModel.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using static Managing.Common.Enums; + +namespace Managing.Api.Models.Responses; + +public class IndicatorViewModel +{ + [Required] + public string Name { get; set; } = string.Empty; + + [Required] + public IndicatorType Type { get; set; } + + [Required] + public SignalType SignalType { get; set; } + + [Required] + public int MinimumHistory { get; set; } + + public int? Period { get; set; } + public int? FastPeriods { get; set; } + public int? SlowPeriods { get; set; } + public int? SignalPeriods { get; set; } + public double? Multiplier { get; set; } + public int? SmoothPeriods { get; set; } + public int? StochPeriods { get; set; } + public int? CyclePeriods { get; set; } + + [Required] + public string UserName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Managing.Api/Models/Responses/ScenarioViewModel.cs b/src/Managing.Api/Models/Responses/ScenarioViewModel.cs new file mode 100644 index 0000000..31aacdb --- /dev/null +++ b/src/Managing.Api/Models/Responses/ScenarioViewModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace Managing.Api.Models.Responses; + +public class ScenarioViewModel +{ + [Required] + public string Name { get; set; } = string.Empty; + + [Required] + public List Indicators { get; set; } = new(); + + public int? LoopbackPeriod { get; set; } + + [Required] + public string UserName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Repositories/ISynthRepository.cs b/src/Managing.Application.Abstractions/Repositories/ISynthRepository.cs new file mode 100644 index 0000000..aa02849 --- /dev/null +++ b/src/Managing.Application.Abstractions/Repositories/ISynthRepository.cs @@ -0,0 +1,59 @@ +using Managing.Domain.Synth.Models; + +namespace Managing.Application.Abstractions.Repositories; + +/// +/// Repository interface for Synth-related data operations +/// Provides MongoDB persistence for leaderboard and individual predictions data +/// +public interface ISynthRepository +{ + /// + /// Gets cached leaderboard data by cache key + /// + /// The cache key to search for + /// Cached leaderboard data if found, null otherwise + Task GetLeaderboardAsync(string cacheKey); + + /// + /// Saves leaderboard data to MongoDB + /// + /// The leaderboard data to save + Task SaveLeaderboardAsync(SynthMinersLeaderboard leaderboard); + + /// + /// Gets individual cached prediction data by asset, parameters, and miner UIDs + /// + /// Asset symbol + /// Time increment in seconds + /// Time length in seconds + /// List of miner UIDs to get predictions for + /// Whether this is backtest data + /// Signal date for backtest data + /// List of cached individual predictions + Task> GetIndividualPredictionsAsync( + string asset, + int timeIncrement, + int timeLength, + List minerUids, + bool isBacktest, + DateTime? signalDate); + + /// + /// Saves individual prediction data to MongoDB + /// + /// The individual prediction data to save + Task SaveIndividualPredictionAsync(SynthPrediction prediction); + + /// + /// Saves multiple individual predictions to MongoDB in batch + /// + /// The list of individual predictions to save + Task SaveIndividualPredictionsAsync(List predictions); + + /// + /// Cleans up old cached data beyond the retention period + /// + /// Number of days to retain data + Task CleanupOldDataAsync(int retentionDays = 30); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/ISynthApiClient.cs b/src/Managing.Application.Abstractions/Services/ISynthApiClient.cs new file mode 100644 index 0000000..538a6aa --- /dev/null +++ b/src/Managing.Application.Abstractions/Services/ISynthApiClient.cs @@ -0,0 +1,59 @@ +using Managing.Domain.Synth.Models; + +namespace Managing.Application.Abstractions.Services; + +/// +/// Interface for communicating with the Synth API +/// +public interface ISynthApiClient +{ + /// + /// Fetches the current leaderboard from Synth API + /// + /// Synth configuration containing API key and settings + /// List of miners with their rankings and stats + Task> GetLeaderboardAsync(SynthConfiguration config); + + /// + /// Fetches historical leaderboard data from Synth API for a specific time range + /// + /// Start time for historical data (ISO 8601 format) + /// End time for historical data (ISO 8601 format) + /// Synth configuration containing API key and settings + /// List of miners with their historical rankings and stats + Task> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config); + + /// + /// Fetches latest predictions from specified miners + /// + /// List of miner UIDs to get predictions from + /// Asset symbol (e.g., "BTC", "ETH") + /// Time interval in seconds between each prediction point + /// Total prediction time length in seconds + /// Synth configuration containing API key and settings + /// List of predictions from the specified miners + Task> GetMinerPredictionsAsync( + List minerUids, + string asset, + int timeIncrement, + int timeLength, + SynthConfiguration config); + + /// + /// Fetches historical predictions from specified miners for a specific time point + /// + /// List of miner UIDs to get predictions from + /// Asset symbol (e.g., "BTC", "ETH") + /// Start time for historical predictions (when the prediction was made) + /// Time interval in seconds between each prediction point + /// Total prediction time length in seconds + /// Synth configuration containing API key and settings + /// List of historical predictions from the specified miners + Task> GetHistoricalMinerPredictionsAsync( + List minerUids, + string asset, + DateTime startTime, + int timeIncrement, + int timeLength, + SynthConfiguration config); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs b/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs new file mode 100644 index 0000000..4971d7d --- /dev/null +++ b/src/Managing.Application.Abstractions/Services/ISynthPredictionService.cs @@ -0,0 +1,109 @@ +using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; +using Managing.Domain.Strategies; +using Managing.Domain.Synth.Models; +using static Managing.Common.Enums; + +namespace Managing.Application.Abstractions.Services; + +/// +/// Service interface for Synth prediction business logic and probability calculations +/// +public interface ISynthPredictionService +{ + /// + /// Calculates the probability of price reaching a target within a specified time horizon + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// Current market price + /// Target price to reach + /// Time horizon in seconds + /// True for long positions (liquidation when price drops), false for short positions (liquidation when price rises) + /// Synth configuration for this operation + /// Probability as a decimal between 0.0 and 1.0 + Task GetProbabilityOfTargetPriceAsync( + string asset, + decimal currentPrice, + decimal targetPrice, + int timeHorizonSeconds, + bool isLongPosition, + SynthConfiguration config); + + /// + /// Gets probabilities for multiple price thresholds at once + /// + /// Asset symbol + /// Current market price + /// Dictionary of threshold names to prices + /// Time horizon in seconds + /// True for long positions, false for short positions + /// Synth configuration for this operation + /// Parameter for backtest + /// Signal date + /// Dictionary of threshold names to probabilities + Task> GetMultipleThresholdProbabilitiesAsync( + string asset, + decimal currentPrice, + Dictionary priceThresholds, + int timeHorizonSeconds, + bool isLongPosition, + SynthConfiguration config, + bool isBacktest, + DateTime signalDate); + + /// + /// Clears cached predictions (useful for testing or forced refresh) + /// + void ClearCache(); + + /// + /// Clears cached predictions from MongoDB asynchronously + /// + Task ClearCacheAsync(); + + /// + /// Validates a trading signal using Synth predictions to check for adverse price movements + /// + /// The trading signal containing ticker, direction, candle data, and other context + /// Current market price (required) + /// Bot configuration with Synth settings + /// Whether this is a backtest + /// Custom probability thresholds for decision-making. If null, uses default thresholds. + /// Comprehensive signal validation result including confidence, probabilities, and risk analysis + Task ValidateSignalAsync(Signal signal, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest, Dictionary customThresholds = null); + + /// + /// Performs risk assessment before opening a position + /// + /// Trading ticker + /// Position direction + /// Current market price + /// Bot configuration with Synth settings + /// Whether this is a backtest + /// True if position should be allowed, false if blocked + Task AssessPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest); + + /// + /// Monitors liquidation risk for an open position + /// + /// Trading ticker + /// Position direction + /// Current market price + /// Position liquidation price + /// Position identifier for logging + /// Bot configuration with Synth settings + /// Risk assessment result + Task MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, + decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig); + + /// + /// Estimates liquidation price based on money management settings + /// + /// Current market price + /// Position direction + /// Money management settings + /// Estimated liquidation price + decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, MoneyManagement moneyManagement); +} \ No newline at end of file diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index a128f45..17a93d6 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -1,7 +1,9 @@ using Managing.Domain.Accounts; +using Managing.Domain.Bots; using Managing.Domain.Scenarios; using Managing.Domain.Statistics; using Managing.Domain.Strategies; +using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Managing.Infrastructure.Evm.Models.Privy; using static Managing.Common.Enums; @@ -37,4 +39,15 @@ public interface ITradingService void UpdateStrategy(Indicator indicator); Task> GetBrokerPositions(Account account); Task InitPrivyWallet(string publicAddress); + + // Synth API integration methods + Task ValidateSynthSignalAsync(Signal signal, decimal currentPrice, + TradingBotConfig botConfig, + bool isBacktest); + + Task AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest); + + Task MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, + decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig); } \ No newline at end of file diff --git a/src/Managing.Application/Abstractions/IBotService.cs b/src/Managing.Application/Abstractions/IBotService.cs index 4ce07c7..b805466 100644 --- a/src/Managing.Application/Abstractions/IBotService.cs +++ b/src/Managing.Application/Abstractions/IBotService.cs @@ -7,7 +7,7 @@ namespace Managing.Application.Abstractions; public interface IBotService { - void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data); + void SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, string data); void AddSimpleBotToCache(IBot bot); void AddTradingBotToCache(ITradingBot bot); List GetActiveBots(); @@ -21,7 +21,7 @@ public interface IBotService /// The trading bot configuration /// ITradingBot instance ITradingBot CreateTradingBot(TradingBotConfig config); - + /// /// Creates a trading bot for backtesting using the unified TradingBot class /// diff --git a/src/Managing.Application/Abstractions/IScenarioService.cs b/src/Managing.Application/Abstractions/IScenarioService.cs index ea95d3d..5967315 100644 --- a/src/Managing.Application/Abstractions/IScenarioService.cs +++ b/src/Managing.Application/Abstractions/IScenarioService.cs @@ -9,7 +9,7 @@ namespace Managing.Application.Abstractions { IEnumerable GetScenarios(); Scenario CreateScenario(string name, List strategies, int? loopbackPeriod = 1); - IEnumerable GetStrategies(); + IEnumerable GetIndicators(); bool DeleteStrategy(string name); bool DeleteScenario(string name); Scenario GetScenario(string name); diff --git a/src/Managing.Application/Abstractions/ITradingBot.cs b/src/Managing.Application/Abstractions/ITradingBot.cs index a8aad6b..b6d3f70 100644 --- a/src/Managing.Application/Abstractions/ITradingBot.cs +++ b/src/Managing.Application/Abstractions/ITradingBot.cs @@ -14,7 +14,6 @@ namespace Managing.Application.Abstractions { TradingBotConfig Config { get; set; } Account Account { get; set; } - HashSet Indicators { get; set; } FixedSizeQueue OptimizedCandles { get; set; } HashSet Candles { get; set; } HashSet Signals { get; set; } @@ -25,14 +24,12 @@ namespace Managing.Application.Abstractions DateTime PreloadSince { get; set; } int PreloadedCandlesCount { get; set; } decimal Fee { get; set; } - Scenario Scenario { get; set; } Task Run(); Task ToggleIsForWatchOnly(); int GetWinRate(); decimal GetProfitAndLoss(); decimal GetTotalFees(); - void LoadIndicators(IEnumerable indicators); void LoadScenario(string scenarioName); void LoadScenario(Scenario scenario); void UpdateIndicatorsValues(); diff --git a/src/Managing.Application/Backtesting/Backtester.cs b/src/Managing.Application/Backtesting/Backtester.cs index dc6053c..15be376 100644 --- a/src/Managing.Application/Backtesting/Backtester.cs +++ b/src/Managing.Application/Backtesting/Backtester.cs @@ -183,19 +183,44 @@ namespace Managing.Application.Backtesting throw new Exception("No candle to backtest"); } + var totalCandles = candles.Count; + var currentCandle = 0; + var lastLoggedPercentage = 0; + + _logger.LogInformation("Starting backtest with {TotalCandles} candles for {Ticker} on {Timeframe}", + totalCandles, config.Ticker, config.Timeframe); + bot.WalletBalances.Add(candles.FirstOrDefault().Date, config.BotTradingBalance); + foreach (var candle in candles) { bot.OptimizedCandles.Enqueue(candle); bot.Candles.Add(candle); bot.Run(); + + currentCandle++; + + // Log progress every 10% or every 1000 candles, whichever comes first + var currentPercentage = (int)((double)currentCandle / totalCandles * 100); + var shouldLog = currentPercentage >= lastLoggedPercentage + 10 || + currentCandle % 1000 == 0 || + currentCandle == totalCandles; + + if (shouldLog && currentPercentage > lastLoggedPercentage) + { + _logger.LogInformation( + "Backtest progress: {CurrentCandle}/{TotalCandles} ({Percentage}%) - Processing candle from {CandleDate}", + currentCandle, totalCandles, currentPercentage, candle.Date.ToString("yyyy-MM-dd HH:mm")); + lastLoggedPercentage = currentPercentage; + } } - bot.Candles = new HashSet(candles); - bot.UpdateIndicatorsValues(); + _logger.LogInformation("Backtest processing completed. Calculating final results..."); - var strategies = _scenarioService.GetStrategies(); - var strategiesValues = GetStrategiesValues(strategies, candles); + bot.Candles = new HashSet(candles); + // bot.UpdateIndicatorsValues(); + + var indicatorsValues = GetIndicatorsValues(bot.Config.Scenario.Indicators, candles); var finalPnl = bot.GetProfitAndLoss(); var winRate = bot.GetWinRate(); @@ -230,7 +255,7 @@ namespace Managing.Application.Backtesting WalletBalances = bot.WalletBalances.ToList(), Statistics = stats, OptimizedMoneyManagement = optimizedMoneyManagement, - StrategiesValues = AggregateValues(strategiesValues, bot.IndicatorsValues), + IndicatorsValues = AggregateValues(indicatorsValues, bot.IndicatorsValues), Score = score }; @@ -238,14 +263,14 @@ namespace Managing.Application.Backtesting } private Dictionary AggregateValues( - Dictionary strategiesValues, + Dictionary indicatorsValues, Dictionary botStrategiesValues) { // Foreach strategy type, only retrieve the values where the strategy is not present already in the bot // Then, add the values to the bot values var result = new Dictionary(); - foreach (var strategy in strategiesValues) + foreach (var indicator in indicatorsValues) { // if (!botStrategiesValues.ContainsKey(strategy.Key)) // { @@ -255,29 +280,29 @@ namespace Managing.Application.Backtesting // result[strategy.Key] = botStrategiesValues[strategy.Key]; // } - result[strategy.Key] = strategy.Value; + result[indicator.Key] = indicator.Value; } return result; } - private Dictionary GetStrategiesValues(IEnumerable strategies, + private Dictionary GetIndicatorsValues(List indicators, List candles) { - var strategiesValues = new Dictionary(); + var indicatorsValues = new Dictionary(); var fixedCandles = new FixedSizeQueue(10000); foreach (var candle in candles) { fixedCandles.Enqueue(candle); } - foreach (var strategy in strategies) + foreach (var indicator in indicators) { try { - var s = ScenarioHelpers.BuildIndicator(strategy, 10000); + var s = ScenarioHelpers.BuildIndicator(indicator, 10000); s.Candles = fixedCandles; - strategiesValues[strategy.Type] = s.GetStrategyValues(); + indicatorsValues[indicator.Type] = s.GetIndicatorValues(); } catch (Exception e) { @@ -285,7 +310,7 @@ namespace Managing.Application.Backtesting } } - return strategiesValues; + return indicatorsValues; } public bool DeleteBacktest(string id) diff --git a/src/Managing.Application/Bots/SimpleBot.cs b/src/Managing.Application/Bots/SimpleBot.cs index 425a73b..368ce53 100644 --- a/src/Managing.Application/Bots/SimpleBot.cs +++ b/src/Managing.Application/Bots/SimpleBot.cs @@ -3,7 +3,6 @@ using Managing.Domain.Bots; using Managing.Domain.Workflows; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using static Managing.Common.Enums; namespace Managing.Application.Bots { @@ -44,7 +43,7 @@ namespace Managing.Application.Bots public override void SaveBackup() { var data = JsonConvert.SerializeObject(_workflow); - _botService.SaveOrUpdateBotBackup(User, Identifier, BotType.SimpleBot, Status, data); + _botService.SaveOrUpdateBotBackup(User, Identifier, Status, data); } public override void LoadBackup(BotBackup backup) diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index d856e43..3e0034e 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -7,7 +7,6 @@ using Managing.Core.FixedSizedQueue; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Candles; -using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Strategies; @@ -41,8 +40,6 @@ public class TradingBot : Bot, ITradingBot public DateTime PreloadSince { get; set; } public int PreloadedCandlesCount { get; set; } public decimal Fee { get; set; } - public Scenario Scenario { get; set; } - public TradingBot( IExchangeService exchangeService, @@ -132,6 +129,9 @@ public class TradingBot : Bot, ITradingBot public void LoadScenario(string scenarioName) { + if (Config.Scenario != null) + return; + var scenario = TradingService.GetScenarioByName(scenarioName); if (scenario == null) { @@ -140,7 +140,6 @@ public class TradingBot : Bot, ITradingBot } else { - Scenario = scenario; LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario)); } } @@ -154,11 +153,15 @@ public class TradingBot : Bot, ITradingBot } else { - Scenario = scenario; LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario)); } } + public void LoadIndicators(Scenario scenario) + { + LoadIndicators(ScenarioHelpers.GetIndicatorsFromScenario(scenario)); + } + public void LoadIndicators(IEnumerable indicators) { foreach (var strategy in indicators) @@ -209,7 +212,7 @@ public class TradingBot : Bot, ITradingBot } UpdateWalletBalances(); - if (OptimizedCandles.Count % 100 == 0) // Log every 10th execution + if (!Config.IsForBacktest) // Log every 10th execution { Logger.LogInformation($"Candle date : {OptimizedCandles.Last().Date:u}"); Logger.LogInformation($"Signals : {Signals.Count}"); @@ -223,7 +226,7 @@ public class TradingBot : Bot, ITradingBot { foreach (var strategy in Indicators) { - IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetStrategyValues(); + IndicatorsValues[strategy.Type] = ((Indicator)strategy).GetIndicatorValues(); } } @@ -260,7 +263,10 @@ public class TradingBot : Bot, ITradingBot private async Task UpdateSignals(FixedSizeQueue candles) { - var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Scenario.LoopbackPeriod); + // If position open and not flipped, do not update signals + if (!Config.FlipPosition && Positions.Any(p => !p.IsFinished())) return; + + var signal = TradingBox.GetSignal(candles.ToHashSet(), Indicators, Signals, Config.Scenario.LoopbackPeriod); if (signal == null) return; signal.User = Account.User; @@ -272,11 +278,39 @@ public class TradingBot : Bot, ITradingBot if (Config.IsForWatchingOnly || (ExecutionCount < 1 && !Config.IsForBacktest)) signal.Status = SignalStatus.Expired; - Signals.Add(signal); - var signalText = $"{Config.ScenarioName} trigger a signal. Signal told you " + $"to {signal.Direction} {Config.Ticker} on {Config.Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}"; + + // Apply Synth-based signal filtering if enabled + if (Config.UseSynthApi) + { + var currentPrice = Config.IsForBacktest + ? OptimizedCandles.Last().Close + : ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + + var signalValidationResult = TradingService.ValidateSynthSignalAsync(signal, currentPrice, Config, + Config.IsForBacktest).GetAwaiter().GetResult(); + + if (signalValidationResult.Confidence == Confidence.None || + signalValidationResult.Confidence == Confidence.Low || + signalValidationResult.IsBlocked) + { + signal.Status = SignalStatus.Expired; + await LogInformation( + $"🚫 **Synth Signal Filter** - Signal {signal.Identifier} blocked by Synth risk assessment. Context : {signalValidationResult.ValidationContext}"); + return; + } + else + { + signal.SetConfidence(signalValidationResult.Confidence); + signalText += + $" and Synth risk assessment passed. Context : {signalValidationResult.ValidationContext}"; + } + } + + Signals.Add(signal); + Logger.LogInformation(signalText); if (Config.IsForWatchingOnly && !Config.IsForBacktest && ExecutionCount > 0) @@ -326,7 +360,8 @@ public class TradingBot : Bot, ITradingBot date: position.Open.Date, exchange: Account.Exchange, indicatorType: IndicatorType.Stc, // Use a valid strategy type for recreated signals - signalType: SignalType.Signal + signalType: SignalType.Signal, + indicatorName: "RecreatedSignal" ); // Since Signal identifier is auto-generated, we need to update our position @@ -414,8 +449,6 @@ public class TradingBot : Bot, ITradingBot { try { - Logger.LogInformation($"📊 **Position Update**\nUpdating position: `{positionForSignal.SignalIdentifier}`"); - var position = Config.IsForBacktest ? positionForSignal : TradingService.GetPositionByIdentifier(positionForSignal.Identifier); @@ -624,6 +657,38 @@ public class TradingBot : Bot, ITradingBot await OpenPosition(signal); } } + + // Synth-based position monitoring for liquidation risk + if (Config.UseSynthApi && !Config.IsForBacktest && + positionForSignal.Status == PositionStatus.Filled) + { + var currentPrice = ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + var riskResult = await TradingService.MonitorSynthPositionRiskAsync( + Config.Ticker, + positionForSignal.OriginDirection, + currentPrice, + positionForSignal.StopLoss.Price, + positionForSignal.Identifier, + Config); + + if (riskResult != null && riskResult.ShouldWarn && !string.IsNullOrEmpty(riskResult.WarningMessage)) + { + await LogWarning(riskResult.WarningMessage); + } + + if (riskResult.ShouldAutoClose && !string.IsNullOrEmpty(riskResult.EmergencyMessage)) + { + await LogWarning(riskResult.EmergencyMessage); + + var signalForAutoClose = + Signals.FirstOrDefault(s => s.Identifier == positionForSignal.SignalIdentifier); + if (signalForAutoClose != null) + { + await CloseTrade(signalForAutoClose, positionForSignal, positionForSignal.StopLoss, + currentPrice, true); + } + } + } } catch (Exception ex) { @@ -784,6 +849,20 @@ public class TradingBot : Bot, ITradingBot return false; } + // Synth-based pre-trade risk assessment + if (Config.UseSynthApi) + { + var currentPrice = Config.IsForBacktest + ? OptimizedCandles.Last().Close + : ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + + if (!(await TradingService.AssessSynthPositionRiskAsync(Config.Ticker, signal.Direction, currentPrice, + Config, Config.IsForBacktest))) + { + return false; + } + } + // Check cooldown period and loss streak return await CheckCooldownPeriod(signal) && await CheckLossStreak(signal); } @@ -1030,7 +1109,7 @@ public class TradingBot : Bot, ITradingBot { // Add PnL (could be positive or negative) Config.BotTradingBalance += position.ProfitAndLoss.Realized; - + Logger.LogInformation( $"💰 **Balance Updated**\nNew bot trading balance: `${Config.BotTradingBalance:F2}`"); } @@ -1150,7 +1229,7 @@ public class TradingBot : Bot, ITradingBot public decimal GetTotalFees() { decimal totalFees = 0; - + foreach (var position in Positions.Where(p => p.Open.Price > 0 && p.Open.Quantity > 0)) { totalFees += CalculatePositionFees(position); @@ -1167,22 +1246,22 @@ public class TradingBot : Bot, ITradingBot private decimal CalculatePositionFees(Position position) { decimal fees = 0; - + // Calculate position size in USD (leverage is already included in quantity calculation) var positionSizeUsd = position.Open.Price * position.Open.Quantity; - + // UI Fee: 0.1% of position size paid BOTH on opening AND closing var uiFeeRate = 0.001m; // 0.1% - var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening - var uiFeeClose = positionSizeUsd * uiFeeRate; // Fee paid on closing - var totalUiFees = uiFeeOpen + uiFeeClose; // Total: 0.2% of position size + var uiFeeOpen = positionSizeUsd * uiFeeRate; // Fee paid on opening + var uiFeeClose = positionSizeUsd * uiFeeRate; // Fee paid on closing + var totalUiFees = uiFeeOpen + uiFeeClose; // Total: 0.2% of position size fees += totalUiFees; - + // Network Fee: $0.50 for opening position only // Closing is handled by oracle, so no network fee for closing var networkFeeForOpening = 0.50m; fees += networkFeeForOpening; - + return fees; } @@ -1236,53 +1315,29 @@ public class TradingBot : Bot, ITradingBot { var data = new TradingBotBackup { - Name = Name, - BotType = Config.BotType, + Config = Config, Signals = Signals, Positions = Positions, - Timeframe = Config.Timeframe, - Ticker = Config.Ticker, - ScenarioName = Config.ScenarioName, - AccountName = Config.AccountName, - IsForWatchingOnly = Config.IsForWatchingOnly, WalletBalances = WalletBalances, - MoneyManagement = Config.MoneyManagement, - BotTradingBalance = Config.BotTradingBalance, - StartupTime = StartupTime, - CooldownPeriod = Config.CooldownPeriod, - MaxLossStreak = Config.MaxLossStreak, - MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m, - FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, - CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable, + StartupTime = StartupTime }; - BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data)); + BotService.SaveOrUpdateBotBackup(User, Identifier, Status, JsonConvert.SerializeObject(data)); } public override void LoadBackup(BotBackup backup) { var data = JsonConvert.DeserializeObject(backup.Data); - Config = new TradingBotConfig - { - AccountName = data.AccountName, - MoneyManagement = data.MoneyManagement, - Ticker = data.Ticker, - ScenarioName = data.ScenarioName, - Timeframe = data.Timeframe, - IsForBacktest = false, // Always false when loading from backup - IsForWatchingOnly = data.IsForWatchingOnly, - BotTradingBalance = data.BotTradingBalance, - BotType = data.BotType, - CooldownPeriod = data.CooldownPeriod, - MaxLossStreak = data.MaxLossStreak, - MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours, - FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit, - CloseEarlyWhenProfitable = data.CloseEarlyWhenProfitable, - Name = data.Name - }; - Signals = data.Signals; - Positions = data.Positions; - WalletBalances = data.WalletBalances; + // Load the configuration directly + Config = data.Config; + + // Ensure IsForBacktest is always false when loading from backup + Config.IsForBacktest = false; + + // Load runtime state + Signals = data.Signals ?? new HashSet(); + Positions = data.Positions ?? new List(); + WalletBalances = data.WalletBalances ?? new Dictionary(); PreloadSince = data.StartupTime; Identifier = backup.Identifier; User = backup.User; @@ -1307,7 +1362,7 @@ public class TradingBot : Bot, ITradingBot // Create a fake signal for manual position opening var signal = new Signal(Config.Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date, TradingExchanges.GmxV2, - IndicatorType.Stc, SignalType.Signal); + IndicatorType.Stc, SignalType.Signal, "Manual Signal"); signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct signal.User = Account.User; // Assign user @@ -1433,7 +1488,7 @@ public class TradingBot : Bot, ITradingBot } // If scenario changed, reload it - var currentScenario = Scenario?.Name; + var currentScenario = Config.Scenario?.Name; if (Config.ScenarioName != currentScenario) { LoadScenario(Config.ScenarioName); @@ -1485,29 +1540,36 @@ public class TradingBot : Bot, ITradingBot FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit, FlipPosition = Config.FlipPosition, Name = Config.Name, - CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable + CloseEarlyWhenProfitable = Config.CloseEarlyWhenProfitable, + UseSynthApi = Config.UseSynthApi, }; } } public class TradingBotBackup { - public string Name { get; set; } - public BotType BotType { get; set; } + /// + /// The complete trading bot configuration + /// + public TradingBotConfig Config { get; set; } + + /// + /// Runtime state: Active signals for the bot + /// public HashSet Signals { get; set; } + + /// + /// Runtime state: Open and closed positions for the bot + /// public List Positions { get; set; } - public Timeframe Timeframe { get; set; } - public Ticker Ticker { get; set; } - public string ScenarioName { get; set; } - public string AccountName { get; set; } - public bool IsForWatchingOnly { get; set; } + + /// + /// Runtime state: Historical wallet balances over time + /// public Dictionary WalletBalances { get; set; } - public MoneyManagement MoneyManagement { get; set; } + + /// + /// Runtime state: When the bot was started + /// public DateTime StartupTime { get; set; } - public decimal BotTradingBalance { get; set; } - public int CooldownPeriod { get; set; } - public int MaxLossStreak { get; set; } - public decimal MaxPositionTimeHours { get; set; } - public bool FlipOnlyWhenInProfit { get; set; } - public bool CloseEarlyWhenProfitable { get; set; } } \ No newline at end of file diff --git a/src/Managing.Application/ManageBot/BotService.cs b/src/Managing.Application/ManageBot/BotService.cs index 886c25b..6177de7 100644 --- a/src/Managing.Application/ManageBot/BotService.cs +++ b/src/Managing.Application/ManageBot/BotService.cs @@ -45,7 +45,7 @@ namespace Managing.Application.ManageBot return _botRepository.GetBots().FirstOrDefault(b => b.Identifier == identifier); } - public void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data) + public void SaveOrUpdateBotBackup(User user, string identifier, BotStatus status, string data) { var backup = GetBotBackup(identifier); @@ -62,7 +62,6 @@ namespace Managing.Application.ManageBot LastStatus = status, User = user, Identifier = identifier, - BotType = botType, Data = data }; @@ -118,40 +117,29 @@ namespace Managing.Application.ManageBot object bot = null; Task botTask = null; - switch (backupBot.BotType) + var scalpingBotData = JsonConvert.DeserializeObject(backupBot.Data); + + // Get the config directly from the backup + var scalpingConfig = scalpingBotData.Config; + + // Ensure the money management is properly loaded from database if needed + if (scalpingConfig.MoneyManagement != null && + !string.IsNullOrEmpty(scalpingConfig.MoneyManagement.Name)) { - case BotType.ScalpingBot: - case BotType.FlippingBot: - var scalpingBotData = JsonConvert.DeserializeObject(backupBot.Data); - var scalpingMoneyManagement = - _moneyManagementService.GetMoneyMangement(scalpingBotData.MoneyManagement.Name).Result; - - // Create config from backup data - var scalpingConfig = new TradingBotConfig - { - AccountName = scalpingBotData.AccountName, - MoneyManagement = scalpingMoneyManagement, - Ticker = scalpingBotData.Ticker, - ScenarioName = scalpingBotData.ScenarioName, - Timeframe = scalpingBotData.Timeframe, - IsForWatchingOnly = scalpingBotData.IsForWatchingOnly, - BotTradingBalance = scalpingBotData.BotTradingBalance, - BotType = scalpingBotData.BotType, - Name = scalpingBotData.Name, - CooldownPeriod = scalpingBotData.CooldownPeriod, - MaxLossStreak = scalpingBotData.MaxLossStreak, - MaxPositionTimeHours = scalpingBotData.MaxPositionTimeHours == 0m ? null : scalpingBotData.MaxPositionTimeHours, - FlipOnlyWhenInProfit = scalpingBotData.FlipOnlyWhenInProfit, - IsForBacktest = false, - FlipPosition = false, - CloseEarlyWhenProfitable = scalpingBotData.CloseEarlyWhenProfitable - }; - - bot = CreateTradingBot(scalpingConfig); - botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot)); - break; + var moneyManagement = _moneyManagementService + .GetMoneyMangement(scalpingConfig.MoneyManagement.Name).Result; + if (moneyManagement != null) + { + scalpingConfig.MoneyManagement = moneyManagement; + } } + // Ensure critical properties are set correctly for restored bots + scalpingConfig.IsForBacktest = false; + + bot = CreateTradingBot(scalpingConfig); + botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot)); + if (bot != null && botTask != null) { var botWrapper = new BotTaskWrapper(botTask, bot.GetType(), bot); @@ -258,14 +246,14 @@ namespace Managing.Application.ManageBot // Update the bot configuration first var updateResult = await tradingBot.UpdateConfiguration(newConfig, allowNameChange: true); - + if (updateResult) { // Update the dictionary key if (_botTasks.TryRemove(identifier, out var removedWrapper)) { _botTasks.TryAdd(newConfig.Name, removedWrapper); - + // Update the backup with the new identifier if (!newConfig.IsForBacktest) { @@ -275,7 +263,7 @@ namespace Managing.Application.ManageBot } } } - + return updateResult; } else @@ -288,7 +276,6 @@ namespace Managing.Application.ManageBot return false; } - public ITradingBot CreateTradingBot(TradingBotConfig config) { diff --git a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs index dfcdbe8..f9b74d5 100644 --- a/src/Managing.Application/ManageBot/StartBotCommandHandler.cs +++ b/src/Managing.Application/ManageBot/StartBotCommandHandler.cs @@ -52,7 +52,9 @@ namespace Managing.Application.ManageBot var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString()); - if (usdcBalance == null || usdcBalance.Value < request.Config.BotTradingBalance) + if (usdcBalance == null || + usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount || + usdcBalance.Value < request.Config.BotTradingBalance) { throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance"); } @@ -64,12 +66,14 @@ namespace Managing.Application.ManageBot MoneyManagement = request.Config.MoneyManagement, Ticker = request.Config.Ticker, ScenarioName = request.Config.ScenarioName, + Scenario = request.Config.Scenario, Timeframe = request.Config.Timeframe, IsForWatchingOnly = request.Config.IsForWatchingOnly, BotTradingBalance = request.Config.BotTradingBalance, BotType = request.Config.BotType, IsForBacktest = request.Config.IsForBacktest, - CooldownPeriod = request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set + CooldownPeriod = + request.Config.CooldownPeriod > 0 ? request.Config.CooldownPeriod : 1, // Default to 1 if not set MaxLossStreak = request.Config.MaxLossStreak, MaxPositionTimeHours = request.Config.MaxPositionTimeHours, // Properly handle nullable value FlipOnlyWhenInProfit = request.Config.FlipOnlyWhenInProfit, @@ -85,15 +89,15 @@ namespace Managing.Application.ManageBot bot.User = request.User; _botService.AddSimpleBotToCache(bot); return bot.GetStatus(); - + case BotType.ScalpingBot: case BotType.FlippingBot: var tradingBot = _botFactory.CreateTradingBot(configToUse); tradingBot.User = request.User; - + // Log the configuration being used await LogBotConfigurationAsync(tradingBot, $"{configToUse.BotType} created"); - + _botService.AddTradingBotToCache(tradingBot); return tradingBot.GetStatus(); } @@ -112,16 +116,16 @@ namespace Managing.Application.ManageBot { var config = bot.GetConfiguration(); var logMessage = $"{context} - Bot: {config.Name}, " + - $"Type: {config.BotType}, " + - $"Account: {config.AccountName}, " + - $"Ticker: {config.Ticker}, " + - $"Balance: {config.BotTradingBalance}, " + - $"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + - $"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " + - $"FlipPosition: {config.FlipPosition}, " + - $"Cooldown: {config.CooldownPeriod}, " + - $"MaxLoss: {config.MaxLossStreak}"; - + $"Type: {config.BotType}, " + + $"Account: {config.AccountName}, " + + $"Ticker: {config.Ticker}, " + + $"Balance: {config.BotTradingBalance}, " + + $"MaxTime: {config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + + $"FlipOnlyProfit: {config.FlipOnlyWhenInProfit}, " + + $"FlipPosition: {config.FlipPosition}, " + + $"Cooldown: {config.CooldownPeriod}, " + + $"MaxLoss: {config.MaxLossStreak}"; + // Log through the bot's logger (this will use the bot's logging mechanism) // For now, we'll just add a comment that this could be enhanced with actual logging // Console.WriteLine(logMessage); // Could be replaced with proper logging diff --git a/src/Managing.Application/Scenarios/ScenarioService.cs b/src/Managing.Application/Scenarios/ScenarioService.cs index 45c2ed6..daa1ee1 100644 --- a/src/Managing.Application/Scenarios/ScenarioService.cs +++ b/src/Managing.Application/Scenarios/ScenarioService.cs @@ -79,7 +79,7 @@ namespace Managing.Application.Scenarios return _tradingService.GetScenarioByName(name); } - public IEnumerable GetStrategies() + public IEnumerable GetIndicators() { return _tradingService.GetStrategies(); } diff --git a/src/Managing.Application/Synth/SynthApiClient.cs b/src/Managing.Application/Synth/SynthApiClient.cs new file mode 100644 index 0000000..c08ca70 --- /dev/null +++ b/src/Managing.Application/Synth/SynthApiClient.cs @@ -0,0 +1,324 @@ +using System.Text.Json; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Synth.Models; +using Microsoft.Extensions.Logging; + +namespace Managing.Application.Synth; + +/// +/// Client for communicating with the Synth API +/// +public class SynthApiClient : ISynthApiClient, IDisposable +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + // Private configuration - should come from app settings or environment variables + private readonly string _apiKey; + private readonly string _baseUrl; + + public SynthApiClient(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + // TODO: These should come from IConfiguration or environment variables + _apiKey = Environment.GetEnvironmentVariable("SYNTH_API_KEY") ?? + "bfd2a078b412452af2e01ca74b2a7045d4ae411a85943342"; + _baseUrl = Environment.GetEnvironmentVariable("SYNTH_BASE_URL") ?? "https://api.synthdata.co"; + + // Configure HttpClient once + ConfigureHttpClient(); + + // Configure JSON options + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + + /// + /// Configures the HTTP client with API settings + /// + private void ConfigureHttpClient() + { + // Validate API configuration + if (string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_baseUrl)) + { + throw new InvalidOperationException( + "Synth API configuration is missing. Please set SYNTH_API_KEY and SYNTH_BASE_URL environment variables."); + } + + // Set base address and authorization + _httpClient.BaseAddress = new Uri(_baseUrl); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Apikey {_apiKey}"); + _httpClient.Timeout = TimeSpan.FromSeconds(30); + } + + /// + /// Fetches the current leaderboard from Synth API + /// + public async Task> GetLeaderboardAsync(SynthConfiguration config) + { + try + { + _logger.LogInformation("🔍 **Synth API** - Fetching leaderboard"); + + var response = await _httpClient.GetAsync("/leaderboard/latest"); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning( + $"Synth API leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}"); + return new List(); + } + + var jsonContent = await response.Content.ReadAsStringAsync(); + var miners = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); + + _logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from leaderboard"); + + return miners ?? new List(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while fetching Synth leaderboard"); + return new List(); + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + _logger.LogError(ex, "Timeout while fetching Synth leaderboard"); + return new List(); + } + catch (JsonException ex) + { + _logger.LogError(ex, "JSON deserialization error while parsing Synth leaderboard"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while fetching Synth leaderboard"); + return new List(); + } + } + + /// + /// Fetches historical leaderboard data from Synth API for a specific time range + /// + public async Task> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config) + { + try + { + // Format dates to ISO 8601 format as required by the API + var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ")); + var endTimeStr = Uri.EscapeDataString(endTime.ToString("yyyy-MM-ddTHH:mm:ssZ")); + + var url = $"/leaderboard/historical?start_time={startTimeStr}&end_time={endTimeStr}"; + + _logger.LogInformation($"🔍 **Synth API** - Fetching historical leaderboard from {startTime:yyyy-MM-dd HH:mm} to {endTime:yyyy-MM-dd HH:mm}"); + + var response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning( + $"Synth API historical leaderboard request failed: {response.StatusCode} - {response.ReasonPhrase}"); + return new List(); + } + + var jsonContent = await response.Content.ReadAsStringAsync(); + var miners = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); + + _logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from historical leaderboard"); + + return miners ?? new List(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while fetching Synth historical leaderboard"); + return new List(); + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + _logger.LogError(ex, "Timeout while fetching Synth historical leaderboard"); + return new List(); + } + catch (JsonException ex) + { + _logger.LogError(ex, "JSON deserialization error while parsing Synth historical leaderboard"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while fetching Synth historical leaderboard"); + return new List(); + } + } + + /// + /// Fetches latest predictions from specified miners + /// + public async Task> GetMinerPredictionsAsync( + List minerUids, + string asset, + int timeIncrement, + int timeLength, + SynthConfiguration config) + { + if (minerUids == null || !minerUids.Any()) + { + _logger.LogWarning("No miner UIDs provided for prediction request"); + return new List(); + } + + try + { + // Build URL with proper array formatting for miner parameter + var queryParams = new List + { + $"asset={Uri.EscapeDataString(asset)}", + $"time_increment={timeIncrement}", + $"time_length={timeLength}" + }; + + // Add each miner UID as a separate parameter (standard array query parameter format) + foreach (var minerUid in minerUids) + { + queryParams.Add($"miner={minerUid}"); + } + + var url = $"/prediction/latest?{string.Join("&", queryParams)}"; + + _logger.LogInformation( + $"🔮 **Synth API** - Fetching predictions for {minerUids.Count} miners, asset: {asset}, time: {timeLength}s"); + + var response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning( + $"Synth API predictions request failed: {response.StatusCode} - {response.ReasonPhrase}"); + return new List(); + } + + var jsonContent = await response.Content.ReadAsStringAsync(); + var predictions = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); + + var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0; + _logger.LogInformation( + $"📈 **Synth API** - Retrieved {predictions?.Count ?? 0} predictions with {totalPaths} total simulation paths"); + + return predictions ?? new List(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, $"HTTP error while fetching Synth predictions for {asset}"); + return new List(); + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + _logger.LogError(ex, $"Timeout while fetching Synth predictions for {asset}"); + return new List(); + } + catch (JsonException ex) + { + _logger.LogError(ex, $"JSON deserialization error while parsing Synth predictions for {asset}"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Unexpected error while fetching Synth predictions for {asset}"); + return new List(); + } + } + + /// + /// Fetches historical predictions from specified miners for a specific time point + /// + public async Task> GetHistoricalMinerPredictionsAsync( + List minerUids, + string asset, + DateTime startTime, + int timeIncrement, + int timeLength, + SynthConfiguration config) + { + if (minerUids == null || !minerUids.Any()) + { + _logger.LogWarning("No miner UIDs provided for historical prediction request"); + return new List(); + } + + try + { + // Format start time to ISO 8601 format as required by the API + var startTimeStr = Uri.EscapeDataString(startTime.ToString("yyyy-MM-ddTHH:mm:ssZ")); + + // Build URL with proper array formatting for miner parameter + var queryParams = new List + { + $"asset={Uri.EscapeDataString(asset)}", + $"start_time={startTimeStr}", + $"time_increment={timeIncrement}", + $"time_length={timeLength}" + }; + + // Add each miner UID as a separate parameter (standard array query parameter format) + foreach (var minerUid in minerUids) + { + queryParams.Add($"miner={minerUid}"); + } + + var url = $"/prediction/historical?{string.Join("&", queryParams)}"; + + _logger.LogInformation( + $"🔮 **Synth API** - Fetching historical predictions for {minerUids.Count} miners, asset: {asset}, time: {startTime:yyyy-MM-dd HH:mm}, duration: {timeLength}s"); + + var response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning( + $"Synth API historical predictions request failed: {response.StatusCode} - {response.ReasonPhrase}"); + return new List(); + } + + var jsonContent = await response.Content.ReadAsStringAsync(); + var predictions = JsonSerializer.Deserialize>(jsonContent, _jsonOptions); + + var totalPaths = predictions?.Sum(p => p.NumSimulations) ?? 0; + _logger.LogInformation( + $"📈 **Synth API** - Retrieved {predictions?.Count ?? 0} historical predictions with {totalPaths} total simulation paths"); + + return predictions ?? new List(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, $"HTTP error while fetching Synth historical predictions for {asset}"); + return new List(); + } + catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException) + { + _logger.LogError(ex, $"Timeout while fetching Synth historical predictions for {asset}"); + return new List(); + } + catch (JsonException ex) + { + _logger.LogError(ex, $"JSON deserialization error while parsing Synth historical predictions for {asset}"); + return new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Unexpected error while fetching Synth historical predictions for {asset}"); + return new List(); + } + } + + public void Dispose() + { + _httpClient?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Managing.Application/Synth/SynthConfigurationHelper.cs b/src/Managing.Application/Synth/SynthConfigurationHelper.cs new file mode 100644 index 0000000..0d88d3f --- /dev/null +++ b/src/Managing.Application/Synth/SynthConfigurationHelper.cs @@ -0,0 +1,169 @@ +using Managing.Domain.Synth.Models; + +namespace Managing.Application.Synth; + +/// +/// Helper class for creating and configuring Synth API integration +/// +public static class SynthConfigurationHelper +{ + /// + /// Creates a default Synth configuration for live trading + /// + /// A configured SynthConfiguration instance + public static SynthConfiguration CreateLiveTradingConfig() + { + return new SynthConfiguration + { + IsEnabled = true, + TopMinersCount = 10, + TimeIncrement = 300, // 5 minutes + DefaultTimeLength = 86400, // 24 hours + MaxLiquidationProbability = 0.10m, // 10% max risk + PredictionCacheDurationMinutes = 5, + UseForPositionSizing = true, + UseForSignalFiltering = true, + UseForDynamicStopLoss = true + }; + } + + /// + /// Creates a conservative Synth configuration with lower risk tolerances + /// + /// A conservative SynthConfiguration instance + public static SynthConfiguration CreateConservativeConfig() + { + return new SynthConfiguration + { + IsEnabled = true, + TopMinersCount = 10, + TimeIncrement = 300, // 5 minutes + DefaultTimeLength = 86400, // 24 hours + MaxLiquidationProbability = 0.05m, // 5% max risk (more conservative) + PredictionCacheDurationMinutes = 3, // More frequent updates + UseForPositionSizing = true, + UseForSignalFiltering = true, + UseForDynamicStopLoss = true + }; + } + + /// + /// Creates an aggressive Synth configuration with higher risk tolerances + /// + /// An aggressive SynthConfiguration instance + public static SynthConfiguration CreateAggressiveConfig() + { + return new SynthConfiguration + { + IsEnabled = true, + TopMinersCount = 15, // More miners for broader consensus + TimeIncrement = 300, // 5 minutes + DefaultTimeLength = 86400, // 24 hours + MaxLiquidationProbability = 0.15m, // 15% max risk (more aggressive) + PredictionCacheDurationMinutes = 7, // Less frequent updates to reduce API calls + UseForPositionSizing = true, + UseForSignalFiltering = false, // Don't filter signals in aggressive mode + UseForDynamicStopLoss = true + }; + } + + /// + /// Creates a disabled Synth configuration (bot will operate without Synth predictions) + /// + /// A disabled SynthConfiguration instance + public static SynthConfiguration CreateDisabledConfig() + { + return new SynthConfiguration + { + IsEnabled = false, + TopMinersCount = 10, + TimeIncrement = 300, + DefaultTimeLength = 86400, + MaxLiquidationProbability = 0.10m, + PredictionCacheDurationMinutes = 5, + UseForPositionSizing = false, + UseForSignalFiltering = false, + UseForDynamicStopLoss = false + }; + } + + /// + /// Creates a Synth configuration optimized for backtesting (disabled) + /// + /// A backtesting-optimized SynthConfiguration instance + public static SynthConfiguration CreateBacktestConfig() + { + // Synth predictions are not available for historical data, so always disabled for backtests + return CreateDisabledConfig(); + } + + /// + /// Validates and provides suggestions for improving a Synth configuration + /// + /// The configuration to validate + /// List of validation messages and suggestions + public static List ValidateConfiguration(SynthConfiguration config) + { + var messages = new List(); + + if (config == null) + { + messages.Add("❌ Configuration is null"); + return messages; + } + + if (!config.IsEnabled) + { + messages.Add("ℹ️ Synth API is disabled - bot will operate without predictions"); + return messages; + } + + if (config.TopMinersCount <= 0) + { + messages.Add("❌ TopMinersCount must be greater than 0"); + } + else if (config.TopMinersCount > 20) + { + messages.Add("⚠️ TopMinersCount > 20 may result in slower performance and higher API usage"); + } + + if (config.TimeIncrement <= 0) + { + messages.Add("❌ TimeIncrement must be greater than 0"); + } + + if (config.DefaultTimeLength <= 0) + { + messages.Add("❌ DefaultTimeLength must be greater than 0"); + } + + if (config.MaxLiquidationProbability < 0 || config.MaxLiquidationProbability > 1) + { + messages.Add("❌ MaxLiquidationProbability must be between 0 and 1"); + } + else if (config.MaxLiquidationProbability < 0.02m) + { + messages.Add("⚠️ MaxLiquidationProbability < 2% is very conservative and may block many trades"); + } + else if (config.MaxLiquidationProbability > 0.20m) + { + messages.Add("⚠️ MaxLiquidationProbability > 20% is very aggressive and may increase risk"); + } + + if (config.PredictionCacheDurationMinutes <= 0) + { + messages.Add("❌ PredictionCacheDurationMinutes must be greater than 0"); + } + else if (config.PredictionCacheDurationMinutes < 1) + { + messages.Add("⚠️ Cache duration < 1 minute may result in excessive API calls"); + } + + if (messages.Count == 0) + { + messages.Add("✅ Configuration appears valid"); + } + + return messages; + } +} \ No newline at end of file diff --git a/src/Managing.Application/Synth/SynthPredictionService.cs b/src/Managing.Application/Synth/SynthPredictionService.cs new file mode 100644 index 0000000..3e4daf7 --- /dev/null +++ b/src/Managing.Application/Synth/SynthPredictionService.cs @@ -0,0 +1,1194 @@ +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; +using Managing.Domain.Risk; +using Managing.Domain.Strategies; +using Managing.Domain.Synth.Models; +using Microsoft.Extensions.Logging; +using static Managing.Common.Enums; + +namespace Managing.Application.Synth; + +/// +/// Service for Synth prediction business logic and probability calculations +/// Uses MongoDB persistence for caching to prevent RAM saturation and improve performance +/// +public class SynthPredictionService : ISynthPredictionService +{ + private readonly ISynthApiClient _synthApiClient; + private readonly ISynthRepository _synthRepository; + private readonly ILogger _logger; + + private readonly List _availableTickers = new List + { + Ticker.BTC, + Ticker.ETH, + }; + + /// + /// Builds Synth configuration based on timeframe and bot configuration + /// Different timeframes require different time horizons and update frequencies + /// Note: Synth API only supports 5 minutes (300s) and 24 hours (86400s) time increments + /// + /// Trading timeframe + /// Bot configuration with Synth settings + /// Optimized SynthConfiguration for the timeframe + public static SynthConfiguration BuildConfigurationForTimeframe(Timeframe timeframe, TradingBotConfig botConfig) + { + if (!botConfig.UseSynthApi) + { + return SynthConfigurationHelper.CreateDisabledConfig(); + } + + var config = new SynthConfiguration + { + IsEnabled = true, + TopMinersCount = 10, + UseForPositionSizing = botConfig.UseForPositionSizing, + UseForSignalFiltering = botConfig.UseForSignalFiltering, + UseForDynamicStopLoss = botConfig.UseForDynamicStopLoss, + MaxLiquidationProbability = 0.10m // 10% default + }; + + // Adjust configuration based on timeframe + // Note: API only supports 5 minutes (300s) and 24 hours (86400s) increments + switch (timeframe) + { + case Timeframe.OneMinute: + case Timeframe.FiveMinutes: + case Timeframe.FifteenMinutes: + case Timeframe.ThirtyMinutes: + // Short timeframes - use 5 minute increments with shorter horizons + config.TimeIncrement = 300; // 5 minutes (only supported short increment) + config.DefaultTimeLength = 14400; // 4 hour horizon + config.PredictionCacheDurationMinutes = 2; // Update every 2 minutes + break; + + case Timeframe.OneHour: + // Medium timeframes - use 5 minute increments with medium horizons + config.TimeIncrement = 300; // 5 minutes (only supported short increment) + config.DefaultTimeLength = 43200; // 12 hour horizon + config.PredictionCacheDurationMinutes = 5; // Update every 5 minutes + break; + + case Timeframe.FourHour: + case Timeframe.OneDay: + // Longer timeframes - use 24 hour increments with longer horizons + config.TimeIncrement = 86400; // 24 hours (only supported long increment) + config.DefaultTimeLength = 172800; // 48 hour horizon + config.PredictionCacheDurationMinutes = 15; // Update every 15 minutes + break; + + default: + // Default to 5 minute increments for unknown timeframes + config.TimeIncrement = 300; // 5 minutes + config.DefaultTimeLength = 43200; // 12 hour horizon + config.PredictionCacheDurationMinutes = 5; // Update every 5 minutes + break; + } + + return config; + } + + public SynthPredictionService( + ISynthApiClient synthApiClient, + ISynthRepository synthRepository, + ILogger logger) + { + _synthApiClient = synthApiClient ?? throw new ArgumentNullException(nameof(synthApiClient)); + _synthRepository = synthRepository ?? throw new ArgumentNullException(nameof(synthRepository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Calculates the probability of price reaching a target within a specified time horizon + /// + public async Task GetProbabilityOfTargetPriceAsync( + string asset, + decimal currentPrice, + decimal targetPrice, + int timeHorizonSeconds, + bool isLongPosition, + SynthConfiguration config) + { + if (!config.IsEnabled) + { + _logger.LogDebug("Synth API is disabled, returning 0 probability"); + return 0m; + } + + try + { + var predictions = + await GetCachedPredictionsAsync(asset, timeHorizonSeconds, config, false, DateTime.UtcNow); + if (!predictions.Any()) + { + _logger.LogWarning($"🚫 **Synth Probability** - No predictions available for {asset}"); + return 0m; + } + + var probability = CalculateProbabilityFromPaths(predictions, currentPrice, targetPrice, timeHorizonSeconds, + isLongPosition); + + _logger.LogInformation($"🎯 **Synth Probability** - {asset}\n" + + $"📊 Target: ${targetPrice:F2} | Current: ${currentPrice:F2}\n" + + $"⏱️ Horizon: {timeHorizonSeconds / 3600.0:F1}h | Position: {(isLongPosition ? "LONG" : "SHORT")}\n" + + $"🎲 Probability: {probability:P2}"); + + return probability; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error calculating probability for {asset}"); + return 0m; + } + } + + /// + /// Gets probabilities for multiple price thresholds at once + /// + public async Task> GetMultipleThresholdProbabilitiesAsync( + string asset, + decimal currentPrice, + Dictionary priceThresholds, + int timeHorizonSeconds, + bool isLongPosition, + SynthConfiguration config, + bool isBacktest, + DateTime signalDate) + { + var results = new Dictionary(); + + if (!config.IsEnabled || !priceThresholds.Any()) + { + return results; + } + + try + { + var predictions = + await GetCachedPredictionsAsync(asset, timeHorizonSeconds, config, isBacktest, signalDate); + if (!predictions.Any()) + { + _logger.LogWarning($"🚫 **Synth Multi-Threshold** - No predictions available for {asset}"); + return results; + } + + foreach (var threshold in priceThresholds) + { + var probability = CalculateProbabilityFromPaths(predictions, currentPrice, threshold.Value, + timeHorizonSeconds, isLongPosition); + results[threshold.Key] = probability; + } + + _logger.LogInformation($"🎯 **Synth Multi-Threshold** - {asset}\n" + + $"📊 Calculated {results.Count} threshold probabilities\n" + + $"⏱️ Horizon: {timeHorizonSeconds / 3600.0:F1}h | Position: {(isLongPosition ? "LONG" : "SHORT")} {(isBacktest ? "(HISTORICAL)" : "(LIVE)")}"); + + return results; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error calculating multiple threshold probabilities for {asset}"); + return results; + } + } + + /// + /// Clears cached predictions from MongoDB + /// + public async Task ClearCacheAsync() + { + await _synthRepository.CleanupOldDataAsync(0); // Remove all data + _logger.LogInformation("🧹 **Synth Cache** - Cleared all cached individual predictions from MongoDB"); + } + + /// + /// Legacy method for backward compatibility + /// + public void ClearCache() + { + // For backward compatibility - schedule async cleanup + _ = Task.Run(async () => await ClearCacheAsync()); + } + + /// + /// Validates a trading signal using Synth predictions to check for adverse price movements + /// Returns comprehensive validation result with confidence, probabilities, and risk analysis + /// + /// The trading signal containing ticker, direction, candle data, and other context + /// Current market price (required) + /// Trading bot configuration + /// Whether this is a backtest scenario + /// Custom probability thresholds for decision-making. If null, uses default thresholds. + /// Comprehensive signal validation result including confidence, probabilities, ratio, and blocking status + public async Task ValidateSignalAsync(Signal signal, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest, Dictionary customThresholds = null) + { + var config = BuildConfigurationForTimeframe(botConfig.Timeframe, botConfig); + + if (!config.IsEnabled || !config.UseForSignalFiltering || !_availableTickers.Contains(signal.Ticker)) + { + return SignalValidationResult.CreateDisabledResult(signal.Confidence); + } + + try + { + var asset = GetAssetSymbolFromTicker(signal.Ticker); + var timeHorizon = 86400; // 24 hours for signal validation (API supported increment) + + // Calculate dynamic price thresholds based on money management settings + var priceThresholds = new Dictionary(); + + // Get SL and TP percentages from money management (they are stored as decimals, e.g., 0.05 for 5%) + var stopLossPercentage = botConfig.MoneyManagement.StopLoss; + var takeProfitPercentage = botConfig.MoneyManagement.TakeProfit; + + if (signal.Direction == TradeDirection.Long) + { + // For LONG positions, check downward movement risks + priceThresholds[$"SL Drop ({stopLossPercentage:P1})"] = currentPrice * (1 - stopLossPercentage); + priceThresholds[$"TP Rise ({takeProfitPercentage:P1})"] = currentPrice * (1 + takeProfitPercentage); + } + else + { + // For SHORT positions, check upward movement risks + priceThresholds[$"SL Rise ({stopLossPercentage:P1})"] = currentPrice * (1 + stopLossPercentage); + priceThresholds[$"TP Drop ({takeProfitPercentage:P1})"] = currentPrice * (1 - takeProfitPercentage); + } + + var probabilities = await GetMultipleThresholdProbabilitiesAsync( + asset, currentPrice, priceThresholds, timeHorizon, signal.Direction == TradeDirection.Long, config, + isBacktest, signal.Date); + + // Debug logging to understand LONG vs SHORT differences + _logger.LogInformation($"🔍 **Debug Signal Direction Analysis** - {signal.Direction} {signal.Ticker}\n" + + $"💰 Current Price: ${currentPrice:F2}\n" + + $"📊 Price Targets Requested:\n" + + $" {string.Join("\n ", priceThresholds.Select(kvp => $"{kvp.Key}: ${kvp.Value:F2}"))}\n" + + $"📈 API Probabilities Returned:\n" + + $" {string.Join("\n ", probabilities.Select(kvp => $"{kvp.Key}: {kvp.Value:P2}"))}\n" + + $"🎯 Position Type Used for API: {(signal.Direction == TradeDirection.Long ? "LONG" : "SHORT")}"); + + // Use RiskManagement configuration if available, otherwise fall back to custom thresholds or defaults + var riskConfig = botConfig.RiskManagement ?? new RiskManagement(); + + var adverseProbabilityThreshold = + customThresholds?.GetValueOrDefault("AdverseProbabilityThreshold", + riskConfig.AdverseProbabilityThreshold) ?? + riskConfig.AdverseProbabilityThreshold; + + var favorableProbabilityThreshold = + customThresholds?.GetValueOrDefault("FavorableProbabilityThreshold", + riskConfig.FavorableProbabilityThreshold) ?? + riskConfig.FavorableProbabilityThreshold; + + // Get Stop Loss and Take Profit probabilities + string slKey = signal.Direction == TradeDirection.Long + ? $"SL Drop ({stopLossPercentage:P1})" + : $"SL Rise ({stopLossPercentage:P1})"; + + string tpKey = signal.Direction == TradeDirection.Long + ? $"TP Rise ({takeProfitPercentage:P1})" + : $"TP Drop ({takeProfitPercentage:P1})"; + + var slProbability = probabilities.GetValueOrDefault(slKey, 0m); + var tpProbability = probabilities.GetValueOrDefault(tpKey, 0m); + + // Calculate TP/SL ratio + var tpSlRatio = slProbability > 0 + ? tpProbability / slProbability + : (tpProbability > 0 ? decimal.MaxValue : 0m); + + // Calculate monetary gains and losses for Expected Utility Theory + decimal takeProfitGain = 0m; + decimal stopLossLoss = 0m; + + if (signal.Direction == TradeDirection.Long) + { + // For LONG positions + var tpPrice = currentPrice * (1 + takeProfitPercentage); + var slPrice = currentPrice * (1 - stopLossPercentage); + takeProfitGain = tpPrice - currentPrice; + stopLossLoss = currentPrice - slPrice; + } + else + { + // For SHORT positions + var tpPrice = currentPrice * (1 - takeProfitPercentage); + var slPrice = currentPrice * (1 + stopLossPercentage); + takeProfitGain = currentPrice - tpPrice; + stopLossLoss = slPrice - currentPrice; + } + + var result = new SignalValidationResult + { + Confidence = Confidence.Medium, // Temporary - will be calculated using full object + StopLossProbability = slProbability, + TakeProfitProbability = tpProbability, + TpSlRatio = tpSlRatio, + IsBlocked = false, // Temporary - will be determined after full analysis + AdverseProbabilityThreshold = adverseProbabilityThreshold, + TimeHorizonSeconds = timeHorizon, + UsedCustomThresholds = customThresholds != null, + TakeProfitGain = takeProfitGain, + StopLossLoss = stopLossLoss, + ValidationContext = + $"{signal.Direction} {signal.Ticker} - SL: {slProbability:P2}, TP: {tpProbability:P2}, Ratio: {tpSlRatio:F2}x, EMV: TBD" + }; + + // Calculate Expected Utility Theory metrics using RiskManagement configuration + result.CalculateExpectedMetrics( + botConfig.BotTradingBalance, // Use actual bot trading balance + riskConfig); // Pass complete risk configuration + + // Now calculate confidence using the full SignalValidationResult object + var finalConfidence = CalculateSignalConfidenceFromResult(result, riskConfig); + result.Confidence = finalConfidence; + result.IsBlocked = finalConfidence == Confidence.None; + + // Update validation context with comprehensive analysis + result.ValidationContext = + $"{signal.Direction} {signal.Ticker} - SL: {slProbability:P2}, TP: {tpProbability:P2}, " + + $"Ratio: {tpSlRatio:F2}x, EMV: ${result.ExpectedMonetaryValue:F2}, " + + $"EU: {result.ExpectedUtility:F4}, Kelly: {result.KellyFraction:P2}, " + + $"Confidence: {finalConfidence}, Risk: {result.GetUtilityRiskAssessment()}"; + + _logger.LogInformation( + $"🎯 **Synth Signal Analysis** - {signal.Direction} {signal.Ticker} {(isBacktest ? "(BACKTEST)" : "(LIVE)")}\n" + + $"📊 Signal: `{signal.Identifier}`\n" + + $"📊 SL Risk: {slProbability:P2} | TP Probability: {tpProbability:P2}\n" + + $"🎲 Ratio: {tpSlRatio:F2}x (TP/SL) | Win/Loss: {result.WinLossRatio:F2}:1\n" + + $"💰 Expected Monetary Value: ${result.ExpectedMonetaryValue:F2}\n" + + $"🧮 Expected Utility: {result.ExpectedUtility:F4} | Utility/Risk Ratio: {result.UtilityRiskRatio:F4}\n" + + $"📈 Gains/Losses: TP +${takeProfitGain:F2} | SL -${stopLossLoss:F2}\n" + + $"🎯 Kelly Criterion: {result.KellyFraction:P2} (Capped: {result.KellyCappedFraction:P2})\n" + + $"📊 Kelly Assessment: {result.KellyAssessment}\n" + + $"🎯 Confidence: {finalConfidence} | Blocked: {result.IsBlocked} | Kelly Favorable: {result.IsKellyFavorable(riskConfig)}\n" + + $"📋 Thresholds: Adverse {adverseProbabilityThreshold:P2} | Favorable {favorableProbabilityThreshold:P2} | Custom: {(customThresholds != null ? "Yes" : "No")}\n" + + $"🔍 Risk Assessment: {result.GetUtilityRiskAssessment()} | Risk Tolerance: {riskConfig.RiskTolerance}\n" + + $"💰 Price: ${currentPrice:F2} | Signal Date: {signal.Date:HH:mm:ss}\n" + + $"⏱️ Time horizon: {riskConfig.SignalValidationTimeHorizonHours}h"); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, + $"Error in Synth signal validation for signal {signal.Identifier}, returning fallback confidence"); + return SignalValidationResult.CreateErrorResult(signal.Confidence, ex.Message); + } + } + + /// + /// Calculates signal confidence based on the full SignalValidationResult object + /// Uses Expected Utility Theory, Kelly Criterion, and probability analysis for comprehensive assessment + /// + /// The SignalValidationResult object with calculated metrics + /// The RiskManagement configuration + /// Calculated confidence level + private Confidence CalculateSignalConfidenceFromResult(SignalValidationResult result, RiskManagement riskConfig) + { + // Enhanced blocking logic - allow conditional pass-through for signals with positive elements + var isOverAdverseThreshold = result.StopLossProbability > result.AdverseProbabilityThreshold; + + if (isOverAdverseThreshold) + { + // Check if signal has redeeming qualities that warrant LOW confidence instead of blocking + var hasRedeemingQualities = CheckForRedeemingQualities(result, riskConfig); + + if (!hasRedeemingQualities) + { + // Complete block for signals with no redeeming qualities + return Confidence.None; + } + + // Signal exceeded adverse threshold but has positive elements - force to LOW confidence maximum + var constrainedScore = CalculateConstrainedScore(result, riskConfig); + + // For over-threshold signals, cap at LOW confidence and add penalty + var (_, _, configLowThreshold) = GetConfigurationSpecificThresholds(riskConfig); + var penalizedScore = + Math.Min(constrainedScore * 0.75m, configLowThreshold + 0.05m); // Small buffer above LOW threshold + + _logger.LogDebug( + $"🚨 **Conditional Pass-Through** - Signal exceeded {result.AdverseProbabilityThreshold:P1} threshold " + + $"(SL: {result.StopLossProbability:P2}) but has redeeming qualities. " + + $"Constrained score: {constrainedScore:F4} → Penalized: {penalizedScore:F4} → LOW confidence"); + + return Confidence.Low; // Force to LOW confidence for risky but potentially profitable signals + } + + // Standard confidence calculation for signals within adverse threshold + var configScore = CalculateConfigurationAwareScore(result, riskConfig); + var probabilityScore = CalculateProbabilityScore(result); + var thresholdAlignmentScore = CalculateThresholdAlignmentScore(result, riskConfig); + + // Heavily weight configuration-aware factors to create differentiation + var compositeScore = (configScore * 0.50m) + // 50% - Configuration-specific scoring + (thresholdAlignmentScore * 0.30m) + // 30% - How well signal aligns with config thresholds + (probabilityScore * 0.20m); // 20% - Raw probability assessment + + // More nuanced confidence thresholds based on risk tolerance + var (highThreshold, mediumThreshold, lowThreshold) = GetConfigurationSpecificThresholds(riskConfig); + + // Determine confidence level using config-specific thresholds + var confidence = compositeScore switch + { + var score when score >= highThreshold => Confidence.High, + var score when score >= mediumThreshold => Confidence.Medium, + var score when score >= lowThreshold => Confidence.Low, + _ => Confidence.None + }; + + // Debug logging to understand scoring breakdown + _logger.LogDebug($"🧮 **Config-Aware Confidence** [{riskConfig.RiskTolerance}] - " + + $"Composite: {compositeScore:F4} = " + + $"Config({configScore:F4})*0.5 + " + + $"Threshold({thresholdAlignmentScore:F4})*0.3 + " + + $"Prob({probabilityScore:F4})*0.2 " + + $"| Thresholds: H≥{highThreshold:F2}, M≥{mediumThreshold:F2}, L≥{lowThreshold:F2} " + + $"→ {confidence}"); + + return confidence; + } + + /// + /// Checks if a signal that exceeded the adverse probability threshold has redeeming qualities + /// that warrant LOW confidence instead of complete blocking + /// + private bool CheckForRedeemingQualities(SignalValidationResult result, RiskManagement riskConfig) + { + var redeemingFactors = 0; + var totalFactors = 0; + + // Factor 1: Kelly Criterion shows SIGNIFICANT edge (stricter criteria) + totalFactors++; + if (result.KellyFraction > riskConfig.KellyMinimumThreshold * 2m && + result.KellyFraction > 0.05m) // Need 2x minimum threshold AND >5% + { + redeemingFactors++; + } + + // Factor 2: Positive AND meaningful Expected Monetary Value (stricter) + totalFactors++; + if (result.ExpectedMonetaryValue > 100m) // Need at least $100 expected profit + { + redeemingFactors++; + } + + // Factor 3: EXCELLENT TP/SL Ratio (stricter criteria) + totalFactors++; + if (result.TpSlRatio >= 2.0m) // Need at least 2:1 ratio (was 1.5:1) + { + redeemingFactors++; + } + + // Factor 4: Significant positive Expected Utility (stricter) + totalFactors++; + if (result.ExpectedUtility > 1.0m) // Need meaningful utility, not just positive + { + redeemingFactors++; + } + + // Factor 5: TP probability SIGNIFICANTLY higher than SL probability (stricter) + totalFactors++; + if (result.TakeProfitProbability > result.StopLossProbability * 1.5m) // Need 150% higher (was 120%) + { + redeemingFactors++; + } + + // Factor 6: Only SLIGHTLY over the threshold (stricter tolerance) + totalFactors++; + var overThresholdRatio = result.StopLossProbability / result.AdverseProbabilityThreshold; + if (overThresholdRatio <= 1.25m) // Not more than 25% over the threshold (was 50%) + { + redeemingFactors++; + } + + // Factor 7: NEW - Kelly assessment shows strong edge + totalFactors++; + if (result.KellyAssessment != null && + (result.KellyAssessment.Contains("Strong") || + result.KellyAssessment.Contains("Exceptional") || + result.KellyAssessment.Contains("Extraordinary"))) + { + redeemingFactors++; + } + + // Factor 8: NEW - Win/Loss ratio is favorable + totalFactors++; + if (result.WinLossRatio >= 2.0m) // Need at least 2:1 win/loss ratio + { + redeemingFactors++; + } + + // Require at least 75% of factors to be positive for conditional pass-through (was 50%) + var redeemingPercentage = (decimal)redeemingFactors / totalFactors; + + _logger.LogDebug( + $"🔍 **Redeeming Qualities Check** - {redeemingFactors}/{totalFactors} factors positive ({redeemingPercentage:P0}). " + + $"Kelly: {result.KellyFraction:P2} (>2x{riskConfig.KellyMinimumThreshold:P2}), " + + $"EMV: ${result.ExpectedMonetaryValue:F2} (>$100), " + + $"TP/SL: {result.TpSlRatio:F2}x (>2.0), " + + $"EU: {result.ExpectedUtility:F4} (>1.0), " + + $"TP vs SL: {result.TakeProfitProbability:P2} vs {result.StopLossProbability:P2} (>150%), " + + $"Threshold Overage: {overThresholdRatio:F2}x (<1.25), " + + $"Kelly Assessment: {result.KellyAssessment}, " + + $"Win/Loss: {result.WinLossRatio:F2}x"); + + return redeemingPercentage >= 0.75m; // Need at least 75% positive factors (was 50%) + } + + /// + /// Calculates a constrained score for signals that exceeded adverse threshold but have redeeming qualities + /// + private decimal CalculateConstrainedScore(SignalValidationResult result, RiskManagement riskConfig) + { + // Focus on the positive aspects while acknowledging the high risk + var kellyScore = Math.Min(MapToScore(result.KellyFraction, 0.05m, 5.0m), 0.8m); // Cap Kelly contribution + var tpSlScore = Math.Min(MapToScore(result.TpSlRatio, 1.5m, 3.0m), 0.7m); // Cap TP/SL contribution + var emvScore = result.ExpectedMonetaryValue > 0 ? 0.6m : 0.2m; // Binary scoring for EMV + var utilityScore = result.ExpectedUtility > 0 ? 0.5m : 0.2m; // Binary scoring for utility + + // Risk penalty for being over threshold + var overThresholdRatio = result.StopLossProbability / result.AdverseProbabilityThreshold; + var riskPenalty = MapToInverseScore(overThresholdRatio, 1.2m, 3.0m); // Penalty starts at 120% of threshold + + // Weighted combination with risk penalty + var constrainedScore = (kellyScore * 0.25m) + + (tpSlScore * 0.25m) + + (emvScore * 0.20m) + + (utilityScore * 0.15m) + + (riskPenalty * 0.15m); + + return Math.Min(constrainedScore, 0.60m); // Cap to ensure it stays in LOW confidence range + } + + /// + /// Calculates configuration-specific thresholds for confidence levels based on risk tolerance + /// + private (decimal high, decimal medium, decimal low) GetConfigurationSpecificThresholds(RiskManagement riskConfig) + { + return riskConfig.RiskTolerance switch + { + // Conservative: More gaps between thresholds to create MEDIUM zones + RiskToleranceLevel.Conservative => (0.80m, 0.60m, 0.40m), // Stricter with wider MEDIUM zone + RiskToleranceLevel.Moderate => (0.75m, 0.55m, 0.35m), // Wider MEDIUM zone (55%-75%) + RiskToleranceLevel.Aggressive => (0.70m, 0.45m, 0.25m), // Lower HIGH threshold, wider MEDIUM zone + _ => (0.75m, 0.55m, 0.35m) // Default to moderate + }; + } + + /// + /// Calculates a score based on how well the signal aligns with the configuration's specific thresholds and limits + /// This creates differentiation between Conservative, Moderate, and Aggressive settings + /// + private decimal CalculateConfigurationAwareScore(SignalValidationResult result, RiskManagement riskConfig) + { + decimal score = 0m; + + // 1. Adverse Probability Pressure - how close are we to the blocking threshold? + var adversePressure = result.StopLossProbability / result.AdverseProbabilityThreshold; + var adverseScore = riskConfig.RiskTolerance switch + { + RiskToleranceLevel.Conservative => MapToInverseScore(adversePressure, 0.4m, + 6.0m), // More sensitive - penalize at 40% of threshold + RiskToleranceLevel.Moderate => MapToInverseScore(adversePressure, 0.6m, + 4.0m), // Moderate sensitivity at 60% of threshold + RiskToleranceLevel.Aggressive => MapToInverseScore(adversePressure, 0.8m, + 3.0m), // Less sensitive - only penalize at 80% of threshold + _ => MapToInverseScore(adversePressure, 0.6m, 4.0m) + }; + + // 2. Kelly Alignment - does the Kelly fraction match the configuration's expectations? + // Make this more nuanced with smoother transitions to create MEDIUM scores + var kellyAlignmentScore = riskConfig.RiskTolerance switch + { + RiskToleranceLevel.Conservative => + // Conservative: Smoother curve for Kelly fractions, more MEDIUM scores + result.KellyCappedFraction <= 0.03m + ? MapToScore(result.KellyCappedFraction, 0.02m, 15.0m) + : result.KellyCappedFraction <= 0.08m + ? MapToScore(result.KellyCappedFraction, 0.05m, 10.0m) + : // Smoother penalty + result.KellyCappedFraction <= 0.15m + ? 0.3m + : 0.1m, // Gradual penalty for >15% + + RiskToleranceLevel.Moderate => + // Moderate: Create broader MEDIUM scoring range + result.KellyCappedFraction <= 0.01m + ? 0.2m + : // Too small + result.KellyCappedFraction <= 0.15m + ? MapToScore(result.KellyCappedFraction, 0.06m, 8.0m) + : // Broader range + 0.3m, // Less penalty for larger Kelly + + RiskToleranceLevel.Aggressive => + // Aggressive: More forgiving for higher Kelly, but still create MEDIUM range + result.KellyCappedFraction <= 0.02m + ? 0.3m + : // Less penalty for small Kelly + MapToScore(result.KellyCappedFraction, 0.10m, 6.0m), // Reward higher Kelly with smoother curve + + _ => MapToScore(result.KellyCappedFraction, 0.06m, 8.0m) // Default moderate + }; + + // 3. Risk Aversion Alignment - make this more sensitive to create MEDIUM scores + var riskAversionScore = 0.4m; // Start lower to allow more MEDIUM scores + if (result.ExpectedUtility != 0m) + { + var expectedUtilityNormalized = + Math.Max(-1.0m, Math.Min(1.0m, result.ExpectedUtility * 1000m)); // Scale for visibility + + riskAversionScore = riskConfig.RiskTolerance switch + { + RiskToleranceLevel.Conservative => + // Conservative: More lenient utility requirements, broader MEDIUM range + MapToScore(expectedUtilityNormalized, -0.2m, 3.0m), // Lower bar, smoother curve + + RiskToleranceLevel.Moderate => + // Moderate: Balanced but broader MEDIUM range + MapToScore(expectedUtilityNormalized, 0.0m, 5.0m), // Centered at 0, broader curve + + RiskToleranceLevel.Aggressive => + // Aggressive: Still demand higher utility but with smoother transitions + MapToScore(expectedUtilityNormalized, 0.2m, 8.0m), // Lower high bar, smoother curve + + _ => MapToScore(expectedUtilityNormalized, 0.0m, 5.0m) + }; + } + + // 4. Configuration Consistency Bonus - reduce the bonus to avoid pushing scores too high + var consistencyBonus = 0m; + + // Conservative consistency: More moderate bonuses + if (riskConfig.RiskTolerance == RiskToleranceLevel.Conservative) + { + if (result.StopLossProbability < 0.12m && result.KellyCappedFraction < 0.06m && result.TpSlRatio > 1.5m) + consistencyBonus = 0.08m; // Smaller bonus + else if (result.StopLossProbability < 0.15m && result.KellyCappedFraction < 0.10m && + result.TpSlRatio > 1.2m) + consistencyBonus = 0.04m; // Partial bonus + } + // Aggressive consistency: More moderate bonuses + else if (riskConfig.RiskTolerance == RiskToleranceLevel.Aggressive) + { + if (result.KellyCappedFraction > 0.08m && result.TpSlRatio > 2.0m && result.ExpectedMonetaryValue > 0) + consistencyBonus = 0.08m; // Smaller bonus + else if (result.KellyCappedFraction > 0.05m && result.TpSlRatio > 1.5m && result.ExpectedMonetaryValue > 0) + consistencyBonus = 0.04m; // Partial bonus + } + // Moderate consistency: Easier to achieve partial bonuses + else if (riskConfig.RiskTolerance == RiskToleranceLevel.Moderate) + { + if (result.StopLossProbability < 0.16m && result.KellyCappedFraction > 0.03m && result.TpSlRatio > 1.5m) + consistencyBonus = 0.06m; // Moderate bonus + else if (result.StopLossProbability < 0.18m && result.KellyCappedFraction > 0.02m && + result.TpSlRatio > 1.3m) + consistencyBonus = 0.03m; // Small bonus + } + + // Weighted combination with more balanced weights to avoid extremes + score = (adverseScore * 0.30m) + // 30% - Adverse probability pressure + (kellyAlignmentScore * 0.25m) + // 25% - Kelly fraction alignment + (riskAversionScore * 0.25m) + // 25% - Risk aversion alignment + (consistencyBonus) + // Variable - Configuration consistency bonus + 0.20m; // 20% - Base score to lift all signals into scoreable range + + return Math.Min(1.0m, score); + } + + /// + /// Calculates how well the signal aligns with the configuration's specific thresholds and limits + /// + private decimal CalculateThresholdAlignmentScore(SignalValidationResult result, RiskManagement riskConfig) + { + decimal score = 0m; + + // 1. Kelly Threshold Alignment - is Kelly above minimum but below maximum? + var kellyMinScore = result.KellyFraction >= riskConfig.KellyMinimumThreshold + ? 1.0m + : MapToScore(result.KellyFraction / riskConfig.KellyMinimumThreshold, 1.0m, 10.0m); + + var kellyMaxScore = result.KellyCappedFraction <= riskConfig.KellyMaximumCap + ? 1.0m + : MapToInverseScore(result.KellyCappedFraction / riskConfig.KellyMaximumCap, 1.0m, 8.0m); + + // 2. Favorable Probability Threshold - does TP probability meet expectations? + var favorableThresholdScore = result.TakeProfitProbability >= riskConfig.FavorableProbabilityThreshold + ? 1.0m + : MapToScore(result.TakeProfitProbability / riskConfig.FavorableProbabilityThreshold, 1.0m, 8.0m); + + // 3. Multiplier Alignment - is the signal worthy of the Kelly multiplier being used? + var multiplierScore = 0.5m; // Default neutral + if (riskConfig.KellyFractionalMultiplier < 1.0m) // Conservative fractional Kelly + { + // For fractional Kelly, prefer signals with lower variance/risk + multiplierScore = MapToInverseScore(result.StopLossProbability, 0.15m, 8.0m); + } + else if (riskConfig.KellyFractionalMultiplier >= 1.0m) // Full or over-Kelly + { + // For full Kelly, demand higher quality signals + multiplierScore = MapToScore(result.TpSlRatio, 1.8m, 5.0m); + } + + // Weighted combination + score = (kellyMinScore * 0.25m) + // 25% - Meet minimum Kelly threshold + (kellyMaxScore * 0.25m) + // 25% - Stay within maximum Kelly cap + (favorableThresholdScore * 0.30m) + // 30% - Meet favorable probability threshold + (multiplierScore * 0.20m); // 20% - Align with Kelly multiplier philosophy + + return Math.Min(1.0m, score); + } + + /// + /// Calculates probability-based score (0.0 to 1.0) using continuous functions + /// Enhanced to be the primary confidence factor - focuses on signal quality through TP/SL analysis + /// + private decimal CalculateProbabilityScore(SignalValidationResult result) + { + // TP/SL ratio scoring (continuous) - Primary signal quality indicator + // Midpoint at 1.5 (realistic good ratio), steepness 3.0 for better sensitivity + var ratioScore = MapToScore(result.TpSlRatio, 1.5m, 3.0m); + + // Stop Loss probability scoring (inverse - lower is better) - Critical risk factor + // Use percentage of threshold for more nuanced scoring + var slRiskThreshold = result.AdverseProbabilityThreshold; + var slScore = MapToInverseScore(result.StopLossProbability, slRiskThreshold * 0.75m, 8.0m); + + // Take Profit probability scoring (direct - higher is better) - Success likelihood + // Midpoint at 60% TP probability, steepness 4.0 for good sensitivity around realistic ranges + var tpScore = MapToScore(result.TakeProfitProbability, 0.60m, 4.0m); + + // Win/Loss ratio scoring - Overall signal attractiveness + // Midpoint at 1.5 (favorable win/loss), steepness 3.0 for realistic sensitivity + var winLossScore = MapToScore(result.WinLossRatio, 1.5m, 3.0m); + + // Probability dominance check - TP should significantly exceed SL for high confidence + var probabilityDominance = result.StopLossProbability > 0 + ? Math.Min(1.0m, + result.TakeProfitProbability / result.StopLossProbability / 2.0m) // Divide by 2 for scaling + : (result.TakeProfitProbability > 0.5m ? 1.0m : 0.0m); + + // Weighted combination emphasizing the most important probability factors + var combinedScore = (tpScore * 0.30m) + // 30% - TP probability (success likelihood) + (slScore * 0.30m) + // 30% - SL risk management (risk control) + (ratioScore * 0.25m) + // 25% - TP/SL ratio (reward/risk) + (winLossScore * 0.10m) + // 10% - Win/Loss ratio (historical context) + (probabilityDominance * 0.05m); // 5% - Probability dominance bonus + + return Math.Min(1.0m, combinedScore); + } + + /// + /// Helper function to map a value to a score between 0 and 1 using a sigmoid-like curve + /// + /// The input metric (e.g., TpSlRatio, ExpectedMonetaryValue) + /// The value at which the score should be approximately 0.5 + /// Controls how quickly the score changes around the midpoint. Higher = steeper + /// Score between 0.0 and 1.0 + private decimal MapToScore(decimal value, decimal midpoint, decimal steepness) + { + // Using Math.Exp for the exponential part + // Clamp the exponent to prevent overflow/underflow + double exponent = (double)(-steepness * (value - midpoint)); + exponent = Math.Max(-20.0, Math.Min(20.0, exponent)); // Prevent Math.Exp overflow + return 1m / (1m + (decimal)Math.Exp(exponent)); + } + + /// + /// Helper function for metrics where lower values are better (e.g., risk probabilities) + /// + /// The input metric + /// The value at which the score should be approximately 0.5 + /// Controls how quickly the score changes around the midpoint + /// Inverse score between 0.0 and 1.0 + private decimal MapToInverseScore(decimal value, decimal midpoint, decimal steepness) + { + // This is for metrics where lower values are better + double exponent = (double)(steepness * (value - midpoint)); // Note the positive steepness + exponent = Math.Max(-20.0, Math.Min(20.0, exponent)); + return 1m / (1m + (decimal)Math.Exp(exponent)); + } + + /// + /// Performs risk assessment before opening a position + /// + public async Task AssessPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest) + { + var config = BuildConfigurationForTimeframe(botConfig.Timeframe, botConfig); + + if (!config.IsEnabled || !config.UseForPositionSizing || isBacktest) + { + return true; // Allow position if Synth is disabled or in backtest mode + } + + try + { + var asset = GetAssetSymbolFromTicker(ticker); + var timeHorizon = 86400; // 24 hours for risk assessment + + // Estimate liquidation price based on money management + var estimatedLiquidationPrice = + EstimateLiquidationPrice(currentPrice, direction, botConfig.MoneyManagement); + + var liquidationProbability = await GetProbabilityOfTargetPriceAsync( + asset, currentPrice, estimatedLiquidationPrice, timeHorizon, direction == TradeDirection.Long, config); + + if (liquidationProbability > config.MaxLiquidationProbability) + { + _logger.LogWarning( + $"🚫 **Synth Risk Block**\n" + + $"Liquidation probability too high: {liquidationProbability:P2}\n" + + $"📊 Max allowed: {config.MaxLiquidationProbability:P2}\n" + + $"💰 Est. liquidation: ${estimatedLiquidationPrice:F2}\n" + + $"⏱️ Time horizon: 24h"); + return false; + } + + _logger.LogInformation( + $"✅ **Synth Risk Check**\n" + + $"Liquidation probability acceptable: {liquidationProbability:P2}\n" + + $"📊 Max allowed: {config.MaxLiquidationProbability:P2}"); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in Synth risk assessment, allowing position to proceed"); + return true; // Allow position on error to avoid blocking trades + } + } + + /// + /// Monitors liquidation risk for an open position + /// + public async Task MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, + decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig) + { + var result = new SynthRiskResult(); + var config = BuildConfigurationForTimeframe(botConfig.Timeframe, botConfig); + + if (!config.IsEnabled) + { + return result; // Return empty result if disabled + } + + try + { + var asset = GetAssetSymbolFromTicker(ticker); + var timeHorizon = 21600; // 6 hours for position monitoring + + var liquidationProbability = await GetProbabilityOfTargetPriceAsync( + asset, currentPrice, liquidationPrice, timeHorizon, direction == TradeDirection.Long, config); + + result.LiquidationProbability = liquidationProbability; + + // Set warning threshold at 20% + if (liquidationProbability > 0.20m) + { + result.ShouldWarn = true; + result.WarningMessage = + $"⚠️ **High Liquidation Risk**\n" + + $"Position: `{positionIdentifier}`\n" + + $"🎲 Liquidation probability: {liquidationProbability:P2}\n" + + $"💰 Stop loss: ${liquidationPrice:F2}\n" + + $"📊 Current: ${currentPrice:F2}\n" + + $"⏱️ Next 6h forecast"; + } + + // Set auto-close threshold at 50% if dynamic stop-loss is enabled + if (liquidationProbability > 0.50m && config.UseForDynamicStopLoss) + { + result.ShouldAutoClose = true; + result.EmergencyMessage = + $"🚨 **Emergency Close**\n" + + $"Extremely high liquidation risk: {liquidationProbability:P2}\n" + + $"Auto-closing position for protection"; + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error monitoring Synth liquidation risk for position {positionIdentifier}"); + return result; + } + } + + /// + /// Estimates liquidation price based on money management settings + /// + public decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, + MoneyManagement moneyManagement) + { + // This is a simplified estimation - in reality, you'd use the actual money management logic + var riskPercentage = 0.02m; // Default 2% risk + + if (direction == TradeDirection.Long) + { + return currentPrice * (1 - riskPercentage); + } + else + { + return currentPrice * (1 + riskPercentage); + } + } + + /// + /// Converts ticker to asset symbol for Synth API + /// + private string GetAssetSymbolFromTicker(Ticker ticker) + { + // Map ticker enum to Synth API asset symbols + return ticker switch + { + Ticker.BTC => "BTC", + Ticker.ETH => "ETH", + _ => "BTC" // Default fallback + }; + } + + /// + /// Gets cached predictions or fetches new ones if cache is expired + /// + private async Task> GetCachedPredictionsAsync(string asset, int timeHorizonSeconds, + SynthConfiguration config, bool isBacktest, DateTime signalDate) + { + var now = DateTime.UtcNow; + + // Check if we have cached leaderboard data first + var leaderboardCacheKey = $"{asset}_{config.TimeIncrement}"; + if (isBacktest) + { + leaderboardCacheKey += $"_backtest_{signalDate:yyyy-MM-dd-HH}"; + } + + var cachedLeaderboard = await _synthRepository.GetLeaderboardAsync(leaderboardCacheKey); + + List leaderboard; + if (cachedLeaderboard != null) + { + leaderboard = cachedLeaderboard.Miners; + _logger.LogDebug($"📦 **Synth Cache** - Using cached leaderboard for {asset}"); + } + else + { + if (isBacktest) + { + // For backtests, get historical leaderboard and predictions from the signal date + var startTime = signalDate.AddMinutes(-30); // Get leaderboard 30 minutes before signal + var endTime = signalDate.AddMinutes(30); // Allow 30 minute window for leaderboard data + + leaderboard = await _synthApiClient.GetHistoricalLeaderboardAsync(startTime, endTime, config); + + if (!leaderboard.Any()) + { + _logger.LogWarning( + $"No historical leaderboard data available for {signalDate:yyyy-MM-dd HH:mm}, falling back to current leaderboard"); + } + } + else + { + // For live trading, get current leaderboard + leaderboard = await _synthApiClient.GetLeaderboardAsync(config); + } + + // Cache the leaderboard data + if (leaderboard.Any()) + { + var leaderboardToCache = new SynthMinersLeaderboard + { + Asset = asset, + TimeIncrement = config.TimeIncrement, + SignalDate = isBacktest ? signalDate : null, + IsBacktest = isBacktest, + Miners = leaderboard + }; + await _synthRepository.SaveLeaderboardAsync(leaderboardToCache); + } + } + + var topMinerUids = leaderboard + .OrderByDescending(m => m.Rank) + .Take(config.TopMinersCount) + .Select(m => m.NeuronUid) + .ToList(); + + if (!topMinerUids.Any()) + { + _logger.LogWarning("No miners available from leaderboard"); + return new List(); + } + + var timeLength = Math.Max(timeHorizonSeconds, config.DefaultTimeLength); + + // Try to get individual cached predictions first + var cachedIndividualPredictions = await _synthRepository.GetIndividualPredictionsAsync( + asset, config.TimeIncrement, timeLength, topMinerUids, isBacktest, + isBacktest ? signalDate : null); + + // If we have all predictions cached, return them + if (cachedIndividualPredictions.Count == topMinerUids.Count) + { + _logger.LogDebug( + $"📦 **Synth Individual Cache** - Using all cached individual predictions for {asset} {(isBacktest ? "HISTORICAL" : "LIVE")}"); + + return cachedIndividualPredictions.Select(p => p.Prediction).ToList(); + } + + // If we have partial cache, determine which miners need fresh data + var cachedMinerUids = cachedIndividualPredictions.Select(p => p.MinerUid).ToHashSet(); + var missingMinerUids = topMinerUids.Where(uid => !cachedMinerUids.Contains(uid)).ToList(); + + _logger.LogInformation( + $"🔄 **Synth Individual Cache** - Partial cache hit for {asset}: {cachedIndividualPredictions.Count}/{topMinerUids.Count} cached, fetching {missingMinerUids.Count} fresh predictions {(isBacktest ? "HISTORICAL" : "LIVE")}"); + + // Fetch missing predictions from API + List freshPredictions; + if (isBacktest) + { + // For backtests, get historical predictions from the signal date + freshPredictions = await _synthApiClient.GetHistoricalMinerPredictionsAsync( + missingMinerUids, asset, signalDate, config.TimeIncrement, timeLength, config); + + if (!freshPredictions.Any() && missingMinerUids.Any()) + { + _logger.LogWarning( + $"No historical predictions available for {missingMinerUids.Count} miners on {signalDate:yyyy-MM-dd HH:mm}"); + } + } + else + { + // For live trading, get current predictions + freshPredictions = await _synthApiClient.GetMinerPredictionsAsync( + missingMinerUids, asset, config.TimeIncrement, timeLength, config); + } + + // Map MinerInfo to fresh MinerPrediction objects based on NeuronUid -> MinerUid mapping + foreach (var prediction in freshPredictions) + { + var matchingMinerInfo = leaderboard.FirstOrDefault(m => m.NeuronUid == prediction.MinerUid); + if (matchingMinerInfo != null) + { + prediction.MinerInfo = matchingMinerInfo; + } + } + + // Cache the fresh predictions individually + if (freshPredictions.Any()) + { + var individualPredictionsToCache = freshPredictions.Select(prediction => new SynthPrediction + { + Asset = asset, + MinerUid = prediction.MinerUid, + TimeIncrement = config.TimeIncrement, + TimeLength = timeLength, + SignalDate = isBacktest ? signalDate : null, + IsBacktest = isBacktest, + Prediction = prediction + }).ToList(); + + await _synthRepository.SaveIndividualPredictionsAsync(individualPredictionsToCache); + } + + // Combine cached and fresh predictions + var allPredictions = new List(); + allPredictions.AddRange(cachedIndividualPredictions.Select(p => p.Prediction)); + allPredictions.AddRange(freshPredictions); + + // Map MinerInfo to cached predictions as well (in case they weren't mapped before) + foreach (var prediction in allPredictions.Where(p => p.MinerInfo == null)) + { + var matchingMinerInfo = leaderboard.FirstOrDefault(m => m.NeuronUid == prediction.MinerUid); + if (matchingMinerInfo != null) + { + prediction.MinerInfo = matchingMinerInfo; + } + } + + _logger.LogInformation( + $"📈 **Synth Individual Cache** - Total predictions assembled: {allPredictions.Count} for {asset} {(isBacktest ? "HISTORICAL" : "LIVE")}"); + + return allPredictions; + } + + /// + /// Calculates probability from aggregated prediction paths + /// + private decimal CalculateProbabilityFromPaths( + List predictions, + decimal currentPrice, + decimal targetPrice, + int timeHorizonSeconds, + bool isLongPosition) + { + var allPaths = new List>(); + + // Aggregate all simulation paths from all miners + foreach (var prediction in predictions) + { + if (prediction.Prediction != null) + { + allPaths.AddRange(prediction.Prediction); + } + } + + if (!allPaths.Any()) + { + _logger.LogWarning("No simulation paths available for probability calculation"); + return 0m; + } + + var pathsCrossingThreshold = 0; + var totalPaths = allPaths.Count; + var maxTimeFromNow = DateTime.UtcNow.AddSeconds(timeHorizonSeconds); + + foreach (var path in allPaths) + { + if (!path.Any()) continue; + + var predictionStartTime = predictions.FirstOrDefault()?.GetStartDateTime() ?? DateTime.UtcNow; + + foreach (var pricePoint in path) + { + var pointTime = pricePoint.GetDateTime(); + + // Skip if point is beyond our time horizon + if (pointTime > maxTimeFromNow) break; + + // Check if target price is reached based on position direction + bool targetReached = false; + + if (isLongPosition) + { + // For long positions, we're interested in price dropping to target (liquidation/stop-loss) + targetReached = pricePoint.Price <= targetPrice; + } + else + { + // For short positions, we're interested in price rising to target (liquidation/stop-loss) + targetReached = pricePoint.Price >= targetPrice; + } + + if (targetReached) + { + pathsCrossingThreshold++; + break; // This path crossed the threshold, move to next path + } + } + } + + var probability = totalPaths > 0 ? (decimal)pathsCrossingThreshold / totalPaths : 0m; + + _logger.LogDebug( + $"🧮 **Probability Calculation** - {pathsCrossingThreshold}/{totalPaths} paths crossed threshold = {probability:P2}"); + + return probability; + } +} \ No newline at end of file diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index 0cba462..06b076c 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -1,10 +1,12 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Domain.Accounts; +using Managing.Domain.Bots; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Statistics; using Managing.Domain.Strategies; +using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Managing.Infrastructure.Evm.Models.Privy; using Microsoft.Extensions.Logging; @@ -22,6 +24,7 @@ public class TradingService : ITradingService private readonly IStatisticRepository _statisticRepository; private readonly IEvmManager _evmManager; private readonly ILogger _logger; + private readonly ISynthPredictionService _synthPredictionService; public TradingService( ITradingRepository tradingRepository, @@ -31,7 +34,8 @@ public class TradingService : ITradingService ICacheService cacheService, IMessengerService messengerService, IStatisticRepository statisticRepository, - IEvmManager evmManager) + IEvmManager evmManager, + ISynthPredictionService synthPredictionService) { _tradingRepository = tradingRepository; _exchangeService = exchangeService; @@ -41,6 +45,7 @@ public class TradingService : ITradingService _messengerService = messengerService; _statisticRepository = statisticRepository; _evmManager = evmManager; + _synthPredictionService = synthPredictionService; } public void DeleteScenario(string name) @@ -397,4 +402,25 @@ public class TradingService : ITradingService return new PrivyInitAddressResponse { Success = false, Error = ex.Message }; } } + + // Synth API integration methods + public async Task ValidateSynthSignalAsync(Signal signal, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest) + { + return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest); + } + + public async Task AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice, + TradingBotConfig botConfig, bool isBacktest) + { + return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice, + botConfig, isBacktest); + } + + public async Task MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, + decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig) + { + return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice, + positionIdentifier, botConfig); + } } \ No newline at end of file diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 5262677..790881d 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -14,6 +14,7 @@ using Managing.Application.MoneyManagements; using Managing.Application.Scenarios; using Managing.Application.Shared; using Managing.Application.Shared.Behaviours; +using Managing.Application.Synth; using Managing.Application.Trading; using Managing.Application.Trading.Commands; using Managing.Application.Users; @@ -95,6 +96,8 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); return services; } @@ -133,6 +136,7 @@ public static class ApiBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Cache services.AddDistributedMemoryCache(); diff --git a/src/Managing.Bootstrap/WorkersBootstrap.cs b/src/Managing.Bootstrap/WorkersBootstrap.cs index 7fc0a01..110997e 100644 --- a/src/Managing.Bootstrap/WorkersBootstrap.cs +++ b/src/Managing.Bootstrap/WorkersBootstrap.cs @@ -12,6 +12,7 @@ using Managing.Application.ManageBot; using Managing.Application.MoneyManagements; using Managing.Application.Scenarios; using Managing.Application.Shared; +using Managing.Application.Synth; using Managing.Application.Trading; using Managing.Application.Trading.Commands; using Managing.Application.Users; @@ -66,6 +67,7 @@ public static class WorkersBootstrap services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient, OpenPositionCommandHandler>(); services.AddTransient, ClosePositionCommandHandler>(); @@ -111,6 +113,7 @@ public static class WorkersBootstrap services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Cache services.AddDistributedMemoryCache(); @@ -126,6 +129,7 @@ public static class WorkersBootstrap services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Web3Proxy Configuration services.Configure(configuration.GetSection("Web3Proxy")); diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 0e59b8a..d393eab 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -405,4 +405,14 @@ public static class Enums Position, MoneyManagement } + + /// + /// Risk tolerance levels for trading strategies + /// + public enum RiskToleranceLevel + { + Conservative = 1, + Moderate = 2, + Aggressive = 3 + } } \ No newline at end of file diff --git a/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs b/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs index 5adead8..4fe9c95 100644 --- a/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs +++ b/src/Managing.Core/FixedSizedQueue/FixedSizeQueue.cs @@ -1,10 +1,26 @@ +using System.Text.Json.Serialization; + namespace Managing.Core.FixedSizedQueue; public class FixedSizeQueue : Queue { private readonly int _maxSize; + + /// + /// Parameterless constructor for serialization support + /// + public FixedSizeQueue() : this(500) // Default size + { + } + + [JsonConstructor] public FixedSizeQueue(int maxSize) => _maxSize = maxSize; + /// + /// Gets the maximum size of the queue (for serialization) + /// + public int MaxSize => _maxSize; + public new void Enqueue(T item) { while (Count >= _maxSize) Dequeue(); diff --git a/src/Managing.Domain/Backtests/Backtest.cs b/src/Managing.Domain/Backtests/Backtest.cs index f5233fc..b5dbd3a 100644 --- a/src/Managing.Domain/Backtests/Backtest.cs +++ b/src/Managing.Domain/Backtests/Backtest.cs @@ -24,7 +24,7 @@ public class Backtest Signals = signals; Candles = candles; WalletBalances = new List>(); - StrategiesValues = new Dictionary(); + IndicatorsValues = new Dictionary(); // Initialize start and end dates if candles are provided if (candles != null && candles.Count > 0) @@ -55,7 +55,7 @@ public class Backtest [Required] public List> WalletBalances { get; set; } [Required] public MoneyManagement OptimizedMoneyManagement { get; set; } [Required] public User User { get; set; } - [Required] public Dictionary StrategiesValues { get; set; } + [Required] public Dictionary IndicatorsValues { get; set; } [Required] public double Score { get; set; } /// diff --git a/src/Managing.Domain/Bots/BotBackup.cs b/src/Managing.Domain/Bots/BotBackup.cs index 8e84300..8b19f78 100644 --- a/src/Managing.Domain/Bots/BotBackup.cs +++ b/src/Managing.Domain/Bots/BotBackup.cs @@ -5,7 +5,6 @@ namespace Managing.Domain.Bots; public class BotBackup { - public BotType BotType { get; set; } public string Identifier { get; set; } public User User { get; set; } public string Data { get; set; } diff --git a/src/Managing.Domain/Bots/TradingBotConfig.cs b/src/Managing.Domain/Bots/TradingBotConfig.cs index 2bdae94..1eb163a 100644 --- a/src/Managing.Domain/Bots/TradingBotConfig.cs +++ b/src/Managing.Domain/Bots/TradingBotConfig.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Managing.Domain.MoneyManagements; +using Managing.Domain.Risk; using Managing.Domain.Scenarios; using static Managing.Common.Enums; @@ -20,6 +21,13 @@ public class TradingBotConfig [Required] public bool FlipPosition { get; set; } [Required] public string Name { get; set; } + /// + /// Risk management configuration for advanced probabilistic analysis and position sizing. + /// Contains all configurable parameters for Expected Utility Theory, Kelly Criterion, and probability thresholds. + /// If null, default risk management settings will be used. + /// + public RiskManagement RiskManagement { get; set; } = new(); + /// /// The scenario object containing all strategies. When provided, this takes precedence over ScenarioName. /// This allows running backtests without requiring scenarios to be saved in the database. @@ -52,4 +60,27 @@ public class TradingBotConfig /// [Required] public bool FlipOnlyWhenInProfit { get; set; } = true; + + /// + /// Whether to use Synth API for probabilistic price forecasts and risk assessment. + /// When true, the bot will use Synth predictions for signal filtering, position risk assessment, and position monitoring. + /// When false, the bot operates in traditional mode without Synth predictions. + /// The actual Synth configuration is managed centrally in SynthPredictionService. + /// + public bool UseSynthApi { get; set; } = false; + + /// + /// Whether to use Synth predictions for position sizing adjustments and risk assessment + /// + public bool UseForPositionSizing { get; set; } = true; + + /// + /// Whether to use Synth predictions for signal filtering + /// + public bool UseForSignalFiltering { get; set; } = true; + + /// + /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments + /// + public bool UseForDynamicStopLoss { get; set; } = true; } \ No newline at end of file diff --git a/src/Managing.Domain/Risk/RiskManagement.cs b/src/Managing.Domain/Risk/RiskManagement.cs new file mode 100644 index 0000000..84424fd --- /dev/null +++ b/src/Managing.Domain/Risk/RiskManagement.cs @@ -0,0 +1,192 @@ +using System.ComponentModel.DataAnnotations; +using Managing.Common; + +namespace Managing.Domain.Risk; + +/// +/// Risk management configuration for trading bots +/// Contains all configurable risk parameters for probabilistic analysis and position sizing +/// +public class RiskManagement +{ + /// + /// Threshold for adverse probability in signal validation (default: 20%) + /// Signals with SL probability above this threshold may be filtered out + /// Range: 0.05 (5%) to 0.50 (50%) + /// + [Range(0.05, 0.50)] + [Required] + public decimal AdverseProbabilityThreshold { get; set; } = 0.20m; + + /// + /// Threshold for favorable probability in signal validation (default: 30%) + /// Used for additional signal filtering and confidence assessment + /// Range: 0.10 (10%) to 0.70 (70%) + /// + [Range(0.10, 0.70)] + [Required] + public decimal FavorableProbabilityThreshold { get; set; } = 0.30m; + + /// + /// Risk aversion parameter for Expected Utility calculations (default: 1.0) + /// Higher values = more risk-averse behavior in utility calculations + /// Range: 0.1 (risk-seeking) to 5.0 (highly risk-averse) + /// + [Range(0.1, 5.0)] + [Required] + public decimal RiskAversion { get; set; } = 1.0m; + + /// + /// Minimum Kelly Criterion fraction to consider a trade favorable (default: 1%) + /// Trades with Kelly fraction below this threshold are considered unfavorable + /// Range: 0.5% to 10% + /// + [Range(0.005, 0.10)] + [Required] + public decimal KellyMinimumThreshold { get; set; } = 0.01m; + + /// + /// Maximum Kelly Criterion fraction cap for practical risk management (default: 25%) + /// Prevents over-allocation even when Kelly suggests higher percentages + /// Range: 5% to 50% + /// + [Range(0.05, 0.50)] + [Required] + public decimal KellyMaximumCap { get; set; } = 0.25m; + + /// + /// Maximum acceptable liquidation probability for position risk assessment (default: 10%) + /// Positions with higher liquidation risk may be blocked or reduced + /// Range: 5% to 30% + /// + [Range(0.05, 0.30)] + [Required] + public decimal MaxLiquidationProbability { get; set; } = 0.10m; + + /// + /// Time horizon in hours for signal validation analysis (default: 24 hours) + /// Longer horizons provide more stable predictions but less responsive signals + /// Range: 1 hour to 168 hours (1 week) + /// + [Range(1, 168)] + [Required] + public int SignalValidationTimeHorizonHours { get; set; } = 24; + + /// + /// Time horizon in hours for position risk monitoring (default: 6 hours) + /// Shorter horizons for more frequent risk updates on open positions + /// Range: 1 hour to 48 hours + /// + [Range(1, 48)] + [Required] + public int PositionMonitoringTimeHorizonHours { get; set; } = 6; + + /// + /// Probability threshold for issuing position risk warnings (default: 20%) + /// Positions exceeding this liquidation risk will trigger warnings + /// Range: 10% to 40% + /// + [Range(0.10, 0.40)] + [Required] + public decimal PositionWarningThreshold { get; set; } = 0.20m; + + /// + /// Probability threshold for automatic position closure (default: 50%) + /// Positions exceeding this liquidation risk will be automatically closed + /// Range: 30% to 80% + /// + [Range(0.30, 0.80)] + [Required] + public decimal PositionAutoCloseThreshold { get; set; } = 0.50m; + + /// + /// Fractional Kelly multiplier for conservative position sizing (default: 1.0) + /// Values less than 1.0 implement fractional Kelly (e.g., 0.5 = half-Kelly) + /// Range: 0.1 to 1.0 + /// + [Range(0.1, 1.0)] + [Required] + public decimal KellyFractionalMultiplier { get; set; } = 1.0m; + + /// + /// Risk tolerance level affecting overall risk calculations + /// + [Required] + public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate; + + /// + /// Whether to use Expected Utility Theory for decision making + /// + [Required] + public bool UseExpectedUtility { get; set; } = true; + + /// + /// Whether to use Kelly Criterion for position sizing recommendations + /// + [Required] + public bool UseKellyCriterion { get; set; } = true; + + /// + /// Validates that the risk management configuration is coherent + /// + /// True if configuration is valid, false otherwise + public bool IsConfigurationValid() + { + // Ensure favorable threshold is higher than adverse threshold + if (FavorableProbabilityThreshold <= AdverseProbabilityThreshold) + return false; + + // Ensure Kelly minimum is less than maximum + if (KellyMinimumThreshold >= KellyMaximumCap) + return false; + + // Ensure warning threshold is less than auto-close threshold + if (PositionWarningThreshold >= PositionAutoCloseThreshold) + return false; + + // Ensure signal validation horizon is longer than position monitoring + if (SignalValidationTimeHorizonHours < PositionMonitoringTimeHorizonHours) + return false; + + return true; + } + + /// + /// Gets a preset configuration based on risk tolerance level + /// + /// Risk tolerance level + /// Configured RiskManagement instance + public static RiskManagement GetPresetConfiguration(Enums.RiskToleranceLevel tolerance) + { + return tolerance switch + { + Enums.RiskToleranceLevel.Conservative => new RiskManagement + { + AdverseProbabilityThreshold = 0.15m, + FavorableProbabilityThreshold = 0.40m, + RiskAversion = 2.0m, + KellyMinimumThreshold = 0.02m, + KellyMaximumCap = 0.15m, + MaxLiquidationProbability = 0.08m, + PositionWarningThreshold = 0.15m, + PositionAutoCloseThreshold = 0.35m, + KellyFractionalMultiplier = 0.5m, + RiskTolerance = tolerance + }, + Enums.RiskToleranceLevel.Aggressive => new RiskManagement + { + AdverseProbabilityThreshold = 0.30m, + FavorableProbabilityThreshold = 0.25m, + RiskAversion = 0.5m, + KellyMinimumThreshold = 0.005m, + KellyMaximumCap = 0.40m, + MaxLiquidationProbability = 0.15m, + PositionWarningThreshold = 0.30m, + PositionAutoCloseThreshold = 0.70m, + KellyFractionalMultiplier = 1.0m, + RiskTolerance = tolerance + }, + _ => new RiskManagement { RiskTolerance = tolerance } // Moderate (default values) + }; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 6f1b721..85a61d7 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -108,15 +108,21 @@ public static class TradingBox } } + // Keep only the latest signal per indicator to avoid count mismatch + var latestSignalsPerIndicator = signalOnCandles + .GroupBy(s => s.IndicatorName) + .Select(g => g.OrderByDescending(s => s.Date).First()) + .ToHashSet(); + // Remove the restrictive requirement that ALL strategies must produce signals // Instead, let ComputeSignals handle the logic based on what we have - if (!signalOnCandles.Any()) + if (!latestSignalsPerIndicator.Any()) { return null; // No signals from any strategy } var data = newCandles.First(); - return ComputeSignals(strategies, signalOnCandles, MiscExtensions.ParseEnum(data.Ticker), + return ComputeSignals(strategies, latestSignalsPerIndicator, MiscExtensions.ParseEnum(data.Ticker), data.Timeframe, config); } @@ -136,51 +142,88 @@ public static class TradingBox } // Check if all strategies produced signals - this is required for composite signals - if (signalOnCandles.Count != strategies.Count) + var strategyNames = strategies.Select(s => s.Name).ToHashSet(); + var signalIndicatorNames = signalOnCandles.Select(s => s.IndicatorName).ToHashSet(); + + if (!strategyNames.SetEquals(signalIndicatorNames)) { // Not all strategies produced signals - composite signal requires all strategies to contribute return null; } // Group signals by type for analysis - var signalStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList(); - var trendStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList(); - var contextStrategies = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList(); + var signals = signalOnCandles.Where(s => s.SignalType == SignalType.Signal).ToList(); + var trendSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Trend).ToList(); + var contextSignals = signalOnCandles.Where(s => s.SignalType == SignalType.Context).ToList(); // Context validation - evaluates market conditions based on confidence levels - if (!ValidateContextStrategies(strategies, contextStrategies, config)) + if (!ValidateContextStrategies(strategies, contextSignals, config)) { return null; // Context strategies are blocking the trade } - // Trend analysis - evaluate overall market direction - var trendDirection = EvaluateTrendDirection(trendStrategies, config); + // Check for 100% agreement across ALL signals (no threshold voting) + var allDirectionalSignals = signalOnCandles + .Where(s => s.Direction != TradeDirection.None && s.SignalType != SignalType.Context).ToList(); - // Signal analysis - evaluate entry signals - var signalDirection = EvaluateSignalDirection(signalStrategies, config); + if (!allDirectionalSignals.Any()) + { + return null; // No directional signals available + } - // Determine final direction and confidence - var (finalDirection, confidence) = - DetermineFinalSignal(signalDirection, trendDirection, signalStrategies, trendStrategies, config); + // Require 100% agreement - all signals must have the same direction + var lastSignalDirection = allDirectionalSignals.Last().Direction; + if (!allDirectionalSignals.All(s => s.Direction == lastSignalDirection)) + { + return null; // Signals are not in complete agreement + } - if (finalDirection == TradeDirection.None || confidence < config.MinimumConfidence) + var finalDirection = lastSignalDirection; + + // Calculate confidence based on the average confidence of all signals + var averageConfidence = CalculateAverageConfidence(allDirectionalSignals); + + if (finalDirection == TradeDirection.None || averageConfidence < config.MinimumConfidence) { return null; // No valid signal or below minimum confidence } // Create composite signal - var lastSignal = signalStrategies.LastOrDefault() ?? - trendStrategies.LastOrDefault() ?? contextStrategies.LastOrDefault(); + var lastSignal = signals.LastOrDefault() ?? + trendSignals.LastOrDefault() ?? contextSignals.LastOrDefault(); return new Signal( ticker, finalDirection, - confidence, + averageConfidence, lastSignal?.Candle, lastSignal?.Date ?? DateTime.UtcNow, lastSignal?.Exchange ?? config.DefaultExchange, IndicatorType.Composite, - SignalType.Signal); + SignalType.Signal, "Aggregated"); + } + + /// + /// Calculates the average confidence level from a list of signals + /// + private static Confidence CalculateAverageConfidence(List signals) + { + if (!signals.Any()) + { + return Confidence.None; + } + + // Convert confidence enum to numeric values for averaging + var confidenceValues = signals.Select(s => (int)s.Confidence).ToList(); + var averageValue = confidenceValues.Average(); + + // Round to nearest confidence level + var roundedValue = Math.Round(averageValue); + + // Ensure the value is within valid confidence enum range + roundedValue = Math.Max(0, Math.Min(3, roundedValue)); + + return (Confidence)(int)roundedValue; } /// diff --git a/src/Managing.Domain/Strategies/Context/StDevContext.cs b/src/Managing.Domain/Strategies/Context/StDevContext.cs index c3c2f08..961e127 100644 --- a/src/Managing.Domain/Strategies/Context/StDevContext.cs +++ b/src/Managing.Domain/Strategies/Context/StDevContext.cs @@ -73,7 +73,7 @@ public class StDevContext : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { var test = new IndicatorsResultBase() { @@ -119,7 +119,7 @@ public class StDevContext : Indicator candleSignal, candleSignal.Date, candleSignal.Exchange, - Type, SignalType); + Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/IIndicator.cs b/src/Managing.Domain/Strategies/IIndicator.cs index 0ee4e74..b280e5e 100644 --- a/src/Managing.Domain/Strategies/IIndicator.cs +++ b/src/Managing.Domain/Strategies/IIndicator.cs @@ -17,7 +17,7 @@ namespace Managing.Domain.Strategies FixedSizeQueue Candles { get; set; } List Run(); - IndicatorsResultBase GetStrategyValues(); + IndicatorsResultBase GetIndicatorValues(); void UpdateCandles(HashSet newCandles); string GetName(); } diff --git a/src/Managing.Domain/Strategies/Indicator.cs b/src/Managing.Domain/Strategies/Indicator.cs index 890b6dc..d0054b1 100644 --- a/src/Managing.Domain/Strategies/Indicator.cs +++ b/src/Managing.Domain/Strategies/Indicator.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; using Managing.Core.FixedSizedQueue; using Managing.Domain.Candles; using Managing.Domain.Scenarios; @@ -19,7 +20,7 @@ namespace Managing.Domain.Strategies } public string Name { get; set; } - [JsonIgnore] public FixedSizeQueue Candles { get; set; } + [JsonIgnore] [IgnoreDataMember] public FixedSizeQueue Candles { get; set; } public IndicatorType Type { get; set; } public SignalType SignalType { get; set; } public int MinimumHistory { get; set; } @@ -38,7 +39,7 @@ namespace Managing.Domain.Strategies return new List(); } - public virtual IndicatorsResultBase GetStrategyValues() + public virtual IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase(); } diff --git a/src/Managing.Domain/Strategies/Signal.cs b/src/Managing.Domain/Strategies/Signal.cs index 4ae8427..05a6c00 100644 --- a/src/Managing.Domain/Strategies/Signal.cs +++ b/src/Managing.Domain/Strategies/Signal.cs @@ -21,9 +21,11 @@ namespace Managing.Domain.Strategies [Required] public IndicatorType IndicatorType { get; set; } [Required] public SignalType SignalType { get; set; } public User User { get; set; } + [Required] public string IndicatorName { get; set; } public Signal(Ticker ticker, TradeDirection direction, Confidence confidence, Candle candle, DateTime date, - TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, User user = null) + TradingExchanges exchange, IndicatorType indicatorType, SignalType signalType, string indicatorName, + User user = null) { Direction = direction; Confidence = confidence; @@ -34,10 +36,11 @@ namespace Managing.Domain.Strategies Status = SignalStatus.WaitingForPosition; IndicatorType = indicatorType; User = user; + IndicatorName = indicatorName; + SignalType = signalType; Identifier = - $"{IndicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}"; - SignalType = signalType; + $"{indicatorName}-{indicatorType}-{direction}-{ticker}-{candle?.Close.ToString(CultureInfo.InvariantCulture)}-{date:yyyyMMdd-HHmmss}"; } public void SetConfidence(Confidence confidence) diff --git a/src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs b/src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs index 20fcfa0..d99e8c5 100644 --- a/src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/ChandelierExitIndicator.cs @@ -40,7 +40,7 @@ public class ChandelierExitIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -113,7 +113,8 @@ public class ChandelierExitIndicator : Indicator candleSignal, candleSignal.Date, candleSignal.Exchange, - Type, SignalType); + Type, SignalType, + Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs b/src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs index 0028700..867e0b1 100644 --- a/src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/DualEmaCrossIndicator.cs @@ -19,7 +19,7 @@ public class DualEmaCrossIndicator : EmaBaseIndicator MinimumHistory = Math.Max(fastPeriod, slowPeriod) * 2; } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -104,7 +104,7 @@ public class DualEmaCrossIndicator : EmaBaseIndicator private void AddSignal(CandleDualEma candleSignal, TradeDirection direction, Confidence confidence) { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, - candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType); + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs b/src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs index 2ba29b7..d7e26f4 100644 --- a/src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/EmaCrossIndicator.cs @@ -16,7 +16,7 @@ public class EmaCrossIndicator : EmaBaseIndicator Period = period; } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -68,7 +68,7 @@ public class EmaCrossIndicator : EmaBaseIndicator private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, - candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType); + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/LaggingSTC.cs b/src/Managing.Domain/Strategies/Signals/LaggingSTC.cs index c1b023f..4151f44 100644 --- a/src/Managing.Domain/Strategies/Signals/LaggingSTC.cs +++ b/src/Managing.Domain/Strategies/Signals/LaggingSTC.cs @@ -89,7 +89,7 @@ public class LaggingSTC : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { var stc = Candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); return new IndicatorsResultBase @@ -130,7 +130,8 @@ public class LaggingSTC : Indicator candleSignal, candleSignal.Date, candleSignal.Exchange, - Type, SignalType); + Type, SignalType, + Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs b/src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs index 24f40c4..f0c666f 100644 --- a/src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/MacdCrossIndicator.cs @@ -59,7 +59,7 @@ public class MacdCrossIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -96,7 +96,7 @@ public class MacdCrossIndicator : Indicator Confidence confidence) { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, - candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType); + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs b/src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs index 1514873..872d0dc 100644 --- a/src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/RsiDivergenceConfirmIndicator.cs @@ -49,7 +49,7 @@ public class RsiDivergenceConfirmIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -233,7 +233,7 @@ public class RsiDivergenceConfirmIndicator : Indicator private void AddSignal(CandleRsi candleSignal, TradeDirection direction, Confidence confidence) { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, - candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType); + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs b/src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs index 96c9270..0e584d3 100644 --- a/src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/RsiDivergenceIndicator.cs @@ -52,7 +52,7 @@ public class RsiDivergenceIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -206,7 +206,7 @@ public class RsiDivergenceIndicator : Indicator private void AddSignal(CandleRsi candleSignal, TradeDirection direction) { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, Confidence.Low, - candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType); + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (Signals.Count(s => s.Identifier == signal.Identifier) < 1) { diff --git a/src/Managing.Domain/Strategies/Signals/StcIndicator.cs b/src/Managing.Domain/Strategies/Signals/StcIndicator.cs index 686b576..b8280f1 100644 --- a/src/Managing.Domain/Strategies/Signals/StcIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/StcIndicator.cs @@ -64,7 +64,7 @@ public class StcIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { if (FastPeriods != null && SlowPeriods != null) { @@ -110,7 +110,8 @@ public class StcIndicator : Indicator candleSignal, candleSignal.Date, candleSignal.Exchange, - Type, SignalType); + Type, SignalType, + Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs b/src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs index 985202f..36ff15d 100644 --- a/src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs +++ b/src/Managing.Domain/Strategies/Signals/SuperTrendCrossEma.cs @@ -157,7 +157,7 @@ public class SuperTrendCrossEma : Indicator return superTrends; } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -171,7 +171,7 @@ public class SuperTrendCrossEma : Indicator { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, candleSignal, candleSignal.Date, - candleSignal.Exchange, Type, SignalType); + candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs b/src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs index db13431..4654657 100644 --- a/src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/SuperTrendIndicator.cs @@ -61,7 +61,7 @@ public class SuperTrendIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -99,7 +99,7 @@ public class SuperTrendIndicator : Indicator { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, candleSignal, candleSignal.Date, - candleSignal.Exchange, Type, SignalType); + candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs b/src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs index 1346fe2..f13822a 100644 --- a/src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs +++ b/src/Managing.Domain/Strategies/Signals/ThreeWhiteSoldiersIndicator.cs @@ -52,7 +52,7 @@ namespace Managing.Domain.Strategies.Signals } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { throw new NotImplementedException(); } diff --git a/src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs b/src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs index 6c33603..c5c3063 100644 --- a/src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs +++ b/src/Managing.Domain/Strategies/Trends/EmaTrendIndicator.cs @@ -54,7 +54,7 @@ public class EmaTrendIndicator : EmaBaseIndicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -65,7 +65,7 @@ public class EmaTrendIndicator : EmaBaseIndicator public void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence) { var signal = new Signal(MiscExtensions.ParseEnum(candleSignal.Ticker), direction, confidence, - candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType); + candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs b/src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs index 5305ec5..d8468a3 100644 --- a/src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs +++ b/src/Managing.Domain/Strategies/Trends/StochRsiTrendIndicator.cs @@ -65,7 +65,7 @@ public class StochRsiTrendIndicator : Indicator } } - public override IndicatorsResultBase GetStrategyValues() + public override IndicatorsResultBase GetIndicatorValues() { return new IndicatorsResultBase() { @@ -108,7 +108,8 @@ public class StochRsiTrendIndicator : Indicator candleSignal.Date, candleSignal.Exchange, Type, - SignalType); + SignalType, + Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); diff --git a/src/Managing.Domain/Synth/Models/MinerInfo.cs b/src/Managing.Domain/Synth/Models/MinerInfo.cs new file mode 100644 index 0000000..c2b3127 --- /dev/null +++ b/src/Managing.Domain/Synth/Models/MinerInfo.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace Managing.Domain.Synth.Models; + +/// +/// Represents a miner on the Synth API leaderboard +/// +public class MinerInfo +{ + [JsonPropertyName("coldkey")] + public string Coldkey { get; set; } + + [JsonPropertyName("emission")] + public decimal Emission { get; set; } + + [JsonPropertyName("incentive")] + public decimal Incentive { get; set; } + + [JsonPropertyName("neuron_uid")] + public int NeuronUid { get; set; } + + [JsonPropertyName("pruning_score")] + public decimal PruningScore { get; set; } + + /// + /// Rank value from API (decimal representing the ranking score) + /// + [JsonPropertyName("rank")] + public decimal Rank { get; set; } + + [JsonPropertyName("stake")] + public decimal Stake { get; set; } + + [JsonPropertyName("updated_at")] + public string UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/MinerPrediction.cs b/src/Managing.Domain/Synth/Models/MinerPrediction.cs new file mode 100644 index 0000000..2056887 --- /dev/null +++ b/src/Managing.Domain/Synth/Models/MinerPrediction.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace Managing.Domain.Synth.Models; + +/// +/// Represents the prediction data from a single miner +/// Contains multiple simulated price paths and the miner's information +/// +public class MinerPrediction +{ + public string Asset { get; set; } + [JsonPropertyName("miner_uid")] public int MinerUid { get; set; } + [JsonPropertyName("num_simulations")] public int NumSimulations { get; set; } + public List> Prediction { get; set; } = new(); + [JsonPropertyName("start_time")] public string StartTime { get; set; } + [JsonPropertyName("time_increment")] public int TimeIncrement { get; set; } + [JsonPropertyName("time_length")] public int TimeLength { get; set; } + + /// + /// Complete miner information including rank, stake, incentive, etc. + /// This is populated after fetching predictions by mapping MinerUid to MinerInfo.NeuronUid + /// + public MinerInfo? MinerInfo { get; set; } + + /// + /// Converts the StartTime string to DateTime for easier manipulation + /// + public DateTime GetStartDateTime() + { + return DateTime.TryParse(StartTime, out var dateTime) ? dateTime : DateTime.MinValue; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/PricePoint.cs b/src/Managing.Domain/Synth/Models/PricePoint.cs new file mode 100644 index 0000000..8193a05 --- /dev/null +++ b/src/Managing.Domain/Synth/Models/PricePoint.cs @@ -0,0 +1,18 @@ +namespace Managing.Domain.Synth.Models; + +/// +/// Represents a price at a specific time within a simulated path +/// +public class PricePoint +{ + public decimal Price { get; set; } + public string Time { get; set; } + + /// + /// Converts the Time string to DateTime for easier manipulation + /// + public DateTime GetDateTime() + { + return DateTime.TryParse(Time, out var dateTime) ? dateTime : DateTime.MinValue; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/SignalValidationResult.cs b/src/Managing.Domain/Synth/Models/SignalValidationResult.cs new file mode 100644 index 0000000..57b34a8 --- /dev/null +++ b/src/Managing.Domain/Synth/Models/SignalValidationResult.cs @@ -0,0 +1,403 @@ +using Managing.Domain.Risk; +using static Managing.Common.Enums; + +namespace Managing.Domain.Synth.Models; + +/// +/// Result of Synth signal validation containing comprehensive analysis data +/// +public class SignalValidationResult +{ + /// + /// Overall confidence level of the signal based on TP vs SL probability analysis + /// + public Confidence Confidence { get; set; } + + /// + /// Raw stop loss probability (0.0 to 1.0) + /// + public decimal StopLossProbability { get; set; } + + /// + /// Raw take profit probability (0.0 to 1.0) + /// + public decimal TakeProfitProbability { get; set; } + + /// + /// Calculated ratio of Take Profit Probability / Stop Loss Probability + /// Higher values indicate more favorable risk/reward + /// + public decimal TpSlRatio { get; set; } + + /// + /// Indicates if the signal should be blocked based on risk analysis + /// True when confidence is None or adverse probability is too high + /// + public bool IsBlocked { get; set; } + + /// + /// Threshold used for adverse probability evaluation + /// + public decimal AdverseProbabilityThreshold { get; set; } + + /// + /// Additional context information about the validation + /// + public string ValidationContext { get; set; } = string.Empty; + + /// + /// Time horizon used for the probability calculations (in seconds) + /// + public int TimeHorizonSeconds { get; set; } + + /// + /// Whether custom thresholds were used in the analysis + /// + public bool UsedCustomThresholds { get; set; } + + /// + /// Monetary gain if take profit is reached (positive value) + /// + public decimal TakeProfitGain { get; set; } + + /// + /// Monetary loss if stop loss is hit (positive value representing loss amount) + /// + public decimal StopLossLoss { get; set; } + + /// + /// Expected Monetary Value: (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob) + /// Positive values indicate favorable expected outcomes + /// + public decimal ExpectedMonetaryValue { get; set; } + + /// + /// Expected Utility using logarithmic utility function for risk-adjusted decision making + /// Higher values indicate more desirable risk-adjusted outcomes + /// + public decimal ExpectedUtility { get; set; } + + /// + /// Risk-adjusted return ratio (Expected Utility / Risk) + /// Higher values indicate better risk-adjusted opportunities + /// + public decimal UtilityRiskRatio { get; set; } + + /// + /// Kelly Criterion fraction - optimal percentage of capital to allocate (0.0 to 1.0) + /// Based on Kelly formula: f* = (bp - q) / b, where b = payoff ratio, p = win probability, q = loss probability + /// Values above 0.25 (25%) are typically capped for practical risk management + /// + public decimal KellyFraction { get; set; } + + /// + /// Capped Kelly Fraction for practical risk management (typically max 25% of capital) + /// + public decimal KellyCappedFraction { get; set; } + + /// + /// Win/Loss ratio used in Kelly calculation (TakeProfitGain / StopLossLoss) + /// + public decimal WinLossRatio { get; set; } + + /// + /// Kelly Criterion assessment indicating the quality of the opportunity + /// + public string KellyAssessment { get; set; } = string.Empty; + + /// + /// Risk tolerance level affecting overall risk calculations + /// + public RiskToleranceLevel RiskTolerance { get; set; } = RiskToleranceLevel.Moderate; + + /// + /// Whether to use Expected Utility Theory for decision making + /// + public bool UseExpectedUtility { get; set; } = true; + + /// + /// Whether to use Kelly Criterion for position sizing recommendations + /// + public bool UseKellyCriterion { get; set; } = true; + + /// + /// Trading balance used for utility calculations (from TradingBotConfig.BotTradingBalance) + /// Represents the actual capital allocated to this trading bot + /// + public decimal TradingBalance { get; private set; } = 10000m; + + /// + /// Risk aversion parameter used for utility calculations (configured from RiskManagement) + /// + public decimal ConfiguredRiskAversion { get; private set; } = 1.0m; + + /// + /// Calculates Expected Monetary Value and Expected Utility using configured risk parameters + /// + /// Actual trading balance allocated to the bot + /// Complete risk management configuration + public void CalculateExpectedMetrics(decimal tradingBalance, RiskManagement riskConfig) + { + // Store configured values for reference + TradingBalance = tradingBalance; + ConfiguredRiskAversion = riskConfig.RiskAversion; + + // Calculate Expected Monetary Value + // EMV = (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob) + ExpectedMonetaryValue = (TakeProfitGain * TakeProfitProbability) - (StopLossLoss * StopLossProbability); + + // Calculate Expected Utility using logarithmic utility function + // This accounts for diminishing marginal utility and risk aversion + ExpectedUtility = CalculateLogarithmicExpectedUtility(); + + // Calculate utility-to-risk ratio for ranking opportunities + var totalRisk = StopLossLoss > 0 ? StopLossLoss : 1m; // Avoid division by zero + UtilityRiskRatio = ExpectedUtility / totalRisk; + + // Calculate Kelly Criterion for optimal position sizing using full risk config + CalculateKellyCriterion(riskConfig); + } + + /// + /// Calculates Expected Utility using logarithmic utility function + /// U(x) = ln(tradingBalance + x) for gains, ln(tradingBalance - x) for losses + /// Uses the actual trading balance and configured risk aversion + /// + /// Expected utility value + private decimal CalculateLogarithmicExpectedUtility() + { + try + { + // Use actual trading balance and configured risk aversion + var baseCapital = TradingBalance > 0 ? TradingBalance : 10000m; + var riskAversion = ConfiguredRiskAversion > 0 ? ConfiguredRiskAversion : 1.0m; + + // Calculate utility of TP outcome: U(tradingBalance + gain) + var tpOutcome = baseCapital + TakeProfitGain; + var tpUtility = tpOutcome > 0 ? (decimal)Math.Log((double)tpOutcome) / riskAversion : decimal.MinValue; + + // Calculate utility of SL outcome: U(tradingBalance - loss) + var slOutcome = baseCapital - StopLossLoss; + var slUtility = slOutcome > 0 ? (decimal)Math.Log((double)slOutcome) / riskAversion : decimal.MinValue; + + // Calculate utility of no-change outcome (neither TP nor SL hit) + var noChangeProb = Math.Max(0m, 1m - TakeProfitProbability - StopLossProbability); + var noChangeUtility = (decimal)Math.Log((double)baseCapital) / riskAversion; + + // Expected Utility = Sum of (Utility * Probability) for all outcomes + var expectedUtility = (tpUtility * TakeProfitProbability) + + (slUtility * StopLossProbability) + + (noChangeUtility * noChangeProb); + + return expectedUtility; + } + catch (Exception) + { + // Return conservative utility value on calculation errors + return decimal.MinValue; + } + } + + /// + /// Calculates Kelly Criterion for optimal position sizing + /// Kelly Formula: f* = (bp - q) / b + /// Where: b = payoff ratio (win/loss), p = win probability, q = loss probability + /// + /// Complete risk management configuration + private void CalculateKellyCriterion(RiskManagement riskConfig) + { + try + { + // Calculate Win/Loss Ratio (b in Kelly formula) + WinLossRatio = StopLossLoss > 0 ? TakeProfitGain / StopLossLoss : 0m; + + // Handle edge cases + if (WinLossRatio <= 0 || TakeProfitProbability <= 0) + { + KellyFraction = 0m; + KellyCappedFraction = 0m; + KellyAssessment = "No Position - Unfavorable risk/reward ratio"; + return; + } + + // Kelly Formula: f* = (bp - q) / b + // Where: + // b = WinLossRatio (TakeProfitGain / StopLossLoss) + // p = TakeProfitProbability + // q = StopLossProbability + var numerator = (WinLossRatio * TakeProfitProbability) - StopLossProbability; + var kellyFraction = numerator / WinLossRatio; + + // Ensure Kelly fraction is not negative (would indicate unfavorable bet) + KellyFraction = Math.Max(0m, kellyFraction); + + // Apply fractional Kelly multiplier + KellyFraction *= riskConfig.KellyFractionalMultiplier; + + // Apply practical cap for risk management + KellyCappedFraction = Math.Min(KellyFraction, riskConfig.KellyMaximumCap); + + // Generate Kelly assessment using the configured threshold + KellyAssessment = GenerateKellyAssessment(riskConfig); + } + catch (Exception) + { + // Safe defaults on calculation errors + KellyFraction = 0m; + KellyCappedFraction = 0m; + WinLossRatio = 0m; + KellyAssessment = "Calculation Error - No position recommended"; + } + } + + /// + /// Generates a descriptive assessment of the Kelly Criterion result + /// + /// Risk management configuration containing Kelly thresholds + /// Human-readable Kelly assessment + private string GenerateKellyAssessment(RiskManagement riskConfig) + { + if (KellyFraction <= 0) + return "No Position - Negative or zero Kelly fraction"; + + if (KellyFraction < riskConfig.KellyMinimumThreshold) + return $"Below Threshold - Kelly {KellyFraction:P2} < {riskConfig.KellyMinimumThreshold:P2} minimum"; + + if (KellyFraction < 0.05m) // 1-5% + return "Small Position - Low but positive edge"; + + if (KellyFraction < 0.10m) // 5-10% + return "Moderate Position - Reasonable edge"; + + if (KellyFraction < 0.25m) // 10-25% + return "Large Position - Strong edge detected"; + + if (KellyFraction < 0.50m) // 25-50% + return "Very Large Position - Exceptional edge (CAPPED for safety)"; + + // Above 50% + return "Extreme Position - Extraordinary edge (HEAVILY CAPPED for safety)"; + } + + /// + /// Gets detailed Kelly Criterion analysis including fractional betting recommendations + /// + /// Total available capital for position sizing + /// Detailed Kelly analysis with dollar amounts + public string GetDetailedKellyAnalysis(decimal totalCapital = 100000m) + { + var recommendedAmount = KellyCappedFraction * totalCapital; + var uncappedAmount = KellyFraction * totalCapital; + + var analysis = $"Kelly Analysis:\n" + + $"• Win/Loss Ratio: {WinLossRatio:F2}:1\n" + + $"• Optimal Kelly %: {KellyFraction:P2}\n" + + $"• Capped Kelly %: {KellyCappedFraction:P2}\n" + + $"• Recommended Amount: ${recommendedAmount:N0}\n"; + + if (KellyFraction > KellyCappedFraction) + { + analysis += $"• Uncapped Amount: ${uncappedAmount:N0} (RISK WARNING)\n"; + } + + analysis += $"• Assessment: {KellyAssessment}"; + + return analysis; + } + + /// + /// Calculates fractional Kelly betting for more conservative position sizing + /// + /// Fraction of Kelly to use (e.g., 0.5 for half-Kelly) + /// Fractional Kelly allocation percentage + public decimal GetFractionalKelly(decimal fraction = 0.5m) + { + if (fraction < 0 || fraction > 1) fraction = 0.5m; // Default to half-Kelly + return KellyFraction * fraction; + } + + /// + /// Validates if the Kelly Criterion suggests this is a profitable opportunity + /// + /// Risk management configuration containing Kelly thresholds + /// True if Kelly fraction is above the configured threshold + public bool IsKellyFavorable(RiskManagement riskConfig) + { + return KellyFraction > riskConfig.KellyMinimumThreshold; + } + + /// + /// Alternative utility calculation using square root utility (less risk-averse than logarithmic) + /// Uses the actual trading balance from the bot configuration + /// + /// Expected utility using square root function + public decimal CalculateSquareRootExpectedUtility() + { + try + { + var baseCapital = TradingBalance > 0 ? TradingBalance : 10000m; + + // Square root utility: U(x) = sqrt(x) + var tpOutcome = baseCapital + TakeProfitGain; + var tpUtility = tpOutcome > 0 ? (decimal)Math.Sqrt((double)tpOutcome) : 0m; + + var slOutcome = Math.Max(0m, baseCapital - StopLossLoss); + var slUtility = (decimal)Math.Sqrt((double)slOutcome); + + var noChangeProb = Math.Max(0m, 1m - TakeProfitProbability - StopLossProbability); + var noChangeUtility = (decimal)Math.Sqrt((double)baseCapital); + + return (tpUtility * TakeProfitProbability) + + (slUtility * StopLossProbability) + + (noChangeUtility * noChangeProb); + } + catch (Exception) + { + return 0m; + } + } + + /// + /// Gets a risk assessment based on Expected Utility Theory + /// + /// Descriptive risk assessment + public string GetUtilityRiskAssessment() + { + if (ExpectedMonetaryValue > 0 && ExpectedUtility > 0) + return "Favorable - Positive expected value and utility"; + + if (ExpectedMonetaryValue > 0 && ExpectedUtility <= 0) + return "Cautious - Positive expected value but negative risk-adjusted utility"; + + if (ExpectedMonetaryValue <= 0 && ExpectedUtility > 0) + return "Risk-Seeking - Negative expected value but positive utility (unusual)"; + + return "Unfavorable - Negative expected value and utility"; + } + + /// + /// Creates a result indicating Synth is disabled + /// + public static SignalValidationResult CreateDisabledResult(Confidence originalConfidence) + { + return new SignalValidationResult + { + Confidence = originalConfidence, + IsBlocked = false, + ValidationContext = "Synth API disabled" + }; + } + + /// + /// Creates a result for error scenarios + /// + public static SignalValidationResult CreateErrorResult(Confidence fallbackConfidence, string errorContext) + { + return new SignalValidationResult + { + Confidence = fallbackConfidence, + IsBlocked = false, + ValidationContext = $"Error in validation: {errorContext}" + }; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/SynthConfiguration.cs b/src/Managing.Domain/Synth/Models/SynthConfiguration.cs new file mode 100644 index 0000000..bfb0709 --- /dev/null +++ b/src/Managing.Domain/Synth/Models/SynthConfiguration.cs @@ -0,0 +1,64 @@ +namespace Managing.Domain.Synth.Models; + +/// +/// Configuration settings for Synth API integration +/// +public class SynthConfiguration +{ + /// + /// Whether to enable Synth API integration + /// + public bool IsEnabled { get; set; } = false; + + /// + /// Number of top miners to fetch predictions from (default: 10) + /// + public int TopMinersCount { get; set; } = 10; + + /// + /// Time increment in seconds for predictions (default: 300 = 5 minutes) + /// + public int TimeIncrement { get; set; } = 300; + + /// + /// Default time length in seconds for predictions (default: 86400 = 24 hours) + /// + public int DefaultTimeLength { get; set; } = 86400; + + /// + /// Maximum acceptable liquidation probability threshold (0.0 to 1.0) + /// If liquidation probability exceeds this, position opening may be blocked + /// + public decimal MaxLiquidationProbability { get; set; } = 0.10m; // 10% + + /// + /// Cache duration for predictions in minutes (default: 5 minutes) + /// + public int PredictionCacheDurationMinutes { get; set; } = 5; + + /// + /// Whether to use Synth predictions for position sizing adjustments + /// + public bool UseForPositionSizing { get; set; } = true; + + /// + /// Whether to use Synth predictions for signal filtering + /// + public bool UseForSignalFiltering { get; set; } = true; + + /// + /// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments + /// + public bool UseForDynamicStopLoss { get; set; } = true; + + /// + /// Validates the configuration + /// + public bool IsValid() + { + return !IsEnabled || (TopMinersCount > 0 && + TimeIncrement > 0 && + DefaultTimeLength > 0 && + MaxLiquidationProbability >= 0 && MaxLiquidationProbability <= 1); + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs b/src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs new file mode 100644 index 0000000..c20661b --- /dev/null +++ b/src/Managing.Domain/Synth/Models/SynthMinersLeaderboard.cs @@ -0,0 +1,57 @@ +namespace Managing.Domain.Synth.Models; + +/// +/// Represents a cached leaderboard entry for Synth miners +/// Used for MongoDB persistence to avoid repeated API calls +/// +public class SynthMinersLeaderboard +{ + /// + /// Unique identifier for this leaderboard entry + /// + public string Id { get; set; } + + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// + public string Asset { get; set; } + + /// + /// Time increment used for this leaderboard data + /// + public int TimeIncrement { get; set; } + + /// + /// Signal date for which this leaderboard was retrieved (for backtests) + /// Null for live trading data + /// + public DateTime? SignalDate { get; set; } + + /// + /// Whether this is backtest data or live data + /// + public bool IsBacktest { get; set; } + + /// + /// List of miners in the leaderboard + /// + public List Miners { get; set; } = new(); + + /// + /// When this leaderboard data was created/stored + /// + public DateTime CreatedAt { get; set; } + + /// + /// Generates a cache key for this leaderboard entry + /// + public string GetCacheKey() + { + var key = $"{Asset}_{TimeIncrement}"; + if (IsBacktest && SignalDate.HasValue) + { + key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}"; + } + return key; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs b/src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs new file mode 100644 index 0000000..42f190c --- /dev/null +++ b/src/Managing.Domain/Synth/Models/SynthMinersPredictions.cs @@ -0,0 +1,72 @@ +namespace Managing.Domain.Synth.Models; + +/// +/// Represents cached prediction data from Synth miners +/// Used for MongoDB persistence to avoid repeated API calls +/// +public class SynthMinersPredictions +{ + /// + /// Unique identifier for this predictions entry + /// + public string Id { get; set; } + + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// + public string Asset { get; set; } + + /// + /// Time increment used for these predictions + /// + public int TimeIncrement { get; set; } + + /// + /// Time length (horizon) for these predictions in seconds + /// + public int TimeLength { get; set; } + + /// + /// Signal date for which these predictions were retrieved (for backtests) + /// Null for live trading data + /// + public DateTime? SignalDate { get; set; } + + /// + /// Whether this is backtest data or live data + /// + public bool IsBacktest { get; set; } + + /// + /// List of miner UIDs these predictions are from + /// + public List MinerUids { get; set; } = new(); + + /// + /// The actual prediction data from miners + /// + public List Predictions { get; set; } = new(); + + /// + /// When this prediction data was fetched from the API + /// + public DateTime FetchedAt { get; set; } + + /// + /// When this prediction data was created/stored + /// + public DateTime CreatedAt { get; set; } + + /// + /// Generates a cache key for this predictions entry + /// + public string GetCacheKey() + { + var key = $"{Asset}_{TimeIncrement}_{TimeLength}"; + if (IsBacktest && SignalDate.HasValue) + { + key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}"; + } + return key; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/SynthPrediction.cs b/src/Managing.Domain/Synth/Models/SynthPrediction.cs new file mode 100644 index 0000000..3bfca6a --- /dev/null +++ b/src/Managing.Domain/Synth/Models/SynthPrediction.cs @@ -0,0 +1,67 @@ +namespace Managing.Domain.Synth.Models; + +/// +/// Represents cached prediction data from a single Synth miner +/// Used for MongoDB persistence to avoid repeated API calls and reduce document size +/// +public class SynthPrediction +{ + /// + /// Unique identifier for this prediction entry + /// + public string Id { get; set; } + + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// + public string Asset { get; set; } + + /// + /// Miner UID that provided this prediction + /// + public int MinerUid { get; set; } + + /// + /// Time increment used for this prediction + /// + public int TimeIncrement { get; set; } + + /// + /// Time length (horizon) for this prediction in seconds + /// + public int TimeLength { get; set; } + + /// + /// Signal date for which this prediction was retrieved (for backtests) + /// Null for live trading data + /// + public DateTime? SignalDate { get; set; } + + /// + /// Whether this is backtest data or live data + /// + public bool IsBacktest { get; set; } + + /// + /// The actual prediction data from the miner + /// + public MinerPrediction Prediction { get; set; } + + /// + /// When this prediction data was created/stored + /// + public DateTime CreatedAt { get; set; } + + /// + /// Generates a cache key for this prediction entry + /// + public string GetCacheKey() + { + var key = $"{Asset}_{TimeIncrement}_{TimeLength}_{MinerUid}"; + if (IsBacktest && SignalDate.HasValue) + { + key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}"; + } + return key; + } +} \ No newline at end of file diff --git a/src/Managing.Domain/Synth/Models/SynthRiskResult.cs b/src/Managing.Domain/Synth/Models/SynthRiskResult.cs new file mode 100644 index 0000000..17441e5 --- /dev/null +++ b/src/Managing.Domain/Synth/Models/SynthRiskResult.cs @@ -0,0 +1,13 @@ +namespace Managing.Domain.Synth.Models; + +/// +/// Result of Synth risk monitoring +/// +public class SynthRiskResult +{ + public decimal LiquidationProbability { get; set; } + public bool ShouldWarn { get; set; } + public bool ShouldAutoClose { get; set; } + public string WarningMessage { get; set; } + public string EmergencyMessage { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/SignalDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/SignalDto.cs index 18a21c6..44d9a2f 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/Collections/SignalDto.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/SignalDto.cs @@ -18,5 +18,6 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections public IndicatorType Type { get; set; } public SignalType SignalType { get; set; } public UserDto User { get; set; } + public string IndicatorName { get; set; } } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthMinersLeaderboardDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthMinersLeaderboardDto.cs new file mode 100644 index 0000000..77b449e --- /dev/null +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthMinersLeaderboardDto.cs @@ -0,0 +1,42 @@ +using Managing.Infrastructure.Databases.MongoDb.Attributes; +using Managing.Infrastructure.Databases.MongoDb.Configurations; + +namespace Managing.Infrastructure.Databases.MongoDb.Collections; + +/// +/// MongoDB DTO for storing Synth miners leaderboard data +/// +[BsonCollection("SynthMinersLeaderboard")] +public class SynthMinersLeaderboardDto : Document +{ + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// + public string Asset { get; set; } + + /// + /// Time increment used for this leaderboard data + /// + public int TimeIncrement { get; set; } + + /// + /// Signal date for which this leaderboard was retrieved (for backtests) + /// Null for live trading data + /// + public DateTime? SignalDate { get; set; } + + /// + /// Whether this is backtest data or live data + /// + public bool IsBacktest { get; set; } + + /// + /// Serialized JSON of miners list + /// + public string MinersData { get; set; } + + /// + /// Cache key for quick lookup + /// + public string CacheKey { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthMinersPredictionsDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthMinersPredictionsDto.cs new file mode 100644 index 0000000..6fa672d --- /dev/null +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthMinersPredictionsDto.cs @@ -0,0 +1,57 @@ +using Managing.Infrastructure.Databases.MongoDb.Attributes; +using Managing.Infrastructure.Databases.MongoDb.Configurations; + +namespace Managing.Infrastructure.Databases.MongoDb.Collections; + +/// +/// MongoDB DTO for storing Synth miners predictions data +/// +[BsonCollection("SynthMinersPredictions")] +public class SynthMinersPredictionsDto : Document +{ + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// + public string Asset { get; set; } + + /// + /// Time increment used for these predictions + /// + public int TimeIncrement { get; set; } + + /// + /// Time length (horizon) for these predictions in seconds + /// + public int TimeLength { get; set; } + + /// + /// Signal date for which these predictions were retrieved (for backtests) + /// Null for live trading data + /// + public DateTime? SignalDate { get; set; } + + /// + /// Whether this is backtest data or live data + /// + public bool IsBacktest { get; set; } + + /// + /// Serialized JSON of miner UIDs list + /// + public string MinerUidsData { get; set; } + + /// + /// Serialized JSON of predictions data + /// + public string PredictionsData { get; set; } + + /// + /// When this prediction data was fetched from the API + /// + public DateTime FetchedAt { get; set; } + + /// + /// Cache key for quick lookup + /// + public string CacheKey { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthPredictionDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthPredictionDto.cs new file mode 100644 index 0000000..3cea1fa --- /dev/null +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/SynthPredictionDto.cs @@ -0,0 +1,52 @@ +using Managing.Infrastructure.Databases.MongoDb.Attributes; +using Managing.Infrastructure.Databases.MongoDb.Configurations; + +namespace Managing.Infrastructure.Databases.MongoDb.Collections; + +/// +/// MongoDB DTO for storing individual Synth miner prediction data +/// +[BsonCollection("SynthPredictions")] +public class SynthPredictionDto : Document +{ + /// + /// Asset symbol (e.g., "BTC", "ETH") + /// + public string Asset { get; set; } + + /// + /// Miner UID that provided this prediction + /// + public int MinerUid { get; set; } + + /// + /// Time increment used for this prediction + /// + public int TimeIncrement { get; set; } + + /// + /// Time length (horizon) for this prediction in seconds + /// + public int TimeLength { get; set; } + + /// + /// Signal date for which this prediction was retrieved (for backtests) + /// Null for live trading data + /// + public DateTime? SignalDate { get; set; } + + /// + /// Whether this is backtest data or live data + /// + public bool IsBacktest { get; set; } + + /// + /// Serialized JSON of the prediction data + /// + public string PredictionData { get; set; } + + /// + /// Cache key for quick lookup + /// + public string CacheKey { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs index e97d1b6..0782a3f 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs @@ -1,4 +1,5 @@ -using Managing.Domain.Accounts; +using System.Text.Json; +using Managing.Domain.Accounts; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; @@ -6,6 +7,7 @@ using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Statistics; using Managing.Domain.Strategies; +using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Managing.Domain.Users; using Managing.Domain.Workers; @@ -371,7 +373,8 @@ public static class MongoMappers Status = signal.Status, Timeframe = signal.Timeframe, Type = signal.IndicatorType, - User = signal.User != null ? Map(signal.User) : null + User = signal.User != null ? Map(signal.User) : null, + IndicatorName = signal.IndicatorName }; } @@ -387,6 +390,7 @@ public static class MongoMappers TradingExchanges.Binance, //TODO FIXME When the signal status is modified from controller bSignal.Type, bSignal.SignalType, + bSignal.IndicatorName, bSignal.User != null ? Map(bSignal.User) : null) { Status = bSignal.Status @@ -744,7 +748,6 @@ public static class MongoMappers { User = Map(bot.User), Identifier = bot.Identifier, - BotType = bot.BotType, Data = bot.Data, LastStatus = bot.LastStatus }; @@ -758,7 +761,6 @@ public static class MongoMappers { User = Map(b.User), Identifier = b.Identifier, - BotType = b.BotType, Data = b.Data, LastStatus = b.LastStatus }; @@ -792,4 +794,143 @@ public static class MongoMappers Direction = fundingRate.Direction }; } + + #region Synth + + /// + /// Maps domain SynthMinersLeaderboard to MongoDB DTO + /// + internal static SynthMinersLeaderboardDto Map(SynthMinersLeaderboard leaderboard) + { + if (leaderboard == null) return null; + + return new SynthMinersLeaderboardDto + { + Asset = leaderboard.Asset, + TimeIncrement = leaderboard.TimeIncrement, + SignalDate = leaderboard.SignalDate, + IsBacktest = leaderboard.IsBacktest, + MinersData = JsonSerializer.Serialize(leaderboard.Miners), + CacheKey = leaderboard.GetCacheKey() + }; + } + + /// + /// Maps MongoDB DTO to domain SynthMinersLeaderboard + /// + internal static SynthMinersLeaderboard Map(SynthMinersLeaderboardDto dto) + { + if (dto == null) return null; + + var miners = string.IsNullOrEmpty(dto.MinersData) + ? new List() + : JsonSerializer.Deserialize>(dto.MinersData) ?? new List(); + + return new SynthMinersLeaderboard + { + Id = dto.Id.ToString(), + Asset = dto.Asset, + TimeIncrement = dto.TimeIncrement, + SignalDate = dto.SignalDate, + IsBacktest = dto.IsBacktest, + Miners = miners, + CreatedAt = dto.CreatedAt + }; + } + + /// + /// Maps domain SynthMinersPredictions to MongoDB DTO + /// + internal static SynthMinersPredictionsDto Map(SynthMinersPredictions predictions) + { + if (predictions == null) return null; + + return new SynthMinersPredictionsDto + { + Asset = predictions.Asset, + TimeIncrement = predictions.TimeIncrement, + TimeLength = predictions.TimeLength, + SignalDate = predictions.SignalDate, + IsBacktest = predictions.IsBacktest, + MinerUidsData = JsonSerializer.Serialize(predictions.MinerUids), + PredictionsData = JsonSerializer.Serialize(predictions.Predictions), + CacheKey = predictions.GetCacheKey() + }; + } + + /// + /// Maps MongoDB DTO to domain SynthMinersPredictions + /// + internal static SynthMinersPredictions Map(SynthMinersPredictionsDto dto) + { + if (dto == null) return null; + + var minerUids = string.IsNullOrEmpty(dto.MinerUidsData) + ? new List() + : JsonSerializer.Deserialize>(dto.MinerUidsData) ?? new List(); + + var predictions = string.IsNullOrEmpty(dto.PredictionsData) + ? new List() + : JsonSerializer.Deserialize>(dto.PredictionsData) ?? new List(); + + return new SynthMinersPredictions + { + Id = dto.Id.ToString(), + Asset = dto.Asset, + TimeIncrement = dto.TimeIncrement, + TimeLength = dto.TimeLength, + SignalDate = dto.SignalDate, + IsBacktest = dto.IsBacktest, + MinerUids = minerUids, + Predictions = predictions, + CreatedAt = dto.CreatedAt + }; + } + + /// + /// Maps domain SynthPrediction to MongoDB DTO + /// + internal static SynthPredictionDto Map(SynthPrediction prediction) + { + if (prediction == null) return null; + + return new SynthPredictionDto + { + Asset = prediction.Asset, + MinerUid = prediction.MinerUid, + TimeIncrement = prediction.TimeIncrement, + TimeLength = prediction.TimeLength, + SignalDate = prediction.SignalDate, + IsBacktest = prediction.IsBacktest, + PredictionData = JsonSerializer.Serialize(prediction.Prediction), + CacheKey = prediction.GetCacheKey() + }; + } + + /// + /// Maps MongoDB DTO to domain SynthPrediction + /// + internal static SynthPrediction Map(SynthPredictionDto dto) + { + if (dto == null) return null; + + var prediction = string.IsNullOrEmpty(dto.PredictionData) + ? null + : JsonSerializer.Deserialize(dto.PredictionData); + + return new SynthPrediction + { + Id = dto.Id.ToString(), + Asset = dto.Asset, + MinerUid = dto.MinerUid, + TimeIncrement = dto.TimeIncrement, + TimeLength = dto.TimeLength, + SignalDate = dto.SignalDate, + IsBacktest = dto.IsBacktest, + Prediction = prediction, + CreatedAt = dto.CreatedAt + }; + } + + #endregion } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/SynthRepository.cs b/src/Managing.Infrastructure.Database/SynthRepository.cs new file mode 100644 index 0000000..aa965f3 --- /dev/null +++ b/src/Managing.Infrastructure.Database/SynthRepository.cs @@ -0,0 +1,223 @@ +using Managing.Application.Abstractions.Repositories; +using Managing.Domain.Synth.Models; +using Managing.Infrastructure.Databases.MongoDb; +using Managing.Infrastructure.Databases.MongoDb.Abstractions; +using Managing.Infrastructure.Databases.MongoDb.Collections; +using Microsoft.Extensions.Logging; + +namespace Managing.Infrastructure.Databases; + +/// +/// Repository implementation for Synth-related data operations using MongoDB +/// Provides persistence for leaderboard and individual predictions data +/// +public class SynthRepository : ISynthRepository +{ + private readonly IMongoRepository _leaderboardRepository; + private readonly IMongoRepository _individualPredictionsRepository; + private readonly ILogger _logger; + + public SynthRepository( + IMongoRepository leaderboardRepository, + IMongoRepository individualPredictionsRepository, + ILogger logger) + { + _leaderboardRepository = leaderboardRepository; + _individualPredictionsRepository = individualPredictionsRepository; + _logger = logger; + } + + /// + /// Gets cached leaderboard data by cache key + /// + public async Task GetLeaderboardAsync(string cacheKey) + { + try + { + var dto = await _leaderboardRepository.FindOneAsync(x => x.CacheKey == cacheKey); + + if (dto == null) + { + _logger.LogDebug($"🔍 **Synth Cache** - No leaderboard cache found for key: {cacheKey}"); + return null; + } + + var result = MongoMappers.Map(dto); + _logger.LogDebug($"📦 **Synth Cache** - Retrieved leaderboard from MongoDB for key: {cacheKey}"); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error retrieving leaderboard cache for key: {cacheKey}"); + return null; + } + } + + /// + /// Saves leaderboard data to MongoDB + /// + public async Task SaveLeaderboardAsync(SynthMinersLeaderboard leaderboard) + { + try + { + leaderboard.CreatedAt = DateTime.UtcNow; + var dto = MongoMappers.Map(leaderboard); + + // Check if we already have this cache key and update instead of inserting + var existing = await _leaderboardRepository.FindOneAsync(x => x.CacheKey == dto.CacheKey); + if (existing != null) + { + dto.Id = existing.Id; + _leaderboardRepository.Update(dto); + _logger.LogDebug($"💾 **Synth Cache** - Updated leaderboard in MongoDB for key: {dto.CacheKey}"); + } + else + { + await _leaderboardRepository.InsertOneAsync(dto); + _logger.LogDebug($"💾 **Synth Cache** - Saved new leaderboard to MongoDB for key: {dto.CacheKey}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error saving leaderboard cache for key: {leaderboard.GetCacheKey()}"); + } + } + + /// + /// Gets individual cached prediction data by asset, parameters, and miner UIDs + /// + public async Task> GetIndividualPredictionsAsync( + string asset, + int timeIncrement, + int timeLength, + List minerUids, + bool isBacktest, + DateTime? signalDate) + { + try + { + var results = new List(); + + foreach (var minerUid in minerUids) + { + // Build cache key for individual prediction + var cacheKey = $"{asset}_{timeIncrement}_{timeLength}_{minerUid}"; + if (isBacktest && signalDate.HasValue) + { + cacheKey += $"_backtest_{signalDate.Value:yyyy-MM-dd-HH}"; + } + + var dto = await _individualPredictionsRepository.FindOneAsync(x => x.CacheKey == cacheKey); + + if (dto != null) + { + var prediction = MongoMappers.Map(dto); + if (prediction != null) + { + results.Add(prediction); + } + } + } + + if (results.Any()) + { + _logger.LogDebug($"📦 **Synth Individual Cache** - Retrieved {results.Count}/{minerUids.Count} individual predictions for {asset}"); + } + else + { + _logger.LogDebug($"🔍 **Synth Individual Cache** - No individual predictions found for {asset}"); + } + + return results; + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error retrieving individual predictions cache for asset: {asset}"); + return new List(); + } + } + + /// + /// Saves individual prediction data to MongoDB + /// + public async Task SaveIndividualPredictionAsync(SynthPrediction prediction) + { + try + { + prediction.CreatedAt = DateTime.UtcNow; + var dto = MongoMappers.Map(prediction); + + // Check if we already have this cache key and update instead of inserting + var existing = await _individualPredictionsRepository.FindOneAsync(x => x.CacheKey == dto.CacheKey); + if (existing != null) + { + dto.Id = existing.Id; + _individualPredictionsRepository.Update(dto); + _logger.LogDebug($"💾 **Synth Individual Cache** - Updated individual prediction for miner {prediction.MinerUid}"); + } + else + { + await _individualPredictionsRepository.InsertOneAsync(dto); + _logger.LogDebug($"💾 **Synth Individual Cache** - Saved new individual prediction for miner {prediction.MinerUid}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error saving individual prediction cache for miner {prediction.MinerUid}: {ex.Message}"); + } + } + + /// + /// Saves multiple individual predictions to MongoDB in batch + /// + public async Task SaveIndividualPredictionsAsync(List predictions) + { + if (!predictions.Any()) + { + return; + } + + try + { + var saveTasks = new List(); + + foreach (var prediction in predictions) + { + // Save each prediction individually to handle potential conflicts + saveTasks.Add(SaveIndividualPredictionAsync(prediction)); + } + + await Task.WhenAll(saveTasks); + + _logger.LogInformation($"💾 **Synth Individual Cache** - Successfully saved {predictions.Count} individual predictions to MongoDB"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error saving batch of {predictions.Count} individual predictions"); + } + } + + /// + /// Cleans up old cached data beyond the retention period + /// + public async Task CleanupOldDataAsync(int retentionDays = 30) + { + try + { + var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays); + + // Clean up old leaderboard data + await _leaderboardRepository.DeleteManyAsync(x => x.CreatedAt < cutoffDate); + + // Clean up old individual predictions data + await _individualPredictionsRepository.DeleteManyAsync(x => x.CreatedAt < cutoffDate); + + _logger.LogInformation($"🧹 **Synth Cache** - Cleaned up old Synth cache data older than {retentionDays} days"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error during cleanup of old Synth cache data"); + } + } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Tests/SynthPredictionTests.cs b/src/Managing.Infrastructure.Tests/SynthPredictionTests.cs new file mode 100644 index 0000000..0431aec --- /dev/null +++ b/src/Managing.Infrastructure.Tests/SynthPredictionTests.cs @@ -0,0 +1,540 @@ +using Managing.Application.Abstractions.Repositories; +using Managing.Application.Synth; +using Managing.Domain.Bots; +using Managing.Domain.Candles; +using Managing.Domain.MoneyManagements; +using Managing.Domain.Risk; +using Managing.Domain.Strategies; +using Managing.Domain.Synth.Models; +using Managing.Domain.Users; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; +using Xunit.Abstractions; +using static Managing.Common.Enums; + +namespace Managing.Infrastructure.Tests; + +public class SynthPredictionTests +{ + private readonly ITestOutputHelper _testOutputHelper; + + public SynthPredictionTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + /// + /// Helper method to create a test signal with realistic candle data + /// + private static Signal CreateTestSignal(Ticker ticker, TradeDirection direction, decimal price, + DateTime? date = null) + { + var signalDate = date ?? DateTime.UtcNow; + var candle = new Candle + { + Date = signalDate, + Open = price * 0.999m, + High = price * 1.001m, + Low = price * 0.998m, + Close = price, + BaseVolume = 1000m, + QuoteVolume = price * 1000m + }; + + return new Signal( + ticker: ticker, + direction: direction, + confidence: Confidence.Medium, // Will be updated by validation + candle: candle, + date: signalDate, + exchange: TradingExchanges.GmxV2, + indicatorType: IndicatorType.Stc, + signalType: SignalType.Signal, + indicatorName: "TestIndicator", + user: new User { Name = "TestUser" } + ); + } + + [Fact] + public async Task GetProbabilityOfTargetPriceAsync_ShouldReturnValidProbability_ForBTC_RealAPI() + { + // Arrange - Static values for testing + const decimal currentBtcPrice = 102000m; // Current BTC price at $102k + const decimal takeProfitPrice = currentBtcPrice * 1.02m; // 2% TP = $104,040 + const decimal stopLossPrice = currentBtcPrice * 0.99m; // 1% SL = $100,980 + const int timeHorizonHours = 24; // 24 hour forecast + + Console.WriteLine($"🚀 Starting Synth API Test for BTC at ${currentBtcPrice:N0}"); + + // Create real API client and service + var httpClient = new HttpClient(); + var logger = new TestLogger(); + var synthApiClient = new SynthApiClient(httpClient, new TestLogger()); + var mockSynthRepository = new Mock(); + var synthPredictionService = new SynthPredictionService(synthApiClient, mockSynthRepository.Object, logger); + + // Create configuration for enabled Synth API + var config = new SynthConfiguration + { + IsEnabled = true, + TopMinersCount = 5, // Use fewer miners for faster testing + TimeIncrement = 300, // 5 minutes (supported by Synth API) + DefaultTimeLength = timeHorizonHours * 3600, // 24 hours in seconds + MaxLiquidationProbability = 0.10m, + PredictionCacheDurationMinutes = 1 // Short cache for testing + }; + + // Act & Assert - Test Take Profit probability (upward movement for LONG) + try + { + Console.WriteLine("🔍 Fetching Take Profit probability from Synth API..."); + + var takeProfitProbability = await synthPredictionService.GetProbabilityOfTargetPriceAsync( + asset: "BTC", + currentPrice: currentBtcPrice, + targetPrice: takeProfitPrice, + timeHorizonSeconds: timeHorizonHours * 3600, + isLongPosition: false, // For TP, we want upward movement (opposite of liquidation direction) + config: config); + + Console.WriteLine($"🎯 Take Profit Analysis (2% gain):"); + Console.WriteLine($"Current Price: ${currentBtcPrice:N0}"); + Console.WriteLine($"Target Price: ${takeProfitPrice:N0}"); + Console.WriteLine($"Probability: {takeProfitProbability:P2}"); + + Assert.True(takeProfitProbability >= 0m && takeProfitProbability <= 1m, + "Take profit probability should be between 0 and 1"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ Take Profit test failed: {ex.Message}"); + Console.WriteLine("⚠️ Skipping Take Profit test due to API issue"); + } + + // Act & Assert - Test Stop Loss probability (downward movement for LONG) + try + { + Console.WriteLine("\n🔍 Fetching Stop Loss probability from Synth API..."); + + var stopLossProbability = await synthPredictionService.GetProbabilityOfTargetPriceAsync( + asset: "BTC", + currentPrice: currentBtcPrice, + targetPrice: stopLossPrice, + timeHorizonSeconds: timeHorizonHours * 3600, + isLongPosition: true, // For SL in long position, we check downward movement + config: config); + + Console.WriteLine($"🛑 Stop Loss Analysis (1% loss):"); + Console.WriteLine($"Current Price: ${currentBtcPrice:N0}"); + Console.WriteLine($"Stop Loss Price: ${stopLossPrice:N0}"); + Console.WriteLine($"Liquidation Risk: {stopLossProbability:P2}"); + + Assert.True(stopLossProbability >= 0m && stopLossProbability <= 1m, + "Stop loss probability should be between 0 and 1"); + + // Risk assessment - typical risk thresholds + if (stopLossProbability > 0.20m) + { + Console.WriteLine("⚠️ HIGH RISK: Liquidation probability exceeds 20%"); + } + else if (stopLossProbability > 0.10m) + { + Console.WriteLine("⚡ MODERATE RISK: Liquidation probability between 10-20%"); + } + else + { + Console.WriteLine("✅ LOW RISK: Liquidation probability below 10%"); + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ Stop Loss test failed: {ex.Message}"); + Console.WriteLine("⚠️ Skipping Stop Loss test due to API issue"); + } + + Console.WriteLine($"\n📊 Money Management Summary:"); + Console.WriteLine($"Position: LONG BTC"); + Console.WriteLine($"Entry: ${currentBtcPrice:N0}"); + Console.WriteLine($"Take Profit: ${takeProfitPrice:N0} (+2.00%)"); + Console.WriteLine($"Stop Loss: ${stopLossPrice:N0} (-1.00%)"); + Console.WriteLine($"Risk/Reward Ratio: 1:2"); + Console.WriteLine($"Time Horizon: {timeHorizonHours} hours"); + Console.WriteLine("🏁 Test completed!"); + } + + + [Fact] + public async Task ValidateSignalAsync_ShouldUseCustomThresholds_ForSignalFiltering_RealAPI() + { + // Arrange - Static values for custom threshold testing + const decimal currentBtcPrice = 107300m; // Current BTC price at $105,700 + + Console.WriteLine($"🔧 Starting RiskManagement Configuration Test for BTC at ${currentBtcPrice:N0}"); + + // Create real API client and service + var httpClient = new HttpClient(); + var logger = new TestLogger(); + var synthApiClient = new SynthApiClient(httpClient, new TestLogger()); + var mockSynthRepository = new Mock(); + var synthPredictionService = new SynthPredictionService(synthApiClient, mockSynthRepository.Object, logger); + + // Define test scenarios for both LONG and SHORT signals + var signalDirections = new[] + { + new { Direction = TradeDirection.Long, Name = "LONG" }, + new { Direction = TradeDirection.Short, Name = "SHORT" } + }; + + // Define RiskManagement configurations to test + var riskConfigs = new[] + { + new + { + Name = "Default (Moderate)", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.25m, // 25% - balanced threshold + FavorableProbabilityThreshold = 0.30m, // 30% - reasonable expectation + RiskAversion = 1.5m, // Moderate risk aversion + KellyMinimumThreshold = 0.02m, // 2% - practical minimum + KellyMaximumCap = 0.20m, // 20% - reasonable maximum + KellyFractionalMultiplier = 0.75m, // 75% of Kelly (conservative) + RiskTolerance = RiskToleranceLevel.Moderate + } + }, + new + { + Name = "Conservative", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.20m, // 20% - stricter threshold + FavorableProbabilityThreshold = 0.40m, // 40% - higher TP expectation + RiskAversion = 2.0m, // Higher risk aversion + KellyMinimumThreshold = 0.03m, // 3% - higher minimum + KellyMaximumCap = 0.15m, // 15% - lower maximum + KellyFractionalMultiplier = 0.50m, // 50% of Kelly (very conservative) + RiskTolerance = RiskToleranceLevel.Conservative + } + }, + new + { + Name = "Aggressive", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.35m, // 35% - more permissive + FavorableProbabilityThreshold = 0.25m, // 25% - lower TP barrier + RiskAversion = 1.0m, // Lower risk aversion + KellyMinimumThreshold = 0.01m, // 1% - lower minimum + KellyMaximumCap = 0.30m, // 30% - higher maximum + KellyFractionalMultiplier = 1.0m, // 100% of Kelly (full Kelly) + RiskTolerance = RiskToleranceLevel.Aggressive + } + }, + new + { + Name = "Moderate-Plus", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.30m, // 30% - slightly more permissive + FavorableProbabilityThreshold = 0.35m, // 35% - balanced expectation + RiskAversion = 1.2m, // Slightly less risk-averse + KellyMinimumThreshold = 0.015m, // 1.5% - practical minimum + KellyMaximumCap = 0.25m, // 25% - reasonable maximum + KellyFractionalMultiplier = 0.85m, // 85% of Kelly + RiskTolerance = RiskToleranceLevel.Moderate + } + }, + new + { + Name = "Risk-Focused", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.18m, // 18% - tight risk control + FavorableProbabilityThreshold = 0.45m, // 45% - high TP requirement + RiskAversion = 2.5m, // High risk aversion + KellyMinimumThreshold = 0.025m, // 2.5% - higher minimum + KellyMaximumCap = 0.12m, // 12% - very conservative maximum + KellyFractionalMultiplier = 0.40m, // 40% of Kelly (very conservative) + RiskTolerance = RiskToleranceLevel.Conservative + } + }, + new + { + Name = "Ultra-Conservative", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.16m, // 16% - very strict threshold (should trigger some LOWs) + FavorableProbabilityThreshold = 0.60m, // 60% - very high TP requirement + RiskAversion = 3.5m, // Very high risk aversion + KellyMinimumThreshold = 0.04m, // 4% - high minimum barrier + KellyMaximumCap = 0.08m, // 8% - very low maximum (forces heavy capping) + KellyFractionalMultiplier = 0.25m, // 25% of Kelly (ultra conservative) + RiskTolerance = RiskToleranceLevel.Conservative + } + }, + new + { + Name = + "Paranoid-Blocking", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.12m, // 12% - very strict (should block 22-25% SL signals) + FavorableProbabilityThreshold = 0.60m, // 60% - very high TP requirement + RiskAversion = 4.0m, // Extremely high risk aversion + KellyMinimumThreshold = 0.05m, // 5% - very high minimum + KellyMaximumCap = 0.06m, // 6% - extremely conservative maximum + KellyFractionalMultiplier = 0.15m, // 15% of Kelly (extremely conservative) + RiskTolerance = RiskToleranceLevel.Conservative, + SignalValidationTimeHorizonHours = 24 + } + }, + new + { + Name = "Extreme-Blocking", + RiskConfig = new RiskManagement + { + AdverseProbabilityThreshold = 0.08m, // 8% - extremely strict (will block 22-25% SL signals) + FavorableProbabilityThreshold = 0.70m, // 70% - extremely high TP requirement + RiskAversion = 5.0m, // Maximum risk aversion + KellyMinimumThreshold = 0.08m, // 8% - very high minimum + KellyMaximumCap = 0.05m, // 5% - extremely small maximum + KellyFractionalMultiplier = 0.10m, // 10% of Kelly (ultra-conservative) + RiskTolerance = RiskToleranceLevel.Conservative, + SignalValidationTimeHorizonHours = 24 + } + } + }; + + // Store results for summary + var testResults = + new Dictionary>(); + + // Test each RiskManagement configuration with both LONG and SHORT signals + foreach (var configTest in riskConfigs) + { + Console.WriteLine($"\n📊 Testing {configTest.Name})"); + testResults[configTest.Name] = + new Dictionary(); + + // Create bot configuration with the specific RiskManagement + var botConfig = new TradingBotConfig + { + BotTradingBalance = 50000m, // $50k trading balance for realistic utility calculations + Timeframe = Timeframe.FifteenMinutes, + UseSynthApi = true, + UseForSignalFiltering = true, + UseForPositionSizing = true, + UseForDynamicStopLoss = false, + RiskManagement = configTest.RiskConfig, // Use the specific risk configuration + MoneyManagement = new MoneyManagement + { + Name = "Test Money Management", + StopLoss = 0.02m, // 2% stop loss + TakeProfit = 0.022m, // 4% take profit (1:2 risk/reward ratio) + Leverage = 10m, + Timeframe = Timeframe.FifteenMinutes + } + }; + + foreach (var signal in signalDirections) + { + try + { + Console.WriteLine($" 🎯 {signal.Name} Signal Test"); + + // Create a test signal for this direction + var testSignal = CreateTestSignal(Ticker.BTC, signal.Direction, currentBtcPrice); + + var result = await synthPredictionService.ValidateSignalAsync( + signal: testSignal, + currentPrice: currentBtcPrice, + botConfig: botConfig, + isBacktest: false, + customThresholds: null); // No custom thresholds - use RiskManagement config + + testResults[configTest.Name][signal.Name] = result; + + Console.WriteLine($" 🎯 Confidence: {result.Confidence}"); + Console.WriteLine( + $" 📊 SL Risk: {result.StopLossProbability:P2} | TP Prob: {result.TakeProfitProbability:P2}"); + Console.WriteLine( + $" 🎲 TP/SL Ratio: {result.TpSlRatio:F2}x | Win/Loss: {result.WinLossRatio:F2}:1"); + Console.WriteLine($" 💰 Expected Value: ${result.ExpectedMonetaryValue:F2}"); + Console.WriteLine($" 🧮 Expected Utility: {result.ExpectedUtility:F4}"); + Console.WriteLine( + $" 🎯 Kelly: {result.KellyFraction:P2} (Capped: {result.KellyCappedFraction:P2})"); + Console.WriteLine($" 📊 Kelly Assessment: {result.KellyAssessment}"); + Console.WriteLine($" ✅ Kelly Favorable: {result.IsKellyFavorable(configTest.RiskConfig)}"); + Console.WriteLine($" 🚫 Blocked: {result.IsBlocked}"); + + // Debug: Show actual probability values and threshold comparison + var adverseThreshold = configTest.RiskConfig.AdverseProbabilityThreshold; + Console.WriteLine( + $" 🔍 DEBUG - SL: {result.StopLossProbability:F4} | TP: {result.TakeProfitProbability:F4} | Threshold: {adverseThreshold:F4}"); + Console.WriteLine( + $" 🔍 DEBUG - SL > Threshold: {result.StopLossProbability > adverseThreshold} | TP > SL: {result.TakeProfitProbability > result.StopLossProbability}"); + + // Assert that the method works with RiskManagement configuration + Assert.True(Enum.IsDefined(typeof(Confidence), result.Confidence), + $"{configTest.Name} - {signal.Name} signal should return a valid Confidence level"); + + // Assert that Kelly calculations were performed + Assert.True(result.KellyFraction >= 0, "Kelly fraction should be non-negative"); + Assert.True(result.KellyCappedFraction >= 0, "Capped Kelly fraction should be non-negative"); + + // Assert that Expected Utility calculations were performed + Assert.True(result.TradingBalance > 0, "Trading balance should be set from bot config"); + Assert.Equal(botConfig.BotTradingBalance, result.TradingBalance); + } + catch (Exception ex) + { + Console.WriteLine($" ❌ {signal.Name} signal test failed: {ex.Message}"); + // Create a fallback result for error cases + testResults[configTest.Name][signal.Name] = new SignalValidationResult + { + Confidence = Confidence.High, // Default to high confidence on error + ValidationContext = $"Error: {ex.Message}" + }; + } + } + } + + // Display comprehensive results summary + Console.WriteLine($"\n📈 Comprehensive RiskManagement Configuration Test Summary:"); + Console.WriteLine($"Asset: BTC | Price: ${currentBtcPrice:N0} | Trading Balance: ${50000:N0}"); + Console.WriteLine($"Stop Loss: 2.0% | Take Profit: 4.0% | Risk/Reward Ratio: 1:2.0"); + + _testOutputHelper.WriteLine($"\n🎯 Results Matrix:"); + _testOutputHelper.WriteLine( + $"{"Configuration",-20} {"LONG Confidence",-15} {"LONG Kelly",-12} {"SHORT Confidence",-16} {"SHORT Kelly",-12}"); + _testOutputHelper.WriteLine(new string('-', 85)); + + foreach (var configTest in riskConfigs) + { + var longResult = testResults[configTest.Name].GetValueOrDefault("LONG"); + var shortResult = testResults[configTest.Name].GetValueOrDefault("SHORT"); + + var longConf = longResult?.Confidence ?? Confidence.None; + var shortConf = shortResult?.Confidence ?? Confidence.None; + var longKelly = longResult?.KellyCappedFraction ?? 0m; + var shortKelly = shortResult?.KellyCappedFraction ?? 0m; + + _testOutputHelper.WriteLine( + $"{configTest.Name,-20} {GetConfidenceDisplay(longConf),-15} {longKelly,-12:P1} {GetConfidenceDisplay(shortConf),-16} {shortKelly,-12:P1}"); + } + + // Display detailed ValidationContext for each configuration and direction + Console.WriteLine($"\n📊 Detailed Analysis Results:"); + Console.WriteLine(new string('=', 120)); + + foreach (var configTest in riskConfigs) + { + Console.WriteLine($"\n🔧 {configTest.Name}"); + Console.WriteLine(new string('-', 80)); + + var longResult = testResults[configTest.Name].GetValueOrDefault("LONG"); + var shortResult = testResults[configTest.Name].GetValueOrDefault("SHORT"); + + if (longResult != null) + { + Console.WriteLine($"📈 LONG Signal Analysis:"); + Console.WriteLine($" Context: {longResult.ValidationContext ?? "N/A"}"); + Console.WriteLine( + $" Confidence: {GetConfidenceDisplay(longResult.Confidence)} | Blocked: {longResult.IsBlocked}"); + Console.WriteLine( + $" Kelly Assessment: {longResult.KellyAssessment} | Kelly Favorable: {longResult.IsKellyFavorable(configTest.RiskConfig)}"); + if (longResult.TradingBalance > 0) + { + Console.WriteLine( + $" Trading Balance: ${longResult.TradingBalance:N0} | Risk Assessment: {longResult.GetUtilityRiskAssessment()}"); + } + } + else + { + Console.WriteLine($"📈 LONG Signal Analysis: ERROR - No result available"); + } + + Console.WriteLine(); // Empty line for separation + + if (shortResult != null) + { + Console.WriteLine($"📉 SHORT Signal Analysis:"); + Console.WriteLine($" Context: {shortResult.ValidationContext ?? "N/A"}"); + Console.WriteLine( + $" Confidence: {GetConfidenceDisplay(shortResult.Confidence)} | Blocked: {shortResult.IsBlocked}"); + Console.WriteLine( + $" Kelly Assessment: {shortResult.KellyAssessment} | Kelly Favorable: {shortResult.IsKellyFavorable(configTest.RiskConfig)}"); + if (shortResult.TradingBalance > 0) + { + Console.WriteLine( + $" Trading Balance: ${shortResult.TradingBalance:N0} | Risk Assessment: {shortResult.GetUtilityRiskAssessment()}"); + } + } + else + { + Console.WriteLine($"📉 SHORT Signal Analysis: ERROR - No result available"); + } + } + + Console.WriteLine($"\n📊 Risk Configuration Analysis:"); + Console.WriteLine($"• Default: Balanced 20% adverse threshold, 1% Kelly minimum"); + Console.WriteLine($"• Conservative: Strict 15% adverse, 2% Kelly min, half-Kelly multiplier"); + Console.WriteLine($"• Aggressive: Permissive 30% adverse, 0.5% Kelly min, full Kelly"); + Console.WriteLine($"• Custom Permissive: Very permissive 35% adverse, low barriers"); + Console.WriteLine($"• Custom Strict: Very strict 10% adverse, high barriers, conservative sizing"); + + Console.WriteLine($"\n💡 Key Insights:"); + Console.WriteLine($"• Conservative configs should block more signals (lower confidence)"); + Console.WriteLine($"• Aggressive configs should allow more signals (higher confidence)"); + Console.WriteLine($"• Kelly fractions should vary based on risk tolerance settings"); + Console.WriteLine($"• Expected Utility should reflect trading balance and risk aversion"); + + // Verify that we have results for all configurations and directions + foreach (var configTest in riskConfigs) + { + foreach (var signal in signalDirections) + { + Assert.True(testResults.ContainsKey(configTest.Name) && + testResults[configTest.Name].ContainsKey(signal.Name), + $"Should have test result for {configTest.Name} - {signal.Name}"); + + var result = testResults[configTest.Name][signal.Name]; + Assert.NotNull(result); + Assert.True(result.TradingBalance > 0, "Trading balance should be populated from bot config"); + } + } + + Console.WriteLine("🏁 Comprehensive RiskManagement Configuration Test completed!"); + } + + /// + /// Helper method to display confidence levels with emojis + /// + /// Confidence level + /// Formatted confidence display + private static string GetConfidenceDisplay(Confidence confidence) + { + return confidence switch + { + Confidence.High => "🟢 HIGH", + Confidence.Medium => "🟡 MEDIUM", + Confidence.Low => "🟠 LOW", + Confidence.None => "🔴 NONE", + _ => "❓ UNKNOWN" + }; + } +} + +// Simple test logger implementation +public class TestLogger : ILogger +{ + public IDisposable BeginScope(TState state) => null; + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) + { + // Silent logger for tests - output goes to Console.WriteLine + } +} \ No newline at end of file diff --git a/src/Managing.Nswag/Program.cs b/src/Managing.Nswag/Program.cs index d68ebf6..72154b4 100644 --- a/src/Managing.Nswag/Program.cs +++ b/src/Managing.Nswag/Program.cs @@ -7,6 +7,25 @@ using NSwag.CodeGeneration.TypeScript; var document = OpenApiDocument.FromUrlAsync(("http://localhost:5000/swagger/v1/swagger.json")).Result; +// Get the solution directory by going up from the current executable location +var currentDirectory = Directory.GetCurrentDirectory(); +var solutionDirectory = currentDirectory; + +// Navigate up until we find the src directory or reach a reasonable limit +for (int i = 0; i < 10; i++) +{ + if (Directory.Exists(Path.Combine(solutionDirectory, "src"))) + break; + + var parent = Directory.GetParent(solutionDirectory); + if (parent == null) + break; + solutionDirectory = parent.FullName; +} + +var targetDirectory = Path.Combine(solutionDirectory, "src", "Managing.WebApp", "src", "generated"); +Directory.CreateDirectory(targetDirectory); // Ensure the directory exists + var settings = new TypeScriptClientGeneratorSettings { ClassName = "{controller}Client", @@ -30,7 +49,27 @@ var settings = new TypeScriptClientGeneratorSettings var generatorApiClient = new TypeScriptClientGenerator(document, settings); var codeApiClient = generatorApiClient.GenerateFile(); -File.WriteAllText("ManagingApi.ts", codeApiClient); + +// Add the necessary imports after the auto-generated comment +var requiredImports = @" +import AuthorizedApiBase from ""./AuthorizedApiBase""; +import IConfig from ""./IConfig""; +"; + +// Find the end of the auto-generated comment and insert imports +var autoGeneratedEndIndex = codeApiClient.IndexOf("//----------------------"); +if (autoGeneratedEndIndex != -1) +{ + // Find the second occurrence (end of the comment block) + autoGeneratedEndIndex = codeApiClient.IndexOf("//----------------------", autoGeneratedEndIndex + 1); + if (autoGeneratedEndIndex != -1) + { + autoGeneratedEndIndex = codeApiClient.IndexOf("\n", autoGeneratedEndIndex) + 1; + codeApiClient = codeApiClient.Insert(autoGeneratedEndIndex, requiredImports); + } +} + +File.WriteAllText(Path.Combine(targetDirectory, "ManagingApi.ts"), codeApiClient); var settingsTypes = new TypeScriptClientGeneratorSettings { @@ -53,4 +92,4 @@ var settingsTypes = new TypeScriptClientGeneratorSettings var generatorTypes = new TypeScriptClientGenerator(document, settingsTypes); var codeTypes = generatorTypes.GenerateFile(); -File.WriteAllText("ManagingApiTypes.ts", codeTypes); \ No newline at end of file +File.WriteAllText(Path.Combine(targetDirectory, "ManagingApiTypes.ts"), codeTypes); \ No newline at end of file diff --git a/src/Managing.WebApp/Dockerfile-web-ui-dev b/src/Managing.WebApp/Dockerfile-web-ui-dev index a3199ec..5fd37b7 100644 --- a/src/Managing.WebApp/Dockerfile-web-ui-dev +++ b/src/Managing.WebApp/Dockerfile-web-ui-dev @@ -1,5 +1,5 @@ # Use an official Node.js image as the base -FROM node:18-alpine +FROM node:22.14.0-alpine # Set the working directory in the container WORKDIR /app @@ -8,37 +8,38 @@ WORKDIR /app ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true # Install git and Python -#RUN apk update && apk add --no-cache git python3 make g++ +RUN apk update && apk add --no-cache git python3 make g++ -# Create a symlink for python3 as python -#RUN ln -sf /usr/bin/python3 /usr/bin/python +# Create a symlink for python3 as python +# This might not be strictly necessary for your current issue but good to keep if Python scripts are involved. +# RUN ln -sf /usr/bin/python3 /usr/bin/python -# Copy package.json and package-lock.json to the container -# COPY package*.json ./ -COPY /src/Managing.WebApp/package.json ./ +# Copy package.json and package-lock.json to the container +# COPY package*.json ./ +COPY /src/Managing.WebApp/package.json ./ -# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts -RUN npm install --legacy-peer-deps -RUN npm install -g tailwindcss postcss autoprefixer @tailwindcss/typography +# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts +RUN npm install --legacy-peer-deps --loglevel verbose +RUN npm install -g tailwindcss postcss autoprefixer @tailwindcss/typography -# Copy the rest of the app's source code to the container -# COPY . . -RUN ls -la -COPY src/Managing.WebApp/ . -RUN node --max-old-space-size=8192 ./node_modules/.bin/vite build +# Copy the rest of the app's source code to the container +# COPY . . +RUN ls -la +COPY src/Managing.WebApp/ . +RUN node --max-old-space-size=8192 ./node_modules/.bin/vite build -# Build the app -RUN npm run build +# Build the app +RUN npm run build -# Use NGINX as the web server -FROM nginx:alpine +# Use NGINX as the web server +FROM nginx:alpine -# Copy the built app to the NGINX web server directory -# COPY --from=0 /app/build /usr/share/nginx/html -COPY --from=0 /app/dist /usr/share/nginx/html +# Copy the built app to the NGINX web server directory +# COPY --from=0 /app/build /usr/share/nginx/html +COPY --from=0 /app/dist /usr/share/nginx/html -# Expose port 80 for the NGINX web server -EXPOSE 80 +# Expose port 80 for the NGINX web server +EXPOSE 80 -# Start the NGINX web server +# Start the NGINX web server CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/src/Managing.WebApp/package.json b/src/Managing.WebApp/package.json index 1526cdc..e37f8a4 100644 --- a/src/Managing.WebApp/package.json +++ b/src/Managing.WebApp/package.json @@ -32,7 +32,6 @@ "canonicalize": "^2.0.0", "classnames": "^2.3.1", "connectkit": "^1.8.2", - "crypto": "^1.0.1", "date-fns": "^2.30.0", "elliptic": "^6.6.1", "jotai": "^1.6.7", diff --git a/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx b/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx index 2fe36e0..4be069c 100644 --- a/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx +++ b/src/Managing.WebApp/src/components/mollecules/BotConfigModal/BotConfigModal.tsx @@ -13,6 +13,7 @@ import { Ticker, Timeframe, TradingBotConfig, + TradingBotConfigRequest, UpdateBotConfigRequest } from '../../../generated/ManagingApi' import Toast from '../Toast/Toast' @@ -58,6 +59,11 @@ const BotConfigModal: React.FC = ({ customStopLoss: number customTakeProfit: number customLeverage: number + // Synth API fields + useSynthApi: boolean + useForPositionSizing: boolean + useForSignalFiltering: boolean + useForDynamicStopLoss: boolean }>({ name: '', accountName: '', @@ -77,9 +83,16 @@ const BotConfigModal: React.FC = ({ useCustomMoneyManagement: false, customStopLoss: 0.01, customTakeProfit: 0.02, - customLeverage: 1 + customLeverage: 1, + useSynthApi: false, + useForPositionSizing: true, + useForSignalFiltering: true, + useForDynamicStopLoss: true }) + // State for advanced parameters dropdown + const [showAdvancedParams, setShowAdvancedParams] = useState(false) + // Fetch data const { data: accounts } = useQuery({ queryFn: async () => { @@ -110,11 +123,11 @@ const BotConfigModal: React.FC = ({ if (mode === 'create' && backtest) { // Initialize from backtest setFormData({ - name: `Bot-${backtest.config.scenarioName}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`, + name: `Bot-${backtest.config.scenarioName || 'Custom'}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`, accountName: backtest.config.accountName, moneyManagementName: moneyManagements?.[0]?.name || '', ticker: backtest.config.ticker, - scenarioName: backtest.config.scenarioName, + scenarioName: backtest.config.scenarioName || '', timeframe: backtest.config.timeframe, isForWatchingOnly: false, botTradingBalance: 1000, @@ -128,7 +141,11 @@ const BotConfigModal: React.FC = ({ useCustomMoneyManagement: true, // Default to custom for backtests customStopLoss: backtest.config.moneyManagement?.stopLoss || 0.01, customTakeProfit: backtest.config.moneyManagement?.takeProfit || 0.02, - customLeverage: backtest.config.moneyManagement?.leverage || 1 + customLeverage: backtest.config.moneyManagement?.leverage || 1, + useSynthApi: false, + useForPositionSizing: true, + useForSignalFiltering: true, + useForDynamicStopLoss: true }) } else if (mode === 'update' && existingBot) { // Initialize from existing bot @@ -137,7 +154,7 @@ const BotConfigModal: React.FC = ({ accountName: existingBot.config.accountName, moneyManagementName: existingBot.config.moneyManagement?.name || '', ticker: existingBot.config.ticker, - scenarioName: existingBot.config.scenarioName, + scenarioName: existingBot.config.scenarioName || '', timeframe: existingBot.config.timeframe, isForWatchingOnly: existingBot.config.isForWatchingOnly, botTradingBalance: existingBot.config.botTradingBalance, @@ -151,7 +168,11 @@ const BotConfigModal: React.FC = ({ useCustomMoneyManagement: false, customStopLoss: existingBot.config.moneyManagement?.stopLoss || 0.01, customTakeProfit: existingBot.config.moneyManagement?.takeProfit || 0.02, - customLeverage: existingBot.config.moneyManagement?.leverage || 1 + customLeverage: existingBot.config.moneyManagement?.leverage || 1, + useSynthApi: existingBot.config.useSynthApi || false, + useForPositionSizing: existingBot.config.useForPositionSizing || true, + useForSignalFiltering: existingBot.config.useForSignalFiltering || true, + useForDynamicStopLoss: existingBot.config.useForDynamicStopLoss || true }) } else if (mode === 'create' && !backtest) { // Initialize for new bot creation @@ -174,7 +195,11 @@ const BotConfigModal: React.FC = ({ useCustomMoneyManagement: false, customStopLoss: 0.01, customTakeProfit: 0.02, - customLeverage: 1 + customLeverage: 1, + useSynthApi: false, + useForPositionSizing: true, + useForSignalFiltering: true, + useForDynamicStopLoss: true }) } }, [mode, backtest, existingBot, accounts, moneyManagements, scenarios]) @@ -216,6 +241,17 @@ const BotConfigModal: React.FC = ({ })) } + const handleSynthApiToggle = (enabled: boolean) => { + setFormData(prev => ({ + ...prev, + useSynthApi: enabled, + // Reset sub-options when main toggle is turned off + useForPositionSizing: enabled ? prev.useForPositionSizing : false, + useForSignalFiltering: enabled ? prev.useForSignalFiltering : false, + useForDynamicStopLoss: enabled ? prev.useForDynamicStopLoss : false + })) + } + const handleSubmit = async () => { const t = new Toast(mode === 'create' ? 'Creating bot...' : 'Updating bot...') const client = new BotClient({}, apiUrl) @@ -249,30 +285,31 @@ const BotConfigModal: React.FC = ({ return } - // Create TradingBotConfig (reused for both create and update) - const tradingBotConfig: TradingBotConfig = { + // Create TradingBotConfigRequest (instead of TradingBotConfig) + const tradingBotConfigRequest: TradingBotConfigRequest = { accountName: formData.accountName, ticker: formData.ticker, - scenarioName: formData.scenarioName, + scenarioName: formData.scenarioName || undefined, timeframe: formData.timeframe, botType: formData.botType, isForWatchingOnly: formData.isForWatchingOnly, - isForBacktest: false, cooldownPeriod: formData.cooldownPeriod, maxLossStreak: formData.maxLossStreak, maxPositionTimeHours: formData.maxPositionTimeHours, flipOnlyWhenInProfit: formData.flipOnlyWhenInProfit, - flipPosition: formData.flipPosition, name: formData.name, botTradingBalance: formData.botTradingBalance, - moneyManagement: moneyManagement, - closeEarlyWhenProfitable: formData.closeEarlyWhenProfitable + closeEarlyWhenProfitable: formData.closeEarlyWhenProfitable, + useSynthApi: formData.useSynthApi, + useForPositionSizing: formData.useForPositionSizing, + useForSignalFiltering: formData.useForSignalFiltering, + useForDynamicStopLoss: formData.useForDynamicStopLoss } if (mode === 'create') { // Create new bot const request: StartBotRequest = { - config: tradingBotConfig, + config: tradingBotConfigRequest, moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName } @@ -282,7 +319,7 @@ const BotConfigModal: React.FC = ({ // Update existing bot const request: UpdateBotConfigRequest = { identifier: existingBot!.identifier, - config: tradingBotConfig, + config: tradingBotConfigRequest, moneyManagementName: formData.useCustomMoneyManagement ? undefined : formData.moneyManagementName } @@ -373,7 +410,12 @@ const BotConfigModal: React.FC = ({
= ({
= ({ />
-
- - handleInputChange('cooldownPeriod', parseInt(e.target.value))} - min="1" - /> -
- -
- - handleInputChange('maxLossStreak', parseInt(e.target.value))} - min="0" - /> -
- -
- - handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)} - min="0.1" - step="0.1" - placeholder="Optional" - /> -
- - {/* Checkboxes */}
- -
- -
- -
- -
- -
- -
+ {/* Advanced Parameters Dropdown */} +
+ +
+ + {showAdvancedParams && ( +
+
+
+ + handleInputChange('cooldownPeriod', parseInt(e.target.value))} + min="1" + /> +
+ +
+ + handleInputChange('maxLossStreak', parseInt(e.target.value))} + min="0" + /> +
+ +
+ + handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)} + min="0.1" + step="0.1" + placeholder="Optional" + /> +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ )} + {/* Money Management Section */}
Money Management
)} + {/* Synth API Section */} +
Synth API Configuration
+ +
+
+ +
+
+ + {/* Show sub-options only when Synth API is enabled */} + {formData.useSynthApi && ( +
+
+ +
+ +
+ +
+ +
+ +
+
+ )} + {/* Validation Messages */} {formData.closeEarlyWhenProfitable && !formData.maxPositionTimeHours && (
diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx index 98f3b1e..e836a5a 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestModal.tsx @@ -4,18 +4,18 @@ import {type SubmitHandler, useForm} from 'react-hook-form' import useApiUrlStore from '../../../app/store/apiStore' import { - AccountClient, - BacktestClient, - BotType, - DataClient, - MoneyManagement, - MoneyManagementClient, - RunBacktestRequest, - Scenario, - ScenarioClient, - Ticker, - Timeframe, - TradingBotConfig, + AccountClient, + BacktestClient, + BotType, + DataClient, + MoneyManagement, + MoneyManagementClient, + RunBacktestRequest, + Scenario, + ScenarioClient, + Ticker, + Timeframe, + TradingBotConfigRequest, } from '../../../generated/ManagingApi' import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type' import {Loader, Slider} from '../../atoms' @@ -42,7 +42,7 @@ const BacktestModal: React.FC = ({ const [startDate, setStartDate] = useState(defaultStartDateString); const [endDate, setEndDate] = useState(defaultEndDateString); - const { register, handleSubmit, setValue } = useForm({ + const { register, handleSubmit, setValue, watch } = useForm({ defaultValues: { startDate: defaultStartDateString, endDate: defaultEndDateString, @@ -51,9 +51,30 @@ const BacktestModal: React.FC = ({ maxPositionTimeHours: null, // Default to null (disabled) flipOnlyWhenInProfit: true, // Default to true balance: 10000, // Default balance - closeEarlyWhenProfitable: false // Default to false + closeEarlyWhenProfitable: false, // Default to false + // Synth API defaults + useSynthApi: false, + useForPositionSizing: true, + useForSignalFiltering: true, + useForDynamicStopLoss: true } }); + + // Watch the useSynthApi value to conditionally show/hide sub-options + const useSynthApi = watch('useSynthApi'); + + // Reset sub-options when main Synth API toggle is turned off + useEffect(() => { + if (!useSynthApi) { + setValue('useForPositionSizing', false); + setValue('useForSignalFiltering', false); + setValue('useForDynamicStopLoss', false); + } + }, [useSynthApi, setValue]); + + // State for advanced parameters dropdown + const [showAdvancedParams, setShowAdvancedParams] = useState(false); + const [selectedAccount, setSelectedAccount] = useState('') const [selectedTimeframe, setSelectedTimeframe] = useState(Timeframe.OneHour) const [selectedLoopQuantity, setLoopQuantity] = React.useState( @@ -127,36 +148,48 @@ const BacktestModal: React.FC = ({ console.log(customScenario) try { - // Create the TradingBotConfig - const tradingBotConfig: TradingBotConfig = { + // Create the TradingBotConfigRequest (note the Request suffix) + const tradingBotConfigRequest: TradingBotConfigRequest = { accountName: form.accountName, ticker: ticker as Ticker, scenarioName: customScenario ? undefined : scenarioName, - scenario: customScenario, + scenario: customScenario ? { + name: customScenario.name || 'Custom Scenario', + indicators: customScenario.indicators?.map(indicator => ({ + name: indicator.name || 'Unnamed Indicator', + type: indicator.type!, + signalType: indicator.signalType!, + minimumHistory: indicator.minimumHistory || 0, + period: indicator.period, + fastPeriods: indicator.fastPeriods, + slowPeriods: indicator.slowPeriods, + signalPeriods: indicator.signalPeriods, + multiplier: indicator.multiplier, + smoothPeriods: indicator.smoothPeriods, + stochPeriods: indicator.stochPeriods, + cyclePeriods: indicator.cyclePeriods + })) || [], + loopbackPeriod: customScenario.loopbackPeriod + } : undefined, timeframe: form.timeframe, botType: form.botType, isForWatchingOnly: false, // Always false for backtests - isForBacktest: true, // Always true for backtests cooldownPeriod: form.cooldownPeriod || 1, maxLossStreak: form.maxLossStreak || 0, maxPositionTimeHours: form.maxPositionTimeHours || null, flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true, - flipPosition: form.botType === BotType.FlippingBot, // Set based on bot type name: `Backtest-${customScenario ? customScenario.name : scenarioName}-${ticker}-${new Date().toISOString()}`, botTradingBalance: form.balance, - moneyManagement: customMoneyManagement || moneyManagements?.find(m => m.name === selectedMoneyManagement) || moneyManagements?.[0] || { - name: 'placeholder', - leverage: 1, - stopLoss: 0.01, - takeProfit: 0.02, - timeframe: form.timeframe - }, - closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false + closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false, + useSynthApi: form.useSynthApi ?? false, + useForPositionSizing: form.useForPositionSizing ?? true, + useForSignalFiltering: form.useForSignalFiltering ?? true, + useForDynamicStopLoss: form.useForDynamicStopLoss ?? true }; // Create the RunBacktestRequest const request: RunBacktestRequest = { - config: tradingBotConfig, + config: tradingBotConfigRequest, // Use the request object startDate: new Date(form.startDate), endDate: new Date(form.endDate), balance: form.balance, @@ -199,7 +232,7 @@ const BacktestModal: React.FC = ({ function onMoneyManagementChange(e: any) { if (e.target.value === 'custom') { setShowCustomMoneyManagement(true) - setCustomMoneyManagement(e.target.value) + setCustomMoneyManagement(undefined) // Reset custom money management when switching to custom mode } else { setShowCustomMoneyManagement(false) setCustomMoneyManagement(undefined) @@ -210,7 +243,7 @@ const BacktestModal: React.FC = ({ function onScenarioChange(e: any) { if (e.target.value === 'custom') { setShowCustomScenario(true) - setCustomScenario(e.target.value) + setCustomScenario(undefined) // Reset custom scenario when switching to custom mode } else { setShowCustomScenario(false) setCustomScenario(undefined) @@ -232,6 +265,11 @@ const BacktestModal: React.FC = ({ useEffect(() => { if (scenarios && scenarios.length > 0 && scenarios[0].name) { setValue('scenarioName', scenarios[0].name); + setShowCustomScenario(false); // Hide custom scenario when scenarios are available + } else if (scenarios && scenarios.length === 0) { + // No scenarios available, automatically show custom scenario creation + setShowCustomScenario(true); + setValue('scenarioName', ''); // Clear any selected scenario } }, [scenarios, setValue]); @@ -263,6 +301,12 @@ const BacktestModal: React.FC = ({ if (moneyManagements && moneyManagements.length > 0){ setSelectedMoneyManagement(moneyManagements[0].name) setCustomMoneyManagement(undefined) + setShowCustomMoneyManagement(false) // Hide custom money management when options are available + } else if (moneyManagements && moneyManagements.length === 0) { + // No money management options available, automatically show custom money management + setShowCustomMoneyManagement(true) + setSelectedMoneyManagement(undefined) + setCustomMoneyManagement(undefined) } }, [moneyManagements]) @@ -336,13 +380,19 @@ const BacktestModal: React.FC = ({ }, })} > - {moneyManagements.map((item) => ( - - ))} + {moneyManagements.length === 0 ? ( + + ) : ( + <> + {moneyManagements.map((item) => ( + + ))} + + )} @@ -405,18 +455,31 @@ const BacktestModal: React.FC = ({ }, })} > - + {scenarios.length === 0 ? ( + + ) : ( + + )} {scenarios.map((item) => ( ))} + {showCustomScenario && ( +
+ +
+ )} + = ({ }} /> - - - -
- {/* Fifth Row: Max Loss Streak & Max Position Time */} -
- - - + {/* Advanced Parameters Dropdown */} +
+ +
- - -
- Leave empty to disable time-based position closure + {showAdvancedParams && ( +
+ {/* Cooldown Period & Dates */} +
+ + Cooldown Period (candles) +
+ i +
+
+ } + htmlFor="cooldownPeriod" + > + +
- -
- {/* Sixth Row: Flip Only When In Profit & Close Early When Profitable */} -
- - -
- If enabled, positions will only flip when current position is profitable + {/* Start Date & End Date */} +
+ + { + setStartDate(e.target.value); + setValue('startDate', e.target.value); + }} + /> + + + + { + setEndDate(e.target.value); + setValue('endDate', e.target.value); + }} + /> +
- - - -
- If enabled, positions will close early when they become profitable + {/* Loop Slider (if enabled) */} + {showLoopSlider && ( + + Loop +
+ i +
+
+ } + htmlFor="loop" + > + setLoopQuantity(Number(e.target.value))} + > +
+ )} + + {/* Max Loss Streak & Max Position Time */} +
+ + Max Loss Streak +
+ i +
+
+ } + htmlFor="maxLossStreak" + > + + + + + Max Position Time (hours) +
+ i +
+
+ } + htmlFor="maxPositionTimeHours" + > + +
-
-
- {/* Seventh Row: Save */} -
- - - -
+ {/* Flip Only When In Profit & Close Early When Profitable */} +
+ + Flip Only When In Profit +
+ i +
+
+ } + htmlFor="flipOnlyWhenInProfit" + > + + - {/* Eighth Row: Start Date & End Date */} -
- - { - setStartDate(e.target.value); - setValue('startDate', e.target.value); - }} - /> - + + Close Early When Profitable +
+ i +
+
+ } + htmlFor="closeEarlyWhenProfitable" + > + + + - - { - setEndDate(e.target.value); - setValue('endDate', e.target.value); - }} - /> - - + {/* Save Option */} +
+ + Save Backtest Results +
+ i +
+
+ } + htmlFor="save" + > + + + - {/* Loop Slider (if enabled) */} - {showLoopSlider && ( - - setLoopQuantity(Number(e.target.value))} - > - + {/* Synth API Section */} +
Synth API Configuration
+ +
+ + Enable Synth API +
+ i +
+
+ } + htmlFor="useSynthApi" + > + + + + + {/* Show sub-options only when Synth API is enabled */} + {useSynthApi && ( +
+ + Use for Position Sizing +
+ i +
+
+ } + htmlFor="useForPositionSizing" + > + + + + + Use for Signal Filtering +
+ i +
+ + } + htmlFor="useForSignalFiltering" + > + +
+ + + Use for Dynamic Stop Loss +
+ i +
+ + } + htmlFor="useForDynamicStopLoss" + > + +
+ + )} + )}
- +
diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx index 07bf275..0cef63d 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx @@ -4,21 +4,16 @@ import {CardPosition, CardText} from '../../mollecules' interface IBacktestRowDetailsProps { backtest: Backtest; - optimizedMoneyManagement: { - stopLoss: number; - takeProfit: number; - }; } const BacktestRowDetails: React.FC = ({ - backtest, - optimizedMoneyManagement + backtest }) => { const { candles, positions, walletBalances, - strategiesValues, + indicatorsValues, signals, statistics, config @@ -364,7 +359,7 @@ const BacktestRowDetails: React.FC = ({ candles={candles} positions={positions} walletBalances={walletBalances} - strategiesValues={strategiesValues} + indicatorsValues={indicatorsValues} signals={signals} > diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx index 74bf1fd..1044f79 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestTable.tsx @@ -1,4 +1,4 @@ -import {ChevronDownIcon, ChevronRightIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid' +import {ChevronDownIcon, ChevronRightIcon, CogIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid' import React, {useEffect, useState} from 'react' import useApiUrlStore from '../../../app/store/apiStore' @@ -6,7 +6,7 @@ import type {Backtest} from '../../../generated/ManagingApi' import {BacktestClient} from '../../../generated/ManagingApi' import type {IBacktestCards} from '../../../global/type' import {CardText, SelectColumnFilter, Table} from '../../mollecules' -import BotConfigModal from '../../mollecules/BotConfigModal/BotConfigModal' +import {UnifiedTradingModal} from '../index' import Toast from '../../mollecules/Toast/Toast' import BacktestRowDetails from './backtestRowDetails' @@ -32,6 +32,10 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest const [showBotConfigModal, setShowBotConfigModal] = useState(false) const [selectedBacktest, setSelectedBacktest] = useState(null) + // Backtest configuration modal state + const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false) + const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState(null) + const handleOpenBotConfigModal = (backtest: Backtest) => { setSelectedBacktest(backtest) setShowBotConfigModal(true) @@ -42,6 +46,16 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest setSelectedBacktest(null) } + const handleOpenBacktestConfigModal = (backtest: Backtest) => { + setSelectedBacktestForRerun(backtest) + setShowBacktestConfigModal(true) + } + + const handleCloseBacktestConfigModal = () => { + setShowBacktestConfigModal(false) + setSelectedBacktestForRerun(null) + } + async function deleteBacktest(id: string) { const t = new Toast('Deleting backtest') const client = new BacktestClient({}, apiUrl) @@ -213,6 +227,23 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest accessor: 'id', disableFilters: true, }, + { + Cell: ({ cell }: any) => ( + <> +
+ +
+ + ), + Header: '', + accessor: 'rerun', + disableFilters: true, + }, { Cell: ({ cell }: any) => ( <> @@ -429,18 +460,29 @@ const BacktestTable: React.FC = ({ list, isFetching, setBacktest renderRowSubCompontent={({ row }: any) => ( )} /> {/* Bot Configuration Modal */} {selectedBacktest && ( - + )} + + {/* Backtest Configuration Modal */} + {selectedBacktestForRerun && ( + )} diff --git a/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx b/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx index fbc5d3f..92e1553 100644 --- a/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx +++ b/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx @@ -16,11 +16,11 @@ import {useEffect, useRef, useState} from 'react' import type { Candle, + IndicatorsResultBase, + IndicatorType, KeyValuePairOfDateTimeAndDecimal, Position, Signal, - StrategiesResultBase, - StrategyType, } from '../../../../generated/ManagingApi' import {PositionStatus, TradeDirection,} from '../../../../generated/ManagingApi' import useTheme from '../../../../hooks/useTheme' @@ -45,7 +45,7 @@ type ITradeChartProps = { positions: Position[] signals: Signal[] walletBalances?: KeyValuePairOfDateTimeAndDecimal[] | null - strategiesValues?: { [key in keyof typeof StrategyType]?: StrategiesResultBase; } | null; + indicatorsValues?: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; } | null; stream?: Candle | null width: number height: number @@ -56,7 +56,7 @@ const TradeChart = ({ positions, signals, walletBalances, - strategiesValues, + indicatorsValues, stream, width, height, @@ -246,9 +246,6 @@ const TradeChart = ({ const data: CandlestickData[] = candles.map((c) => mapCandle(c)) let diff = 0; // Default to 0 if there's not enough data to calculate the difference - console.log(data) - console.log(data.length) - if (data.length > 3) { diff = (data[data.length - 1].time as number) - @@ -308,7 +305,7 @@ const TradeChart = ({ } // Price panel - if (strategiesValues?.EmaTrend != null || strategiesValues?.EmaCross != null) + if (indicatorsValues?.EmaTrend != null || indicatorsValues?.EmaCross != null) { const emaSeries = chart.current.addLineSeries({ color: theme.secondary, @@ -323,7 +320,7 @@ const TradeChart = ({ title: 'EMA', }) - const ema = strategiesValues.EmaTrend?.ema ?? strategiesValues.EmaCross?.ema + const ema = indicatorsValues.EmaTrend?.ema ?? indicatorsValues.EmaCross?.ema const emaData = ema?.map((w) => { return { @@ -339,7 +336,7 @@ const TradeChart = ({ } } - if (strategiesValues?.SuperTrend != null) { + if (indicatorsValues?.SuperTrend != null) { const superTrendSeries = chart.current.addLineSeries({ color: theme.info, lineWidth: 1, @@ -351,7 +348,7 @@ const TradeChart = ({ }) - const superTrend = strategiesValues.SuperTrend.superTrend?.map((w) => { + const superTrend = indicatorsValues.SuperTrend.superTrend?.map((w) => { return { time: moment(w.date).unix(), value: w.superTrend, @@ -361,6 +358,46 @@ const TradeChart = ({ superTrendSeries.setData(superTrend) } + // Display chandeliers exits + if (indicatorsValues?.ChandelierExit != null) { + const chandelierExitsLongsSeries = chart.current.addLineSeries({ + color: theme.info, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: 'Chandelier Long', + pane: 0, + }) + + const chandelierExitsLongs = indicatorsValues.ChandelierExit.chandelierLong?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.chandelierExit, + } + }) + // @ts-ignore + chandelierExitsLongsSeries.setData(chandelierExitsLongs) + + const chandelierExitsShortsSeries = chart.current.addLineSeries({ + color: theme.error, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: 'Chandelier Short', + pane: 0, + }) + + const chandelierExitsShorts = indicatorsValues.ChandelierExit.chandelierShort?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.chandelierExit, + } + }) + + // @ts-ignore + chandelierExitsShortsSeries.setData(chandelierExitsShorts) + } + if (markers.length > 0) { series1.current.setMarkers(markers) } @@ -369,14 +406,14 @@ const TradeChart = ({ var paneCount = 1 - if (strategiesValues?.RsiDivergence != null || strategiesValues?.RsiDivergenceConfirm != null) + if (indicatorsValues?.RsiDivergence != null || indicatorsValues?.RsiDivergenceConfirm != null) { const rsiSeries = chart.current.addLineSeries({ pane: paneCount, title: 'RSI', }) - const rsi = strategiesValues.RsiDivergence?.rsi ?? strategiesValues.RsiDivergenceConfirm?.rsi + const rsi = indicatorsValues.RsiDivergence?.rsi ?? indicatorsValues.RsiDivergenceConfirm?.rsi const rsiData = rsi?.map((w) => { return { @@ -397,7 +434,7 @@ const TradeChart = ({ paneCount++ } - if (strategiesValues?.Stc != null) { + if (indicatorsValues?.Stc != null) { const stcSeries = chart.current.addBaselineSeries({ pane: paneCount, baseValue: {price: 50, type: 'price'}, @@ -408,7 +445,7 @@ const TradeChart = ({ stcSeries.createPriceLine(buildLine(theme.error, 25, 'low')) stcSeries.createPriceLine(buildLine(theme.info, 75, 'high')) - const stcData = strategiesValues?.Stc.stc?.map((w) => { + const stcData = indicatorsValues?.Stc.stc?.map((w) => { return { time: moment(w.date).unix(), value: w.stc, @@ -430,7 +467,42 @@ const TradeChart = ({ paneCount++ } - if (strategiesValues?.MacdCross != null) { + if (indicatorsValues?.LaggingStc != null) { + const laggingStcSeries = chart.current.addBaselineSeries({ + pane: paneCount, + baseValue: {price: 50, type: 'price'}, + + title: 'Lagging STC', + }) + + laggingStcSeries.createPriceLine(buildLine(theme.error, 25, 'low')) + laggingStcSeries.createPriceLine(buildLine(theme.info, 75, 'high')) + + const stcData = indicatorsValues?.LaggingStc.stc?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.stc, + } + }) + // @ts-ignore + laggingStcSeries.setData(stcData) + laggingStcSeries.applyOptions({ + ...baselineOptions, + priceLineVisible: true, + priceFormat: { + minMove: 1, + precision: 1, + type: 'price', + }, + crosshairMarkerVisible: true, + }) + + paneCount++ + } + + console.log(indicatorsValues) + if (indicatorsValues?.MacdCross != null) { + console.log(indicatorsValues.MacdCross) const histogramSeries = chart.current.addHistogramSeries({ color: theme.accent, title: 'MACD', @@ -441,7 +513,7 @@ const TradeChart = ({ } }) - const macd = strategiesValues.MacdCross.macd?.map((w) => { + const macd = indicatorsValues.MacdCross.macd?.map((w) => { return { time: moment(w.date).unix(), value: w.histogram, @@ -472,7 +544,7 @@ const TradeChart = ({ crosshairMarkerVisible: true, }) - const macdData = strategiesValues.MacdCross.macd?.map((w) => { + const macdData = indicatorsValues.MacdCross.macd?.map((w) => { return { time: moment(w.date).unix(), value: w.macd, @@ -497,7 +569,7 @@ const TradeChart = ({ }, }) - const signalData = strategiesValues.MacdCross.macd?.map((w) => { + const signalData = indicatorsValues.MacdCross.macd?.map((w) => { return { time: moment(w.date).unix(), value: w.signal, @@ -510,7 +582,7 @@ const TradeChart = ({ paneCount++ } - if (strategiesValues?.StochRsiTrend){ + if (indicatorsValues?.StochRsiTrend){ const stochRsiSeries = chart.current.addLineSeries({ ...baselineOptions, priceLineVisible: false, @@ -518,7 +590,7 @@ const TradeChart = ({ pane: paneCount, }) - const stochRsi = strategiesValues.StochRsiTrend.stochRsi?.map((w) => { + const stochRsi = indicatorsValues.StochRsiTrend.stochRsi?.map((w) => { return { time: moment(w.date).unix(), value: w.stochRsi, @@ -529,7 +601,7 @@ const TradeChart = ({ paneCount++ } - if (strategiesValues?.StDev != null) { + if (indicatorsValues?.StDev != null) { const stDevSeries = chart.current.addLineSeries({ color: theme.primary, lineWidth: 1, @@ -539,7 +611,7 @@ const TradeChart = ({ pane: paneCount, }) - const stDev = strategiesValues.StDev.stdDev?.map((w) => { + const stDev = indicatorsValues.StDev.stdDev?.map((w) => { return { time: moment(w.date).unix(), value: w.stdDev, @@ -557,7 +629,7 @@ const TradeChart = ({ crosshairMarkerVisible: true, }) - const zScore = strategiesValues.StDev.stdDev?.map((w) => { + const zScore = indicatorsValues.StDev.stdDev?.map((w) => { return { time: moment(w.date).unix(), value: w.zScore, @@ -569,6 +641,47 @@ const TradeChart = ({ paneCount++ } + + // Display dual EMA crossover + if (indicatorsValues?.DualEmaCross != null) { + const fastEmaSeries = chart.current.addLineSeries({ + color: theme.info, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: 'Fast EMA', + pane: paneCount, + }) + + const fastEma = indicatorsValues.DualEmaCross.fastEma?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.ema, + } + }) + // @ts-ignore + fastEmaSeries.setData(fastEma) + + const slowEmaSeries = chart.current.addLineSeries({ + color: theme.primary, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: 'Slow EMA', + pane: paneCount, + }) + + const slowEma = indicatorsValues.DualEmaCross.slowEma?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.ema, + } + }) + // @ts-ignore + slowEmaSeries.setData(slowEma) + paneCount++ + } + if (walletBalances != null && walletBalances.length > 0) { const walletSeries = chart.current.addBaselineSeries({ baseValue: {price: walletBalances[0].value, type: 'price'}, diff --git a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/README.md b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/README.md new file mode 100644 index 0000000..4ce376b --- /dev/null +++ b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/README.md @@ -0,0 +1,124 @@ +# UnifiedTradingModal + +A unified modal component that replaces both `BacktestModal` and `BotConfigModal`. This component handles three different modes: + +- **backtest**: Run backtests with multiple tickers +- **createBot**: Create a new trading bot (optionally from a backtest) +- **updateBot**: Update an existing bot's configuration + +## Features + +### ✅ **Unified Interface** +- Single component for all trading configuration needs +- Mode-specific form fields and validation +- Consistent UI/UX across all use cases + +### ✅ **Advanced Configuration** +- **Advanced Parameters**: Collapsible section with cooldown periods, position limits, trading options +- **Risk Management**: Complete `RiskManagement` configuration with preset levels (Conservative, Moderate, Aggressive) +- **Synth API Integration**: AI-powered probabilistic forecasts and risk assessment + +### ✅ **Smart Defaults** +- Context-aware initialization based on mode +- Automatic data loading and form population +- Preset risk management configurations + +## Usage Examples + +### 1. Backtest Mode +```tsx +import { UnifiedTradingModal } from '../../components/organism' + + setShowModal(false)} + mode="backtest" + setBacktests={setBacktests} + showLoopSlider={true} // Optional: enable loop optimization +/> +``` + +### 2. Create Bot Mode +```tsx + setShowModal(false)} + mode="createBot" +/> +``` + +### 3. Create Bot from Backtest +```tsx + setShowModal(false)} + mode="createBot" + backtest={selectedBacktest} // Initialize from backtest +/> +``` + +### 4. Update Bot Mode +```tsx + setShowModal(false)} + mode="updateBot" + existingBot={{ + identifier: bot.identifier, + config: bot.config + }} +/> +``` + +## Migration Guide + +### From BacktestModal +```tsx +// Old + + +// New + +``` + +### From BotConfigModal +```tsx +// Old + + +// New + +``` + +## Risk Management Features + +The component includes a comprehensive Risk Management section with: + +- **Preset Levels**: Conservative, Moderate, Aggressive configurations +- **Kelly Criterion**: Position sizing based on edge and odds +- **Expected Utility**: Risk-adjusted decision making +- **Probability Thresholds**: Adverse and favorable signal validation +- **Liquidation Risk**: Maximum acceptable liquidation probability + +All risk management parameters use the official `RiskManagement` type from the API and integrate seamlessly with the backend trading logic. \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx new file mode 100644 index 0000000..859bde9 --- /dev/null +++ b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/UnifiedTradingModal.tsx @@ -0,0 +1,1426 @@ +import {useQuery} from '@tanstack/react-query' +import React, {useEffect, useState} from 'react' +import {type SubmitHandler, useForm} from 'react-hook-form' + +import useApiUrlStore from '../../../app/store/apiStore' +import { + AccountClient, + BacktestClient, + BotClient, + BotType, + DataClient, + MoneyManagement, + MoneyManagementClient, + RiskManagement, + RiskToleranceLevel, + RunBacktestRequest, + Scenario, + ScenarioClient, + ScenarioRequest, + SignalType, + StartBotRequest, + Ticker, + Timeframe, + TradingBotConfigRequest, + UpdateBotConfigRequest, +} from '../../../generated/ManagingApi' +import type {IUnifiedTradingConfigInput, UnifiedTradingModalProps} from '../../../global/type' +import {Loader, Slider} from '../../atoms' +import {Modal, Toast} from '../../mollecules' +import FormInput from '../../mollecules/FormInput/FormInput' +import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement' +import CustomScenario from '../CustomScenario/CustomScenario' + +const UnifiedTradingModal: React.FC = ({ + showModal, + closeModal, + mode, + showLoopSlider = false, + setBacktests, + backtest, + existingBot, +}) => { + // Default dates for backtests + const defaultStartDate = new Date(); + defaultStartDate.setDate(defaultStartDate.getDate() - 15); + const defaultStartDateString = defaultStartDate.toISOString().split('T')[0]; + + const defaultEndDate = new Date(); + const defaultEndDateString = defaultEndDate.toISOString().split('T')[0]; + + const [startDate, setStartDate] = useState(defaultStartDateString); + const [endDate, setEndDate] = useState(defaultEndDateString); + + // Initialize default risk management + const getDefaultRiskManagement = (): RiskManagement => ({ + adverseProbabilityThreshold: 0.20, + favorableProbabilityThreshold: 0.30, + riskAversion: 1.0, + kellyMinimumThreshold: 0.01, + kellyMaximumCap: 0.25, + maxLiquidationProbability: 0.10, + signalValidationTimeHorizonHours: 24, + positionMonitoringTimeHorizonHours: 6, + positionWarningThreshold: 0.20, + positionAutoCloseThreshold: 0.50, + kellyFractionalMultiplier: 1.0, + riskTolerance: RiskToleranceLevel.Moderate, + useExpectedUtility: true, + useKellyCriterion: true, + }); + + // Default form values based on mode + const getDefaultFormValues = (): Partial => { + const base = { + startDate: defaultStartDateString, + endDate: defaultEndDateString, + cooldownPeriod: 10, + maxLossStreak: 0, + maxPositionTimeHours: null, + flipOnlyWhenInProfit: true, + balance: 10000, + closeEarlyWhenProfitable: false, + useSynthApi: false, + useForPositionSizing: true, + useForSignalFiltering: true, + useForDynamicStopLoss: true, + useCustomRiskManagement: false, + riskManagement: getDefaultRiskManagement(), + }; + + if (mode === 'backtest') { + return { + ...base, + save: false, + }; + } else { + return { + ...base, + name: `${mode === 'createBot' ? 'Bot' : 'Updated-Bot'}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`, + isForWatchingOnly: false, + flipPosition: false, + }; + } + }; + + const { register, handleSubmit, setValue, watch, getValues } = useForm({ + defaultValues: getDefaultFormValues(), + }); + + // Watch important values + const useSynthApi = watch('useSynthApi'); + const useCustomRiskManagement = watch('useCustomRiskManagement'); + const selectedTimeframe = watch('timeframe'); + + // State management continues below... + + // State for collapsible sections + const [showAdvancedParams, setShowAdvancedParams] = useState(false); + const [showRiskManagement, setShowRiskManagement] = useState(false); + + // State for loop slider (backtests only) + const [selectedLoopQuantity, setLoopQuantity] = React.useState( + showLoopSlider ? 3 : 1 + ); + + // Custom components state + const [customMoneyManagement, setCustomMoneyManagement] = useState(undefined); + const [selectedMoneyManagement, setSelectedMoneyManagement] = useState(); + const [showCustomMoneyManagement, setShowCustomMoneyManagement] = useState(false); + + const [customScenario, setCustomScenario] = useState(undefined); + const [selectedScenario, setSelectedScenario] = useState(); + const [showCustomScenario, setShowCustomScenario] = useState(false); + + // Selected ticker for bots (separate from form tickers for backtests) + const [selectedTicker, setSelectedTicker] = useState(undefined); + + const { apiUrl } = useApiUrlStore(); + + // API clients + const scenarioClient = new ScenarioClient({}, apiUrl); + const accountClient = new AccountClient({}, apiUrl); + const dataClient = new DataClient({}, apiUrl); + const moneyManagementClient = new MoneyManagementClient({}, apiUrl); + const backtestClient = new BacktestClient({}, apiUrl); + const botClient = new BotClient({}, apiUrl); + + // Data fetching + const { data: accounts } = useQuery({ + queryFn: () => accountClient.account_GetAccounts(), + queryKey: ['accounts'], + }); + + const { data: scenarios } = useQuery({ + queryFn: () => scenarioClient.scenario_GetScenarios(), + queryKey: ['scenarios'], + }); + + const { data: moneyManagements } = useQuery({ + queryFn: async () => { + return await moneyManagementClient.moneyManagement_GetMoneyManagements(); + }, + queryKey: ['moneyManagements'], + }); + + const { data: tickers, refetch: refetchTickers } = useQuery({ + enabled: !!selectedTimeframe && mode === 'backtest', + queryFn: () => { + if (selectedTimeframe) { + return dataClient.data_GetTickers(selectedTimeframe); + } + }, + queryKey: ['tickers', selectedTimeframe], + }); + + // Initialize form based on mode and props + useEffect(() => { + if (mode === 'createBot' && backtest) { + // Initialize from backtest + setValue('name', `Bot-${backtest.config.scenarioName || 'Custom'}-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '')}`); + setValue('accountName', backtest.config.accountName); + // For bots, we'll use the first ticker from the backtest + if (backtest.config.ticker) { + // Store ticker in state for bot submission + setSelectedTicker(backtest.config.ticker); + } + setValue('scenarioName', backtest.config.scenarioName || ''); + setValue('timeframe', backtest.config.timeframe); + setValue('botType', backtest.config.botType); + setValue('cooldownPeriod', backtest.config.cooldownPeriod); + setValue('maxLossStreak', backtest.config.maxLossStreak); + setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours); + setValue('flipOnlyWhenInProfit', backtest.config.flipOnlyWhenInProfit || false); + setValue('closeEarlyWhenProfitable', backtest.config.closeEarlyWhenProfitable || false); + setValue('balance', 1000); // Default for bots + setValue('isForWatchingOnly', false); + setValue('useSynthApi', backtest.config.useSynthApi || false); + setValue('useForPositionSizing', backtest.config.useForPositionSizing ?? true); + setValue('useForSignalFiltering', backtest.config.useForSignalFiltering ?? true); + setValue('useForDynamicStopLoss', backtest.config.useForDynamicStopLoss ?? true); + + // Use backtest's money management as custom + if (backtest.config.moneyManagement) { + setShowCustomMoneyManagement(true); + setCustomMoneyManagement(backtest.config.moneyManagement); + } + } else if (mode === 'backtest' && backtest) { + // Initialize from existing backtest for re-running + setValue('accountName', backtest.config.accountName); + setValue('scenarioName', backtest.config.scenarioName || ''); + setValue('timeframe', backtest.config.timeframe); + setValue('botType', backtest.config.botType); + setValue('cooldownPeriod', backtest.config.cooldownPeriod); + setValue('maxLossStreak', backtest.config.maxLossStreak); + setValue('maxPositionTimeHours', backtest.config.maxPositionTimeHours); + setValue('flipOnlyWhenInProfit', backtest.config.flipOnlyWhenInProfit || false); + setValue('closeEarlyWhenProfitable', backtest.config.closeEarlyWhenProfitable || false); + setValue('balance', backtest.config.botTradingBalance || 10000); + setValue('save', false); // Default to not saving for re-runs + setValue('useSynthApi', backtest.config.useSynthApi || false); + setValue('useForPositionSizing', backtest.config.useForPositionSizing ?? true); + setValue('useForSignalFiltering', backtest.config.useForSignalFiltering ?? true); + setValue('useForDynamicStopLoss', backtest.config.useForDynamicStopLoss ?? true); + + // Set tickers for backtest (array) + if (backtest.config.ticker) { + setValue('tickers', [backtest.config.ticker]); + } + + // Set dates from backtest + if (backtest.startDate) { + const backtestStartDate = new Date(backtest.startDate).toISOString().split('T')[0]; + setStartDate(backtestStartDate); + setValue('startDate', backtestStartDate); + } + if (backtest.endDate) { + const backtestEndDate = new Date(backtest.endDate).toISOString().split('T')[0]; + setEndDate(backtestEndDate); + setValue('endDate', backtestEndDate); + } + + // Use backtest's money management as custom + if (backtest.config.moneyManagement) { + setShowCustomMoneyManagement(true); + setCustomMoneyManagement(backtest.config.moneyManagement); + } + + // Handle risk management + if (backtest.config.riskManagement) { + setValue('useCustomRiskManagement', true); + setValue('riskManagement', backtest.config.riskManagement); + } + } else if (mode === 'updateBot' && existingBot) { + // Initialize from existing bot + const config = existingBot.config; + setValue('name', config.name); + setValue('accountName', config.accountName); + // For bots, store ticker in state + if (config.ticker) { + setSelectedTicker(config.ticker); + } + setValue('scenarioName', config.scenarioName || ''); + setValue('timeframe', config.timeframe); + setValue('botType', config.botType); + setValue('cooldownPeriod', config.cooldownPeriod); + setValue('maxLossStreak', config.maxLossStreak); + setValue('maxPositionTimeHours', config.maxPositionTimeHours); + setValue('flipOnlyWhenInProfit', config.flipOnlyWhenInProfit); + setValue('closeEarlyWhenProfitable', config.closeEarlyWhenProfitable || false); + setValue('balance', config.botTradingBalance); + setValue('isForWatchingOnly', config.isForWatchingOnly); + setValue('useSynthApi', config.useSynthApi || false); + setValue('useForPositionSizing', config.useForPositionSizing ?? true); + setValue('useForSignalFiltering', config.useForSignalFiltering ?? true); + setValue('useForDynamicStopLoss', config.useForDynamicStopLoss ?? true); + + // Handle risk management + if (config.riskManagement) { + setValue('useCustomRiskManagement', true); + setValue('riskManagement', config.riskManagement); + } + } + }, [mode, backtest, existingBot, setValue]); + + // Set defaults when data loads + useEffect(() => { + if (scenarios && scenarios.length > 0 && !getValues('scenarioName')) { + setValue('scenarioName', scenarios[0].name || ''); + } else if (scenarios && scenarios.length === 0) { + setShowCustomScenario(true); + } + }, [scenarios, setValue]); + + useEffect(() => { + if (accounts && accounts.length > 0 && !getValues('accountName')) { + setValue('accountName', accounts[0].name); + } + }, [accounts, setValue]); + + useEffect(() => { + if (moneyManagements && moneyManagements.length > 0 && !selectedMoneyManagement) { + setSelectedMoneyManagement(moneyManagements[0].name); + } else if (moneyManagements && moneyManagements.length === 0) { + setShowCustomMoneyManagement(true); + } + }, [moneyManagements]); + + // Handlers continue below... + + // Event handlers + const handleSynthApiToggle = (enabled: boolean) => { + setValue('useSynthApi', enabled); + if (!enabled) { + // Only reset to false when disabling Synth API + setValue('useForPositionSizing', false); + setValue('useForSignalFiltering', false); + setValue('useForDynamicStopLoss', false); + } else { + // Set to true when enabling Synth API + setValue('useForPositionSizing', true); + setValue('useForSignalFiltering', true); + setValue('useForDynamicStopLoss', true); + } + }; + + const handleRiskToleranceChange = (tolerance: RiskToleranceLevel) => { + const presetConfig = getPresetRiskManagement(tolerance); + setValue('riskManagement', presetConfig); + }; + + const getPresetRiskManagement = (tolerance: RiskToleranceLevel): RiskManagement => { + switch (tolerance) { + case RiskToleranceLevel.Conservative: + return { + adverseProbabilityThreshold: 0.15, + favorableProbabilityThreshold: 0.40, + riskAversion: 2.0, + kellyMinimumThreshold: 0.02, + kellyMaximumCap: 0.15, + maxLiquidationProbability: 0.08, + signalValidationTimeHorizonHours: 24, + positionMonitoringTimeHorizonHours: 6, + positionWarningThreshold: 0.15, + positionAutoCloseThreshold: 0.35, + kellyFractionalMultiplier: 0.5, + riskTolerance: tolerance, + useExpectedUtility: true, + useKellyCriterion: true, + }; + case RiskToleranceLevel.Aggressive: + return { + adverseProbabilityThreshold: 0.30, + favorableProbabilityThreshold: 0.25, + riskAversion: 0.5, + kellyMinimumThreshold: 0.005, + kellyMaximumCap: 0.40, + maxLiquidationProbability: 0.15, + signalValidationTimeHorizonHours: 24, + positionMonitoringTimeHorizonHours: 6, + positionWarningThreshold: 0.30, + positionAutoCloseThreshold: 0.70, + kellyFractionalMultiplier: 1.0, + riskTolerance: tolerance, + useExpectedUtility: true, + useKellyCriterion: true, + }; + default: // Moderate + return getDefaultRiskManagement(); + } + }; + + const onMoneyManagementChange = (e: React.ChangeEvent) => { + if (e.target.value === 'custom') { + setShowCustomMoneyManagement(true); + setCustomMoneyManagement(undefined); + } else { + setShowCustomMoneyManagement(false); + setCustomMoneyManagement(undefined); + setSelectedMoneyManagement(e.target.value); + } + }; + + const onScenarioChange = (e: React.ChangeEvent) => { + if (e.target.value === 'custom') { + setShowCustomScenario(true); + setCustomScenario(undefined); + } else { + setShowCustomScenario(false); + setCustomScenario(undefined); + setSelectedScenario(e.target.value); + setValue('scenarioName', e.target.value); + } + }; + + // Main form submission handler + const onSubmit: SubmitHandler = async (form) => { + if (mode === 'backtest') { + await handleBacktestSubmission(form); + } else { + await handleBotSubmission(form); + } + }; + + // Helper function to convert custom scenario to ScenarioRequest format + const convertScenarioToRequest = (scenario: Scenario): ScenarioRequest => { + return { + name: scenario.name || 'Custom Scenario', + loopbackPeriod: scenario.loopbackPeriod || null, + indicators: scenario.indicators?.map(indicator => ({ + name: indicator.name || '', + type: indicator.type!, + signalType: indicator.signalType || SignalType.Signal, // Default to Signal if not set + minimumHistory: indicator.minimumHistory || 0, + period: indicator.period, + fastPeriods: indicator.fastPeriods, + slowPeriods: indicator.slowPeriods, + signalPeriods: indicator.signalPeriods, + multiplier: indicator.multiplier, + smoothPeriods: indicator.smoothPeriods, + stochPeriods: indicator.stochPeriods, + cyclePeriods: indicator.cyclePeriods, + })).filter(indicator => indicator.type) || [] // Only filter out indicators without type + }; + }; + + // Backtest submission handler + const handleBacktestSubmission = async (form: IUnifiedTradingConfigInput) => { + if (!form.scenarioName && !customScenario) { + new Toast('Please select a scenario or create a custom scenario', false); + return; + } + + if (customScenario && (!customScenario.indicators || customScenario.indicators.length === 0)) { + new Toast('Custom scenario must have at least one indicator', false); + return; + } + + if (!form.tickers || form.tickers.length === 0) { + new Toast('Please select at least one ticker', false); + return; + } + + // Process tickers sequentially + for (const ticker of form.tickers) { + try { + await runBacktest(form, ticker, 0); + } catch (error) { + console.error(`Error running backtest for ${ticker}:`, error); + continue; + } + } + }; + + // Bot submission handler + const handleBotSubmission = async (form: IUnifiedTradingConfigInput) => { + const t = new Toast(mode === 'createBot' ? 'Creating bot...' : 'Updating bot...'); + + try { + // Create money management object + let moneyManagement: MoneyManagement | undefined = undefined; + + if (showCustomMoneyManagement || (mode === 'createBot' && backtest)) { + moneyManagement = customMoneyManagement; + } else { + const selectedMM = moneyManagements?.find(mm => mm.name === selectedMoneyManagement); + if (selectedMM) { + moneyManagement = selectedMM; + } + } + + console.log(form) + console.log(moneyManagement) + if (!moneyManagement) { + t.update('error', 'Money management is required'); + return; + } + + // Create TradingBotConfigRequest + const tradingBotConfigRequest: TradingBotConfigRequest = { + accountName: form.accountName, + ticker: selectedTicker!, // Use selected ticker for bots + scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined, + scenarioName: customScenario ? undefined : form.scenarioName, + timeframe: form.timeframe, + botType: form.botType, + isForWatchingOnly: form.isForWatchingOnly || false, + cooldownPeriod: form.cooldownPeriod, + maxLossStreak: form.maxLossStreak, + maxPositionTimeHours: form.maxPositionTimeHours, + flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true, + name: form.name || 'Unnamed Bot', + botTradingBalance: form.balance, + closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false, + useSynthApi: form.useSynthApi ?? false, + useForPositionSizing: form.useForPositionSizing ?? true, + useForSignalFiltering: form.useForSignalFiltering ?? true, + useForDynamicStopLoss: form.useForDynamicStopLoss ?? true, + moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, + moneyManagement: moneyManagement, + }; + + if (mode === 'createBot') { + const request: StartBotRequest = { + config: tradingBotConfigRequest, + }; + await botClient.bot_Start(request); + t.update('success', 'Bot created successfully!'); + } else { + const request: UpdateBotConfigRequest = { + identifier: existingBot!.identifier, + config: tradingBotConfigRequest, + moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, + }; + await botClient.bot_UpdateBotConfig(request); + t.update('success', 'Bot updated successfully!'); + } + + closeModal(); + } catch (error: any) { + t.update('error', `Error: ${error.message || error}`); + } + }; + + // Run backtest function + async function runBacktest(form: IUnifiedTradingConfigInput, ticker: string, loopCount: number): Promise { + const t = new Toast(`${ticker} is running`); + + try { + const tradingBotConfigRequest: TradingBotConfigRequest = { + accountName: form.accountName, + ticker: ticker as any, + scenario: customScenario ? convertScenarioToRequest(customScenario) : undefined, + scenarioName: customScenario ? undefined : form.scenarioName, + timeframe: form.timeframe, + botType: form.botType, + isForWatchingOnly: false, + cooldownPeriod: form.cooldownPeriod, + maxLossStreak: form.maxLossStreak, + maxPositionTimeHours: form.maxPositionTimeHours, + flipOnlyWhenInProfit: form.flipOnlyWhenInProfit ?? true, + name: `Backtest-${customScenario ? customScenario.name : form.scenarioName}-${ticker}-${new Date().toISOString()}`, + botTradingBalance: form.balance, + closeEarlyWhenProfitable: form.closeEarlyWhenProfitable ?? false, + useSynthApi: form.useSynthApi ?? false, + useForPositionSizing: form.useForPositionSizing ?? true, + useForSignalFiltering: form.useForSignalFiltering ?? true, + useForDynamicStopLoss: form.useForDynamicStopLoss ?? true, + moneyManagementName: showCustomMoneyManagement ? undefined : selectedMoneyManagement, + moneyManagement: customMoneyManagement, + }; + + const request: RunBacktestRequest = { + config: tradingBotConfigRequest, + startDate: new Date(form.startDate), + endDate: new Date(form.endDate), + balance: form.balance, + watchOnly: false, + save: form.save || false, + }; + + const backtest = await backtestClient.backtest_Run(request); + + t.update('success', `${ticker} Backtest Succeeded`); + if (setBacktests) { + setBacktests((arr) => [...arr, backtest]); + } + + if (showLoopSlider && selectedLoopQuantity > loopCount) { + const nextCount = loopCount + 1; + const mm: MoneyManagement = { + leverage: backtest.optimizedMoneyManagement.leverage, + name: backtest.optimizedMoneyManagement.name + nextCount, + stopLoss: backtest.optimizedMoneyManagement.stopLoss, + takeProfit: backtest.optimizedMoneyManagement.takeProfit, + timeframe: backtest.optimizedMoneyManagement.timeframe, + }; + await runBacktest(form, ticker, nextCount); + } + } catch (err: any) { + t.update('error', 'Error: ' + err); + throw err; + } + } + + if (!accounts || !scenarios || !moneyManagements) { + return ; + } + + return ( + +
+ {/* Bot Name (for bot modes only) */} + {mode !== 'backtest' && ( + + + + )} + + {/* First Row: Account & Timeframe */} +
+ + + + + + + +
+ + {/* Second Row: Money Management & Bot Type */} +
+ + + + + + + +
+ + {/* Custom Money Management */} + {showCustomMoneyManagement && ( +
+ +
+ )} + + {/* Scenario Selection */} + + + + + {/* Custom Scenario */} + {showCustomScenario && ( +
+ +
+ )} + + {/* Ticker Selection - Multiple for backtests, Single for bots */} + {mode === 'backtest' ? ( + + + + ) : ( + + + + )} + + {/* Trading Balance */} + + + + + {/* Watch Only Mode (bot modes only) */} + {mode !== 'backtest' && ( + + Watch Only Mode +
+ i +
+
+ } + htmlFor="isForWatchingOnly" + > + + + )} + + {/* Continue with Advanced Parameters section... */} + + {/* Advanced Parameters Dropdown */} +
+ +
+ + {showAdvancedParams && ( +
+ {/* Cooldown Period */} +
+ + Cooldown Period (candles) +
+ i +
+
+ } + htmlFor="cooldownPeriod" + > + + +
+ + {/* Dates for backtests */} + {mode === 'backtest' && ( +
+ + { + setStartDate(e.target.value); + setValue('startDate', e.target.value); + }} + /> + + + + { + setEndDate(e.target.value); + setValue('endDate', e.target.value); + }} + /> + +
+ )} + + {/* Loop Slider (if enabled for backtests) */} + {showLoopSlider && mode === 'backtest' && ( + + Loop +
+ i +
+ + } + htmlFor="loop" + > + setLoopQuantity(Number(e.target.value))} + /> +
+ )} + + {/* Max Loss Streak & Max Position Time */} +
+ + Max Loss Streak +
+ i +
+
+ } + htmlFor="maxLossStreak" + > + + + + + Max Position Time (hours) +
+ i +
+ + } + htmlFor="maxPositionTimeHours" + > + +
+ + + {/* Trading Options */} +
+ + Flip Only When In Profit +
+ i +
+
+ } + htmlFor="flipOnlyWhenInProfit" + > + + + + + Close Early When Profitable +
+ i +
+ + } + htmlFor="closeEarlyWhenProfitable" + > + +
+ + + {/* Position Flipping (bot modes only) */} + {mode !== 'backtest' && ( + + Enable Position Flipping +
+ i +
+ + } + htmlFor="flipPosition" + > + +
+ )} + + {/* Save Option (backtest mode only) */} + {mode === 'backtest' && ( + + Save Backtest Results +
+ i +
+ + } + htmlFor="save" + > + +
+ )} + + {/* Synth API Section */} +
Synth API Configuration
+ +
+ + Enable Synth API +
+ i +
+
+ } + htmlFor="useSynthApi" + > + handleSynthApiToggle(e.target.checked)} + /> + + + + {/* Show sub-options only when Synth API is enabled */} + {useSynthApi && ( +
+ + Use for Position Sizing +
+ i +
+
+ } + htmlFor="useForPositionSizing" + > + + + + + Use for Signal Filtering +
+ i +
+ + } + htmlFor="useForSignalFiltering" + > + +
+ + + Use for Dynamic Stop Loss +
+ i +
+ + } + htmlFor="useForDynamicStopLoss" + > + +
+ + )} + + )} + + {/* Risk Management Section */} +
+ +
+ + {showRiskManagement && ( +
+ + Use Custom Risk Management +
+ i +
+
+ } + htmlFor="useCustomRiskManagement" + > + + + + {useCustomRiskManagement && ( +
+ {/* Risk Tolerance Level */} + + + + + {/* Risk Management Parameters Grid */} +
+ + Adverse Probability Threshold +
+ i +
+
+ } + htmlFor="adverseProbabilityThreshold" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + adverseProbabilityThreshold: parseFloat(e.target.value) + }); + }} + /> + + + + Favorable Probability Threshold +
+ i +
+
+ } + htmlFor="favorableProbabilityThreshold" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + favorableProbabilityThreshold: parseFloat(e.target.value) + }); + }} + /> + + + + Risk Aversion Parameter +
+ i +
+ + } + htmlFor="riskAversion" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + riskAversion: parseFloat(e.target.value) + }); + }} + /> +
+ + + Kelly Minimum Threshold +
+ i +
+ + } + htmlFor="kellyMinimumThreshold" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + kellyMinimumThreshold: parseFloat(e.target.value) + }); + }} + /> +
+ + + Kelly Maximum Cap +
+ i +
+ + } + htmlFor="kellyMaximumCap" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + kellyMaximumCap: parseFloat(e.target.value) + }); + }} + /> +
+ + + Max Liquidation Probability +
+ i +
+ + } + htmlFor="maxLiquidationProbability" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + maxLiquidationProbability: parseFloat(e.target.value) + }); + }} + /> +
+ + + {/* Checkboxes for Kelly and Expected Utility */} +
+ + Use Kelly Criterion +
+ i +
+
+ } + htmlFor="useKellyCriterion" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + useKellyCriterion: e.target.checked + }); + }} + /> + + + + Use Expected Utility +
+ i +
+ + } + htmlFor="useExpectedUtility" + > + { + const currentRM = getValues('riskManagement') || getDefaultRiskManagement(); + setValue('riskManagement', { + ...currentRM, + useExpectedUtility: e.target.checked + }); + }} + /> +
+ + + )} + + )} + + +
+ + +
+
+ ); +}; + +export default UnifiedTradingModal; \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/index.ts b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/index.ts new file mode 100644 index 0000000..0d20d5f --- /dev/null +++ b/src/Managing.WebApp/src/components/organism/UnifiedTradingModal/index.ts @@ -0,0 +1 @@ +export { default } from './UnifiedTradingModal' \ No newline at end of file diff --git a/src/Managing.WebApp/src/components/organism/index.tsx b/src/Managing.WebApp/src/components/organism/index.tsx index a123f7b..2c067dd 100644 --- a/src/Managing.WebApp/src/components/organism/index.tsx +++ b/src/Managing.WebApp/src/components/organism/index.tsx @@ -2,8 +2,12 @@ export { default as TradeChart } from './Trading/TradeChart/TradeChart' export { default as CardPositionItem } from './Trading/CardPositionItem' export { default as ActiveBots } from './ActiveBots/ActiveBots' export { default as BacktestCards } from './Backtest/backtestCards' -export { default as BacktestModal } from './Backtest/backtestModal' export { default as BacktestTable } from './Backtest/backtestTable' +// @deprecated - Use UnifiedTradingModal instead +export { default as BacktestModal } from './Backtest/backtestModal' +export { default as UnifiedTradingModal } from './UnifiedTradingModal' +export { default as CustomMoneyManagement } from './CustomMoneyManagement/CustomMoneyManagement' +export { default as CustomScenario } from './CustomScenario/CustomScenario' export { default as SpotLightBadge } from './SpotLightBadge/SpotLightBadge' export { default as StatusBadge } from './StatusBadge/StatusBadge' export { default as PositionsList } from './Positions/PositionList' diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 1985edc..c1547d0 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -337,6 +337,45 @@ export class BacktestClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + backtest_Map(moneyManagementRequest: MoneyManagementRequest): Promise { + let url_ = this.baseUrl + "/Backtest"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(moneyManagementRequest); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBacktest_Map(_response); + }); + } + + protected processBacktest_Map(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MoneyManagement; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + backtest_Backtest(id: string): Promise { let url_ = this.baseUrl + "/Backtest/{id}"; if (id === undefined || id === null) @@ -726,6 +765,45 @@ export class BotClient extends AuthorizedApiBase { return Promise.resolve(null as any); } + bot_Map(moneyManagementRequest: MoneyManagementRequest): Promise { + let url_ = this.baseUrl + "/Bot"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(moneyManagementRequest); + + let options_: RequestInit = { + body: content_, + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processBot_Map(_response); + }); + } + + protected processBot_Map(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as MoneyManagement; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } + bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise { let url_ = this.baseUrl + "/Bot/OpenPosition"; url_ = url_.replace(/[?&]$/, ""); @@ -1432,7 +1510,7 @@ export class ScenarioClient extends AuthorizedApiBase { this.baseUrl = baseUrl ?? "http://localhost:5000"; } - scenario_GetScenarios(): Promise { + scenario_GetScenarios(): Promise { let url_ = this.baseUrl + "/Scenario"; url_ = url_.replace(/[?&]$/, ""); @@ -1450,13 +1528,13 @@ export class ScenarioClient extends AuthorizedApiBase { }); } - protected processScenario_GetScenarios(response: Response): Promise { + protected processScenario_GetScenarios(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Scenario[]; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ScenarioViewModel[]; return result200; }); } else if (status !== 200 && status !== 204) { @@ -1464,10 +1542,10 @@ export class ScenarioClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } - scenario_CreateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise { + scenario_CreateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise { let url_ = this.baseUrl + "/Scenario?"; if (name !== undefined && name !== null) url_ += "name=" + encodeURIComponent("" + name) + "&"; @@ -1493,13 +1571,13 @@ export class ScenarioClient extends AuthorizedApiBase { }); } - protected processScenario_CreateScenario(response: Response): Promise { + protected processScenario_CreateScenario(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Scenario; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as ScenarioViewModel; return result200; }); } else if (status !== 200 && status !== 204) { @@ -1507,7 +1585,7 @@ export class ScenarioClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } scenario_DeleteScenario(name: string | null | undefined): Promise { @@ -1600,7 +1678,7 @@ export class ScenarioClient extends AuthorizedApiBase { return Promise.resolve(null as any); } - scenario_GetIndicators(): Promise { + scenario_GetIndicators(): Promise { let url_ = this.baseUrl + "/Scenario/indicator"; url_ = url_.replace(/[?&]$/, ""); @@ -1618,13 +1696,13 @@ export class ScenarioClient extends AuthorizedApiBase { }); } - protected processScenario_GetIndicators(response: Response): Promise { + protected processScenario_GetIndicators(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Indicator[]; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as IndicatorViewModel[]; return result200; }); } else if (status !== 200 && status !== 204) { @@ -1632,10 +1710,10 @@ export class ScenarioClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } - scenario_CreateIndicator(indicatorType: IndicatorType | undefined, name: string | null | undefined, period: number | null | undefined, fastPeriods: number | null | undefined, slowPeriods: number | null | undefined, signalPeriods: number | null | undefined, multiplier: number | null | undefined, stochPeriods: number | null | undefined, smoothPeriods: number | null | undefined, cyclePeriods: number | null | undefined): Promise { + scenario_CreateIndicator(indicatorType: IndicatorType | undefined, name: string | null | undefined, period: number | null | undefined, fastPeriods: number | null | undefined, slowPeriods: number | null | undefined, signalPeriods: number | null | undefined, multiplier: number | null | undefined, stochPeriods: number | null | undefined, smoothPeriods: number | null | undefined, cyclePeriods: number | null | undefined): Promise { let url_ = this.baseUrl + "/Scenario/indicator?"; if (indicatorType === null) throw new Error("The parameter 'indicatorType' cannot be null."); @@ -1675,13 +1753,13 @@ export class ScenarioClient extends AuthorizedApiBase { }); } - protected processScenario_CreateIndicator(response: Response): Promise { + protected processScenario_CreateIndicator(response: Response): Promise { const status = response.status; let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; if (status === 200) { return response.text().then((_responseText) => { let result200: any = null; - result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Indicator; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as IndicatorViewModel; return result200; }); } else if (status !== 200 && status !== 204) { @@ -1689,7 +1767,7 @@ export class ScenarioClient extends AuthorizedApiBase { return throwException("An unexpected server error occurred.", status, _responseText, _headers); }); } - return Promise.resolve(null as any); + return Promise.resolve(null as any); } scenario_DeleteIndicator(name: string | null | undefined): Promise { @@ -2779,7 +2857,7 @@ export interface Backtest { walletBalances: KeyValuePairOfDateTimeAndDecimal[]; optimizedMoneyManagement: MoneyManagement; user: User; - strategiesValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; + indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; score: number; } @@ -2796,11 +2874,16 @@ export interface TradingBotConfig { maxLossStreak: number; flipPosition: boolean; name: string; + riskManagement?: RiskManagement | null; scenario?: Scenario | null; scenarioName?: string | null; maxPositionTimeHours?: number | null; closeEarlyWhenProfitable?: boolean; flipOnlyWhenInProfit: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; } export interface MoneyManagement { @@ -2937,6 +3020,29 @@ export enum BotType { FlippingBot = "FlippingBot", } +export interface RiskManagement { + adverseProbabilityThreshold: number; + favorableProbabilityThreshold: number; + riskAversion: number; + kellyMinimumThreshold: number; + kellyMaximumCap: number; + maxLiquidationProbability: number; + signalValidationTimeHorizonHours: number; + positionMonitoringTimeHorizonHours: number; + positionWarningThreshold: number; + positionAutoCloseThreshold: number; + kellyFractionalMultiplier: number; + riskTolerance: RiskToleranceLevel; + useExpectedUtility: boolean; + useKellyCriterion: boolean; +} + +export enum RiskToleranceLevel { + Conservative = "Conservative", + Moderate = "Moderate", + Aggressive = "Aggressive", +} + export interface Scenario { name?: string | null; indicators?: Indicator[] | null; @@ -3086,6 +3192,7 @@ export interface Signal extends ValueObject { indicatorType: IndicatorType; signalType: SignalType; user?: User | null; + indicatorName: string; } export enum SignalStatus { @@ -3226,19 +3333,68 @@ export interface SuperTrendResult extends ResultBase { } export interface RunBacktestRequest { - config?: TradingBotConfig | null; + config?: TradingBotConfigRequest | null; startDate?: Date; endDate?: Date; balance?: number; watchOnly?: boolean; save?: boolean; +} + +export interface TradingBotConfigRequest { + accountName: string; + ticker: Ticker; + timeframe: Timeframe; + isForWatchingOnly: boolean; + botTradingBalance: number; + botType: BotType; + name: string; + cooldownPeriod: number; + maxLossStreak: number; + scenario?: ScenarioRequest | null; + scenarioName?: string | null; moneyManagementName?: string | null; - moneyManagement?: MoneyManagement | null; + moneyManagement?: MoneyManagementRequest | null; + maxPositionTimeHours?: number | null; + closeEarlyWhenProfitable?: boolean; + flipOnlyWhenInProfit?: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; +} + +export interface ScenarioRequest { + name: string; + indicators: IndicatorRequest[]; + loopbackPeriod?: number | null; +} + +export interface IndicatorRequest { + name: string; + type: IndicatorType; + signalType: SignalType; + minimumHistory?: number; + period?: number | null; + fastPeriods?: number | null; + slowPeriods?: number | null; + signalPeriods?: number | null; + multiplier?: number | null; + smoothPeriods?: number | null; + stochPeriods?: number | null; + cyclePeriods?: number | null; +} + +export interface MoneyManagementRequest { + name: string; + timeframe: Timeframe; + stopLoss: number; + takeProfit: number; + leverage: number; } export interface StartBotRequest { - config?: TradingBotConfig | null; - moneyManagementName?: string | null; + config?: TradingBotConfigRequest | null; } export interface TradingBotResponse { @@ -3265,8 +3421,9 @@ export interface ClosePositionRequest { export interface UpdateBotConfigRequest { identifier: string; - config: TradingBotConfig; + config: TradingBotConfigRequest; moneyManagementName?: string | null; + moneyManagement?: MoneyManagement | null; } export interface TickerInfos { @@ -3372,6 +3529,29 @@ export interface BestAgentsResponse { totalPages?: number; } +export interface ScenarioViewModel { + name: string; + indicators: IndicatorViewModel[]; + loopbackPeriod?: number | null; + userName: string; +} + +export interface IndicatorViewModel { + name: string; + type: IndicatorType; + signalType: SignalType; + minimumHistory: number; + period?: number | null; + fastPeriods?: number | null; + slowPeriods?: number | null; + signalPeriods?: number | null; + multiplier?: number | null; + smoothPeriods?: number | null; + stochPeriods?: number | null; + cyclePeriods?: number | null; + userName: string; +} + export enum RiskLevel { Low = "Low", Medium = "Medium", diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts new file mode 100644 index 0000000..513a166 --- /dev/null +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -0,0 +1,887 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +/* tslint:disable */ +/* eslint-disable */ +// ReSharper disable InconsistentNaming + + + +export interface Account { + name: string; + exchange: TradingExchanges; + type: AccountType; + key?: string | null; + secret?: string | null; + user?: User | null; + balances?: Balance[] | null; + isPrivyWallet?: boolean; +} + +export enum TradingExchanges { + Binance = "Binance", + Kraken = "Kraken", + Ftx = "Ftx", + Evm = "Evm", + GmxV2 = "GmxV2", +} + +export enum AccountType { + Cex = "Cex", + Trader = "Trader", + Watch = "Watch", + Auth = "Auth", + Privy = "Privy", +} + +export interface User { + name?: string | null; + accounts?: Account[] | null; + agentName?: string | null; + avatarUrl?: string | null; + telegramChannel?: string | null; +} + +export interface Balance { + tokenImage?: string | null; + tokenName?: string | null; + amount?: number; + price?: number; + value?: number; + tokenAdress?: string | null; + chain?: Chain | null; +} + +export interface Chain { + id?: string | null; + rpcUrl?: string | null; + name?: string | null; + chainId?: number; +} + +export interface GmxClaimableSummary { + claimableFundingFees?: FundingFeesData | null; + claimableUiFees?: UiFeesData | null; + rebateStats?: RebateStatsData | null; +} + +export interface FundingFeesData { + totalUsdc?: number; +} + +export interface UiFeesData { + totalUsdc?: number; +} + +export interface RebateStatsData { + totalRebateUsdc?: number; + discountUsdc?: number; + rebateFactor?: number; + discountFactor?: number; +} + +export interface Backtest { + id: string; + finalPnl: number; + winRate: number; + growthPercentage: number; + hodlPercentage: number; + config: TradingBotConfig; + positions: Position[]; + signals: Signal[]; + candles: Candle[]; + startDate: Date; + endDate: Date; + statistics: PerformanceMetrics; + fees: number; + walletBalances: KeyValuePairOfDateTimeAndDecimal[]; + optimizedMoneyManagement: MoneyManagement; + user: User; + indicatorsValues: { [key in keyof typeof IndicatorType]?: IndicatorsResultBase; }; + score: number; +} + +export interface TradingBotConfig { + accountName: string; + moneyManagement: MoneyManagement; + ticker: Ticker; + timeframe: Timeframe; + isForWatchingOnly: boolean; + botTradingBalance: number; + botType: BotType; + isForBacktest: boolean; + cooldownPeriod: number; + maxLossStreak: number; + flipPosition: boolean; + name: string; + riskManagement?: RiskManagement | null; + scenario?: Scenario | null; + scenarioName?: string | null; + maxPositionTimeHours?: number | null; + closeEarlyWhenProfitable?: boolean; + flipOnlyWhenInProfit: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; +} + +export interface MoneyManagement { + name: string; + timeframe: Timeframe; + stopLoss: number; + takeProfit: number; + leverage: number; + user?: User | null; +} + +export enum Timeframe { + FiveMinutes = "FiveMinutes", + FifteenMinutes = "FifteenMinutes", + ThirtyMinutes = "ThirtyMinutes", + OneHour = "OneHour", + FourHour = "FourHour", + OneDay = "OneDay", + OneMinute = "OneMinute", +} + +export enum Ticker { + AAVE = "AAVE", + ADA = "ADA", + APE = "APE", + ALGO = "ALGO", + ARB = "ARB", + ATOM = "ATOM", + AVAX = "AVAX", + BNB = "BNB", + BTC = "BTC", + BAL = "BAL", + CHZ = "CHZ", + COMP = "COMP", + CRO = "CRO", + CRV = "CRV", + DOGE = "DOGE", + DOT = "DOT", + DYDX = "DYDX", + ENS = "ENS", + ETC = "ETC", + ETH = "ETH", + FIL = "FIL", + FLM = "FLM", + FTM = "FTM", + GALA = "GALA", + GMX = "GMX", + GRT = "GRT", + IMX = "IMX", + JASMY = "JASMY", + KSM = "KSM", + LDO = "LDO", + LINK = "LINK", + LRC = "LRC", + LTC = "LTC", + MANA = "MANA", + MATIC = "MATIC", + MKR = "MKR", + NEAR = "NEAR", + OP = "OP", + PEPE = "PEPE", + QTUM = "QTUM", + REN = "REN", + ROSE = "ROSE", + RSR = "RSR", + RUNE = "RUNE", + SAND = "SAND", + SOL = "SOL", + SRM = "SRM", + SUSHI = "SUSHI", + THETA = "THETA", + UNI = "UNI", + USDC = "USDC", + USDT = "USDT", + WIF = "WIF", + XMR = "XMR", + XRP = "XRP", + XTZ = "XTZ", + SHIB = "SHIB", + STX = "STX", + ORDI = "ORDI", + APT = "APT", + BOME = "BOME", + MEME = "MEME", + FLOKI = "FLOKI", + MEW = "MEW", + TAO = "TAO", + BONK = "BONK", + WLD = "WLD", + TBTC = "tBTC", + WBTC_b = "WBTC_b", + EIGEN = "EIGEN", + SUI = "SUI", + SEI = "SEI", + USDC_e = "USDC_e", + DAI = "DAI", + TIA = "TIA", + TRX = "TRX", + TON = "TON", + PENDLE = "PENDLE", + WstETH = "wstETH", + USDe = "USDe", + SATS = "SATS", + POL = "POL", + XLM = "XLM", + BCH = "BCH", + ICP = "ICP", + RENDER = "RENDER", + INJ = "INJ", + TRUMP = "TRUMP", + MELANIA = "MELANIA", + ENA = "ENA", + FARTCOIN = "FARTCOIN", + AI16Z = "AI16Z", + ANIME = "ANIME", + BERA = "BERA", + VIRTUAL = "VIRTUAL", + PENGU = "PENGU", + ONDO = "ONDO", + FET = "FET", + AIXBT = "AIXBT", + CAKE = "CAKE", + S = "S", + JUP = "JUP", + HYPE = "HYPE", + OM = "OM", + DOLO = "DOLO", + Unknown = "Unknown", +} + +export enum BotType { + SimpleBot = "SimpleBot", + ScalpingBot = "ScalpingBot", + FlippingBot = "FlippingBot", +} + +export interface RiskManagement { + adverseProbabilityThreshold: number; + favorableProbabilityThreshold: number; + riskAversion: number; + kellyMinimumThreshold: number; + kellyMaximumCap: number; + maxLiquidationProbability: number; + signalValidationTimeHorizonHours: number; + positionMonitoringTimeHorizonHours: number; + positionWarningThreshold: number; + positionAutoCloseThreshold: number; + kellyFractionalMultiplier: number; + riskTolerance: RiskToleranceLevel; + useExpectedUtility: boolean; + useKellyCriterion: boolean; +} + +export enum RiskToleranceLevel { + Conservative = "Conservative", + Moderate = "Moderate", + Aggressive = "Aggressive", +} + +export interface Scenario { + name?: string | null; + indicators?: Indicator[] | null; + loopbackPeriod?: number | null; + user?: User | null; +} + +export interface Indicator { + name?: string | null; + type?: IndicatorType; + signalType?: SignalType; + minimumHistory?: number; + period?: number | null; + fastPeriods?: number | null; + slowPeriods?: number | null; + signalPeriods?: number | null; + multiplier?: number | null; + smoothPeriods?: number | null; + stochPeriods?: number | null; + cyclePeriods?: number | null; + user?: User | null; +} + +export enum IndicatorType { + RsiDivergence = "RsiDivergence", + RsiDivergenceConfirm = "RsiDivergenceConfirm", + MacdCross = "MacdCross", + EmaCross = "EmaCross", + ThreeWhiteSoldiers = "ThreeWhiteSoldiers", + SuperTrend = "SuperTrend", + ChandelierExit = "ChandelierExit", + EmaTrend = "EmaTrend", + Composite = "Composite", + StochRsiTrend = "StochRsiTrend", + Stc = "Stc", + StDev = "StDev", + LaggingStc = "LaggingStc", + SuperTrendCrossEma = "SuperTrendCrossEma", + DualEmaCross = "DualEmaCross", +} + +export enum SignalType { + Signal = "Signal", + Trend = "Trend", + Context = "Context", +} + +export interface Position { + accountName: string; + date: Date; + originDirection: TradeDirection; + ticker: Ticker; + moneyManagement: MoneyManagement; + open: Trade; + stopLoss: Trade; + takeProfit1: Trade; + takeProfit2?: Trade | null; + profitAndLoss?: ProfitAndLoss | null; + status: PositionStatus; + signalIdentifier?: string | null; + identifier: string; + initiator: PositionInitiator; + user: User; +} + +export enum TradeDirection { + None = "None", + Short = "Short", + Long = "Long", +} + +export interface Trade { + fee?: number; + date: Date; + direction: TradeDirection; + status: TradeStatus; + tradeType: TradeType; + ticker: Ticker; + quantity: number; + price: number; + leverage?: number; + exchangeOrderId: string; + message?: string | null; +} + +export enum TradeStatus { + PendingOpen = "PendingOpen", + Requested = "Requested", + Cancelled = "Cancelled", + Filled = "Filled", +} + +export enum TradeType { + Limit = "Limit", + Market = "Market", + StopMarket = "StopMarket", + StopLimit = "StopLimit", + StopLoss = "StopLoss", + TakeProfit = "TakeProfit", + StopLossProfit = "StopLossProfit", + StopLossProfitLimit = "StopLossProfitLimit", + StopLossLimit = "StopLossLimit", + TakeProfitLimit = "TakeProfitLimit", + TrailingStop = "TrailingStop", + TrailingStopLimit = "TrailingStopLimit", + StopLossAndLimit = "StopLossAndLimit", + SettlePosition = "SettlePosition", +} + +export interface ProfitAndLoss { + realized?: number; + net?: number; + averageOpenPrice?: number; +} + +export enum PositionStatus { + New = "New", + Canceled = "Canceled", + Rejected = "Rejected", + Updating = "Updating", + PartiallyFilled = "PartiallyFilled", + Filled = "Filled", + Flipped = "Flipped", + Finished = "Finished", +} + +export enum PositionInitiator { + PaperTrading = "PaperTrading", + Bot = "Bot", + User = "User", + CopyTrading = "CopyTrading", +} + +export interface ValueObject { +} + +export interface Signal extends ValueObject { + status: SignalStatus; + direction: TradeDirection; + confidence: Confidence; + timeframe: Timeframe; + date: Date; + candle: Candle; + identifier: string; + ticker: Ticker; + exchange: TradingExchanges; + indicatorType: IndicatorType; + signalType: SignalType; + user?: User | null; + indicatorName: string; +} + +export enum SignalStatus { + WaitingForPosition = "WaitingForPosition", + PositionOpen = "PositionOpen", + Expired = "Expired", +} + +export enum Confidence { + Low = "Low", + Medium = "Medium", + High = "High", + None = "None", +} + +export interface Candle { + exchange: TradingExchanges; + ticker: string; + openTime: Date; + date: Date; + open: number; + close: number; + volume?: number; + high: number; + low: number; + baseVolume?: number; + quoteVolume?: number; + tradeCount?: number; + takerBuyBaseVolume?: number; + takerBuyQuoteVolume?: number; + timeframe: Timeframe; +} + +export interface PerformanceMetrics { + count?: number; + sharpeRatio?: number; + maxDrawdown?: number; + maxDrawdownPc?: number; + maxDrawdownRecoveryTime?: string; + winningTrades?: number; + loosingTrades?: number; + totalPnL?: number; +} + +export interface KeyValuePairOfDateTimeAndDecimal { + key?: Date; + value?: number; +} + +export interface IndicatorsResultBase { + ema?: EmaResult[] | null; + fastEma?: EmaResult[] | null; + slowEma?: EmaResult[] | null; + macd?: MacdResult[] | null; + rsi?: RsiResult[] | null; + stoch?: StochResult[] | null; + stochRsi?: StochRsiResult[] | null; + bollingerBands?: BollingerBandsResult[] | null; + chandelierShort?: ChandelierResult[] | null; + stc?: StcResult[] | null; + stdDev?: StdDevResult[] | null; + superTrend?: SuperTrendResult[] | null; + chandelierLong?: ChandelierResult[] | null; +} + +export interface ResultBase { + date?: Date; +} + +export interface EmaResult extends ResultBase { + ema?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface MacdResult extends ResultBase { + macd?: number | null; + signal?: number | null; + histogram?: number | null; + fastEma?: number | null; + slowEma?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface RsiResult extends ResultBase { + rsi?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */ +export interface StochResult extends ResultBase { + oscillator?: number | null; + signal?: number | null; + percentJ?: number | null; + k?: number | null; + d?: number | null; + j?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StochRsiResult extends ResultBase { + stochRsi?: number | null; + signal?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface BollingerBandsResult extends ResultBase { + sma?: number | null; + upperBand?: number | null; + lowerBand?: number | null; + percentB?: number | null; + zScore?: number | null; + width?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface ChandelierResult extends ResultBase { + chandelierExit?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StcResult extends ResultBase { + stc?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface StdDevResult extends ResultBase { + stdDev?: number | null; + mean?: number | null; + zScore?: number | null; + stdDevSma?: number | null; + "skender.Stock.Indicators.IReusableResult.Value"?: number | null; +} + +export interface SuperTrendResult extends ResultBase { + superTrend?: number | null; + upperBand?: number | null; + lowerBand?: number | null; +} + +export interface RunBacktestRequest { + config?: TradingBotConfigRequest | null; + startDate?: Date; + endDate?: Date; + balance?: number; + watchOnly?: boolean; + save?: boolean; +} + +export interface TradingBotConfigRequest { + accountName: string; + ticker: Ticker; + timeframe: Timeframe; + isForWatchingOnly: boolean; + botTradingBalance: number; + botType: BotType; + name: string; + cooldownPeriod: number; + maxLossStreak: number; + scenario?: ScenarioRequest | null; + scenarioName?: string | null; + moneyManagementName?: string | null; + moneyManagement?: MoneyManagementRequest | null; + maxPositionTimeHours?: number | null; + closeEarlyWhenProfitable?: boolean; + flipOnlyWhenInProfit?: boolean; + useSynthApi?: boolean; + useForPositionSizing?: boolean; + useForSignalFiltering?: boolean; + useForDynamicStopLoss?: boolean; +} + +export interface ScenarioRequest { + name: string; + indicators: IndicatorRequest[]; + loopbackPeriod?: number | null; +} + +export interface IndicatorRequest { + name: string; + type: IndicatorType; + signalType: SignalType; + minimumHistory?: number; + period?: number | null; + fastPeriods?: number | null; + slowPeriods?: number | null; + signalPeriods?: number | null; + multiplier?: number | null; + smoothPeriods?: number | null; + stochPeriods?: number | null; + cyclePeriods?: number | null; +} + +export interface MoneyManagementRequest { + name: string; + timeframe: Timeframe; + stopLoss: number; + takeProfit: number; + leverage: number; +} + +export interface StartBotRequest { + config?: TradingBotConfigRequest | null; +} + +export interface TradingBotResponse { + status: string; + signals: Signal[]; + positions: Position[]; + candles: Candle[]; + winRate: number; + profitAndLoss: number; + identifier: string; + agentName: string; + config: TradingBotConfig; +} + +export interface OpenPositionManuallyRequest { + identifier?: string | null; + direction?: TradeDirection; +} + +export interface ClosePositionRequest { + identifier?: string | null; + positionId?: string | null; +} + +export interface UpdateBotConfigRequest { + identifier: string; + config: TradingBotConfigRequest; + moneyManagementName?: string | null; + moneyManagement?: MoneyManagement | null; +} + +export interface TickerInfos { + ticker?: Ticker; + imageUrl?: string | null; +} + +export interface SpotlightOverview { + spotlights: Spotlight[]; + dateTime: Date; + identifier?: string; + scenarioCount?: number; +} + +export interface Spotlight { + scenario: Scenario; + tickerSignals: TickerSignal[]; +} + +export interface TickerSignal { + ticker: Ticker; + fiveMinutes: Signal[]; + fifteenMinutes: Signal[]; + oneHour: Signal[]; + fourHour: Signal[]; + oneDay: Signal[]; +} + +export interface StrategiesStatisticsViewModel { + totalStrategiesRunning?: number; + changeInLast24Hours?: number; +} + +export interface TopStrategiesViewModel { + topStrategies?: StrategyPerformance[] | null; +} + +export interface StrategyPerformance { + strategyName?: string | null; + pnL?: number; +} + +export interface UserStrategyDetailsViewModel { + name?: string | null; + state?: string | null; + pnL?: number; + roiPercentage?: number; + roiLast24H?: number; + runtime?: Date; + winRate?: number; + totalVolumeTraded?: number; + volumeLast24H?: number; + wins?: number; + losses?: number; + positions?: Position[] | null; + identifier?: string | null; + scenarioName?: string | null; +} + +export interface PlatformSummaryViewModel { + totalAgents?: number; + totalActiveStrategies?: number; + totalPlatformPnL?: number; + totalPlatformVolume?: number; + totalPlatformVolumeLast24h?: number; + agentSummaries?: AgentSummaryViewModel[] | null; + timeFilter?: string | null; +} + +export interface AgentSummaryViewModel { + agentName?: string | null; + totalPnL?: number; + pnLLast24h?: number; + totalROI?: number; + roiLast24h?: number; + wins?: number; + losses?: number; + averageWinRate?: number; + activeStrategiesCount?: number; + totalVolume?: number; + volumeLast24h?: number; +} + +export interface AgentBalanceHistory { + agentName?: string | null; + agentBalances?: AgentBalance[] | null; +} + +export interface AgentBalance { + agentName?: string | null; + totalValue?: number; + totalAccountUsdValue?: number; + botsAllocationUsdValue?: number; + pnL?: number; + time?: Date; +} + +export interface BestAgentsResponse { + agents?: AgentBalanceHistory[] | null; + totalCount?: number; + currentPage?: number; + pageSize?: number; + totalPages?: number; +} + +export interface ScenarioViewModel { + name: string; + indicators: IndicatorViewModel[]; + loopbackPeriod?: number | null; + userName: string; +} + +export interface IndicatorViewModel { + name: string; + type: IndicatorType; + signalType: SignalType; + minimumHistory: number; + period?: number | null; + fastPeriods?: number | null; + slowPeriods?: number | null; + signalPeriods?: number | null; + multiplier?: number | null; + smoothPeriods?: number | null; + stochPeriods?: number | null; + cyclePeriods?: number | null; + userName: string; +} + +export enum RiskLevel { + Low = "Low", + Medium = "Medium", + High = "High", + Adaptive = "Adaptive", +} + +export interface PrivyInitAddressResponse { + success?: boolean; + usdcHash?: string | null; + orderVaultHash?: string | null; + exchangeRouterHash?: string | null; + error?: string | null; +} + +export interface LoginRequest { + name: string; + address: string; + signature: string; + message: string; +} + +export interface Workflow { + name: string; + usage: WorkflowUsage; + flows: IFlow[]; + description: string; +} + +export enum WorkflowUsage { + Trading = "Trading", + Task = "Task", +} + +export interface IFlow { + id: string; + name: string; + type: FlowType; + description: string; + acceptedInputs: FlowOutput[]; + children?: IFlow[] | null; + parameters: FlowParameter[]; + parentId?: string; + output?: string | null; + outputTypes: FlowOutput[]; +} + +export enum FlowType { + RsiDivergence = "RsiDivergence", + FeedTicker = "FeedTicker", + OpenPosition = "OpenPosition", +} + +export enum FlowOutput { + Signal = "Signal", + Candles = "Candles", + Position = "Position", + MoneyManagement = "MoneyManagement", +} + +export interface FlowParameter { + value?: any | null; + name?: string | null; +} + +export interface SyntheticWorkflow { + name: string; + usage: WorkflowUsage; + description: string; + flows: SyntheticFlow[]; +} + +export interface SyntheticFlow { + id: string; + parentId?: string | null; + type: FlowType; + parameters: SyntheticFlowParameter[]; +} + +export interface SyntheticFlowParameter { + value: string; + name: string; +} + +export interface FileResponse { + data: Blob; + status: number; + fileName?: string; + headers?: { [name: string]: any }; +} \ No newline at end of file diff --git a/src/Managing.WebApp/src/global/type.ts b/src/Managing.WebApp/src/global/type.ts new file mode 100644 index 0000000..f341989 --- /dev/null +++ b/src/Managing.WebApp/src/global/type.ts @@ -0,0 +1,50 @@ +// Import types from ManagingApi +import type {Backtest, RiskManagement, TradingBotConfig} from '../generated/ManagingApi' + +// Import the existing IBacktestsFormInput from the correct file +import type {IBacktestsFormInput} from './type.tsx' + +export interface IRiskManagementInput { + adverseProbabilityThreshold: number + favorableProbabilityThreshold: number + riskAversion: number + kellyMinimumThreshold: number + kellyMaximumCap: number + maxLiquidationProbability: number + signalValidationTimeHorizonHours: number + positionMonitoringTimeHorizonHours: number + positionWarningThreshold: number + positionAutoCloseThreshold: number + kellyFractionalMultiplier: number + riskTolerance: 'Conservative' | 'Moderate' | 'Aggressive' + useExpectedUtility: boolean + useKellyCriterion: boolean +} + +export interface IUnifiedTradingConfigInput extends IBacktestsFormInput { + // Bot-specific fields + name?: string + isForWatchingOnly?: boolean + flipPosition?: boolean + + // Risk Management fields + riskManagement?: RiskManagement + useCustomRiskManagement?: boolean +} + +export interface UnifiedTradingModalProps { + showModal: boolean + closeModal: () => void + mode: 'backtest' | 'createBot' | 'updateBot' + showLoopSlider?: boolean + + // For backtests + setBacktests?: React.Dispatch> + + // For bot creation/update + backtest?: Backtest // Backtest object when creating from backtest + existingBot?: { + identifier: string + config: TradingBotConfig // TradingBotConfig from API + } +} \ No newline at end of file diff --git a/src/Managing.WebApp/src/global/type.tsx b/src/Managing.WebApp/src/global/type.tsx index 3872f55..db4eb05 100644 --- a/src/Managing.WebApp/src/global/type.tsx +++ b/src/Managing.WebApp/src/global/type.tsx @@ -10,11 +10,11 @@ import type { FlowOutput, FlowType, IFlow, - Indicator, + IndicatorViewModel, MoneyManagement, Position, RiskLevel, - Scenario, + ScenarioViewModel, Signal, Ticker, Timeframe, @@ -112,6 +112,11 @@ export type IBacktestsFormInput = { maxPositionTimeHours?: number | null flipOnlyWhenInProfit?: boolean closeEarlyWhenProfitable?: boolean + // Synth API fields + useSynthApi?: boolean + useForPositionSizing?: boolean + useForSignalFiltering?: boolean + useForDynamicStopLoss?: boolean } export type IBacktestCards = { @@ -123,7 +128,7 @@ export type IBacktestCards = { export type IFormInput = { children: React.ReactNode htmlFor: string - label: string + label: React.ReactNode inline?: boolean } @@ -177,9 +182,9 @@ export type IScenarioFormInput = { loopbackPeriod: number | undefined } export type IScenarioList = { - list: Scenario[] - indicators?: Indicator[] - setScenarios?: React.Dispatch> + list: ScenarioViewModel[] + indicators?: IndicatorViewModel[] + setScenarios?: React.Dispatch> } export type IMoneyManagementList = { diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestLoop.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestLoop.tsx index 58d87e2..cdb9125 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestLoop.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestLoop.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react' +import React, {useState} from 'react' +import 'react-toastify/dist/ReactToastify.css' -import { BacktestTable } from '../../components/organism' -import BacktestModal from '../../components/organism/Backtest/backtestModal' -import type { Backtest } from '../../generated/ManagingApi' +import UnifiedTradingModal from '../../components/organism/UnifiedTradingModal' +import type {Backtest} from '../../generated/ManagingApi' const BacktestLoop: React.FC = () => { - const [backtestingResult, setBacktestingResult] = useState([]) + const [backtestingResult, setBacktest] = useState([]) const [showModal, setShowModal] = useState(false) @@ -20,13 +20,13 @@ const BacktestLoop: React.FC = () => { return (
- -
diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestPlayground.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestPlayground.tsx index 162af52..960f312 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestPlayground.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestPlayground.tsx @@ -1,8 +1,8 @@ -import React, { useState } from 'react' +import React, {useState} from 'react' import 'react-toastify/dist/ReactToastify.css' -import { BacktestCards, BacktestModal } from '../../components/organism' -import type { Backtest } from '../../generated/ManagingApi' +import {BacktestCards, UnifiedTradingModal} from '../../components/organism' +import type {Backtest} from '../../generated/ManagingApi' const BacktestPlayground: React.FC = () => { const [backtestingResult, setBacktest] = useState([]) @@ -23,7 +23,8 @@ const BacktestPlayground: React.FC = () => { Run New Backtest - { - = ({ list }) => { const [showTradesModal, setShowTradesModal] = useState(false) const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null) const [showBotConfigModal, setShowBotConfigModal] = useState(false) - const [botConfigModalMode, setBotConfigModalMode] = useState<'create' | 'update'>('create') + const [botConfigModalMode, setBotConfigModalMode] = useState<'createBot' | 'updateBot'>('createBot') const [selectedBotForUpdate, setSelectedBotForUpdate] = useState<{ identifier: string config: any @@ -219,13 +218,13 @@ const BotList: React.FC = ({ list }) => { } function openCreateBotModal() { - setBotConfigModalMode('create') + setBotConfigModalMode('createBot') setSelectedBotForUpdate(null) setShowBotConfigModal(true) } function openUpdateBotModal(bot: TradingBotResponse) { - setBotConfigModalMode('update') + setBotConfigModalMode('updateBot') setSelectedBotForUpdate({ identifier: bot.identifier, config: bot.config @@ -339,14 +338,17 @@ const BotList: React.FC = ({ list }) => { setSelectedBotForTrades(null) }} /> - { + showModal={showBotConfigModal} + closeModal={() => { setShowBotConfigModal(false) setSelectedBotForUpdate(null) }} + existingBot={selectedBotForUpdate ? { + identifier: selectedBotForUpdate.identifier, + config: selectedBotForUpdate.config + } : undefined} /> ) diff --git a/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx b/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx index 2ba5e2e..725a45a 100644 --- a/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx +++ b/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx @@ -5,7 +5,7 @@ import 'react-toastify/dist/ReactToastify.css' import useApiUrlStore from '../../app/store/apiStore' import {Toast} from '../../components/mollecules' -import type {Indicator} from '../../generated/ManagingApi' +import type {IndicatorViewModel} from '../../generated/ManagingApi' import {IndicatorType, ScenarioClient, Timeframe,} from '../../generated/ManagingApi' import IndicatorTable from './indicatorTable' @@ -28,7 +28,7 @@ const IndicatorList: React.FC = () => { const [indicatorType, setIndicatorType] = useState( IndicatorType.RsiDivergence ) - const [indicators, setIndicators] = useState([]) + const [indicators, setIndicators] = useState([]) const [showModal, setShowModal] = useState(false) const { register, handleSubmit } = useForm() const { apiUrl } = useApiUrlStore() @@ -49,7 +49,7 @@ const IndicatorList: React.FC = () => { form.smoothPeriods, form.cyclePeriods ) - .then((indicator: Indicator) => { + .then((indicator: IndicatorViewModel) => { t.update('success', 'Indicator created') setIndicators((arr) => [...arr, indicator]) }) @@ -68,7 +68,7 @@ const IndicatorList: React.FC = () => { } useEffect(() => { - scenarioClient.scenario_GetIndicators().then((data: Indicator[]) => { + scenarioClient.scenario_GetIndicators().then((data: IndicatorViewModel[]) => { setIndicators(data) }) }, []) diff --git a/src/Managing.WebApp/src/pages/scenarioPage/indicatorTable.tsx b/src/Managing.WebApp/src/pages/scenarioPage/indicatorTable.tsx index 27471ce..fddd6af 100644 --- a/src/Managing.WebApp/src/pages/scenarioPage/indicatorTable.tsx +++ b/src/Managing.WebApp/src/pages/scenarioPage/indicatorTable.tsx @@ -3,15 +3,15 @@ import React, {useEffect, useState} from 'react' import useApiUrlStore from '../../app/store/apiStore' import {SelectColumnFilter, Table, Toast} from '../../components/mollecules' -import type {Indicator} from '../../generated/ManagingApi' +import type {IndicatorViewModel} from '../../generated/ManagingApi' import {ScenarioClient} from '../../generated/ManagingApi' interface IIndicatorList { - list: Indicator[] + list: IndicatorViewModel[] } const IndicatorTable: React.FC = ({ list }) => { - const [rows, setRows] = useState([]) + const [rows, setRows] = useState([]) const { apiUrl } = useApiUrlStore() async function deleteIndicator(name: string) { @@ -41,12 +41,6 @@ const IndicatorTable: React.FC = ({ list }) => { accessor: 'type', disableSortBy: true, }, - { - Filter: SelectColumnFilter, - Header: 'Timeframe', - accessor: 'timeframe', - disableSortBy: true, - }, { Filter: SelectColumnFilter, Header: 'Signal', diff --git a/src/Managing.WebApp/src/pages/scenarioPage/scenario.tsx b/src/Managing.WebApp/src/pages/scenarioPage/scenario.tsx index 07c7141..723194a 100644 --- a/src/Managing.WebApp/src/pages/scenarioPage/scenario.tsx +++ b/src/Managing.WebApp/src/pages/scenarioPage/scenario.tsx @@ -17,7 +17,7 @@ const tabs: TabsType = [ { Component: IndicatorList, index: 2, - label: 'Strategies', + label: 'Indicators', }, ] diff --git a/src/Managing.WebApp/src/pages/scenarioPage/scenarioList.tsx b/src/Managing.WebApp/src/pages/scenarioPage/scenarioList.tsx index 42199e7..698c2b4 100644 --- a/src/Managing.WebApp/src/pages/scenarioPage/scenarioList.tsx +++ b/src/Managing.WebApp/src/pages/scenarioPage/scenarioList.tsx @@ -4,15 +4,15 @@ import 'react-toastify/dist/ReactToastify.css' import useApiUrlStore from '../../app/store/apiStore' import {Toast} from '../../components/mollecules' import {ScenarioModal} from '../../components/organism' -import type {Indicator, Scenario} from '../../generated/ManagingApi' +import type {IndicatorViewModel, ScenarioViewModel} from '../../generated/ManagingApi' import {ScenarioClient} from '../../generated/ManagingApi' import type {IScenarioFormInput} from '../../global/type' import ScenarioTable from './scenarioTable' const ScenarioList: React.FC = () => { - const [indicators, setIndicators] = useState([]) - const [scenarios, setScenarios] = useState([]) + const [indicators, setIndicators] = useState([]) + const [scenarios, setScenarios] = useState([]) const [showModal, setShowModal] = useState(false) const { apiUrl } = useApiUrlStore() const client = new ScenarioClient({}, apiUrl) @@ -21,7 +21,7 @@ const ScenarioList: React.FC = () => { const t = new Toast('Creating scenario') await client .scenario_CreateScenario(form.name, form.loopbackPeriod, form.indicators) - .then((data: Scenario) => { + .then((data: ScenarioViewModel) => { t.update('success', 'Scenario created') setScenarios((arr) => [...arr, data]) }) diff --git a/src/Managing.WebApp/src/pages/scenarioPage/scenarioTable.tsx b/src/Managing.WebApp/src/pages/scenarioPage/scenarioTable.tsx index e9b8f87..6aa1b4f 100644 --- a/src/Managing.WebApp/src/pages/scenarioPage/scenarioTable.tsx +++ b/src/Managing.WebApp/src/pages/scenarioPage/scenarioTable.tsx @@ -4,14 +4,14 @@ import React, {useEffect, useState} from 'react' import useApiUrlStore from '../../app/store/apiStore' import {Table, Toast} from '../../components/mollecules' import {ScenarioModal} from '../../components/organism' -import type {Indicator, Scenario} from '../../generated/ManagingApi' +import type {IndicatorViewModel, ScenarioViewModel} from '../../generated/ManagingApi' import {ScenarioClient} from '../../generated/ManagingApi' import type {IScenarioFormInput, IScenarioList} from '../../global/type' const ScenarioTable: React.FC = ({ list, indicators = [], setScenarios }) => { - const [rows, setRows] = useState([]) + const [rows, setRows] = useState([]) const [showUpdateModal, setShowUpdateModal] = useState(false) - const [selectedScenario, setSelectedScenario] = useState(null) + const [selectedScenario, setSelectedScenario] = useState(null) const { apiUrl } = useApiUrlStore() const client = new ScenarioClient({}, apiUrl) @@ -53,7 +53,7 @@ const ScenarioTable: React.FC = ({ list, indicators = [], setScen }) } - function openUpdateModal(scenario: Scenario) { + function openUpdateModal(scenario: ScenarioViewModel) { setSelectedScenario(scenario) setShowUpdateModal(true) } @@ -77,7 +77,7 @@ const ScenarioTable: React.FC = ({ list, indicators = [], setScen { Cell: ({ cell }: any) => ( <> - {cell.row.values.indicators.map((indicator: Indicator) => ( + {cell.row.values.indicators.map((indicator: IndicatorViewModel) => (
= ({ list, isFetching }) => { } } + async function copyToClipboard(text: string) { + const t = new Toast('Copying to clipboard...') + try { + await navigator.clipboard.writeText(text) + t.update('success', 'Address copied to clipboard!') + } catch (err) { + t.update('error', 'Failed to copy to clipboard') + } + } + const columns = useMemo( () => [ { @@ -97,9 +107,18 @@ const AccountTable: React.FC = ({ list, isFetching }) => { { Cell: ({ cell }: any) => ( <> -
- {cell.row.values.key.substring(0, 6)}... - {cell.row.values.key.slice(-4)} +
+
+ {cell.row.values.key.substring(0, 6)}... + {cell.row.values.key.slice(-4)} +
+
),