Add synthApi (#27)

* Add synthApi

* Put confidence for Synth proba

* Update the code

* Update readme

* Fix bootstraping

* fix github build

* Update the endpoints for scenario

* Add scenario and update backtest modal

* Update bot modal

* Update interfaces for synth

* add synth to backtest

* Add Kelly criterion and better signal

* Update signal confidence

* update doc

* save leaderboard and prediction

* Update nswag to generate ApiClient in the correct path

* Unify the trading modal

* Save miner and prediction

* Update messaging and block new signal until position not close when flipping off

* Rename strategies to indicators

* Update doc

* Update chart + add signal name

* Fix signal direction

* Update docker webui

* remove crypto npm

* Clean
This commit is contained in:
Oda
2025-07-03 00:13:42 +07:00
committed by GitHub
parent 453806356d
commit a547c4a040
103 changed files with 9916 additions and 810 deletions

View File

@@ -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.

View File

@@ -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

355
README-API.md Normal file
View File

@@ -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

386
README.md
View File

@@ -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

494
SYNTH_API_INTEGRATION.md Normal file
View File

@@ -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

View File

@@ -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
};
}
}
/// <summary>
@@ -216,9 +267,9 @@ public class BacktestController : BaseController
public class RunBacktestRequest
{
/// <summary>
/// The trading bot configuration to use for the backtest
/// The trading bot configuration request to use for the backtest
/// </summary>
public TradingBotConfig Config { get; set; }
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// The start date for the backtest
@@ -244,14 +295,4 @@ public class RunBacktestRequest
/// Whether to save the backtest results
/// </summary>
public bool Save { get; set; } = false;
/// <summary>
/// The name of the money management to use (optional if MoneyManagement is provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// The money management details (optional if MoneyManagementName is provided)
/// </summary>
public MoneyManagement? MoneyManagement { get; set; }
}

View File

@@ -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");
}
}

View File

@@ -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
}
}
/// <summary>
/// Stops a bot specified by type and name.
/// </summary>
@@ -643,20 +696,12 @@ 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");
@@ -666,8 +711,21 @@ public class BotController : BaseController
{
return Forbid("You don't have permission to use this money management");
}
}
else if (request.MoneyManagement != null)
{
// Use provided money management object
moneyManagement = request.MoneyManagement;
// Format percentage values if using custom money management
moneyManagement.FormatPercentage();
request.Config.MoneyManagement = moneyManagement;
// 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,8 +734,66 @@ 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)
{
@@ -696,7 +812,7 @@ public class BotController : BaseController
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
};
}
}
/// <summary>
@@ -745,35 +873,7 @@ public class ClosePositionRequest
public class StartBotRequest
{
/// <summary>
/// The trading bot configuration
/// The trading bot configuration request with primary properties
/// </summary>
public TradingBotConfig Config { get; set; }
/// <summary>
/// Optional money management name (if not included in Config.MoneyManagement)
/// </summary>
public string? MoneyManagementName { get; set; }
}
/// <summary>
/// Request model for updating bot configuration
/// </summary>
public class UpdateBotConfigRequest
{
/// <summary>
/// The unique identifier of the bot to update
/// </summary>
[Required]
public string Identifier { get; set; }
/// <summary>
/// The new trading bot configuration
/// </summary>
[Required]
public TradingBotConfig Config { get; set; }
/// <summary>
/// Optional: Money management name to load if Config.MoneyManagement is null
/// </summary>
public string? MoneyManagementName { get; set; }
public TradingBotConfigRequest Config { get; set; }
}

View File

@@ -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
/// </summary>
/// <returns>A list of scenarios.</returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<Scenario>>> GetScenarios()
public async Task<ActionResult<IEnumerable<ScenarioViewModel>>> GetScenarios()
{
var user = await GetUser();
return Ok(_scenarioService.GetScenariosByUser(user));
var scenarios = _scenarioService.GetScenariosByUser(user);
var scenarioViewModels = scenarios.Select(MapToScenarioViewModel);
return Ok(scenarioViewModels);
}
/// <summary>
@@ -52,11 +55,13 @@ public class ScenarioController : BaseController
/// <param name="strategies">A list of strategy names to include in the scenario.</param>
/// <returns>The created scenario.</returns>
[HttpPost]
public async Task<ActionResult<Scenario>> CreateScenario(string name, List<string> strategies,
public async Task<ActionResult<ScenarioViewModel>> CreateScenario(string name, List<string> 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);
}
/// <summary>
@@ -85,10 +90,12 @@ public class ScenarioController : BaseController
/// <returns>A list of strategies.</returns>
[HttpGet]
[Route("indicator")]
public async Task<ActionResult<IEnumerable<Indicator>>> GetIndicators()
public async Task<ActionResult<IEnumerable<IndicatorViewModel>>> GetIndicators()
{
var user = await GetUser();
return Ok(_scenarioService.GetIndicatorsByUser(user));
var indicators = _scenarioService.GetIndicatorsByUser(user);
var indicatorViewModels = indicators.Select(MapToIndicatorViewModel);
return Ok(indicatorViewModels);
}
/// <summary>
@@ -107,7 +114,7 @@ public class ScenarioController : BaseController
/// <returns>The created indicator.</returns>
[HttpPost]
[Route("indicator")]
public async Task<ActionResult<Indicator>> CreateIndicator(
public async Task<ActionResult<IndicatorViewModel>> 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);
}
/// <summary>
@@ -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<IndicatorViewModel>()
};
}
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
};
}
}

View File

@@ -0,0 +1,73 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for indicator configuration without user information
/// </summary>
public class IndicatorRequest
{
/// <summary>
/// The name of the indicator
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// The type of indicator
/// </summary>
[Required]
public IndicatorType Type { get; set; }
/// <summary>
/// The signal type for this indicator
/// </summary>
[Required]
public SignalType SignalType { get; set; }
/// <summary>
/// Minimum history required for this indicator
/// </summary>
public int MinimumHistory { get; set; }
/// <summary>
/// Period parameter for the indicator
/// </summary>
public int? Period { get; set; }
/// <summary>
/// Fast periods parameter for indicators like MACD
/// </summary>
public int? FastPeriods { get; set; }
/// <summary>
/// Slow periods parameter for indicators like MACD
/// </summary>
public int? SlowPeriods { get; set; }
/// <summary>
/// Signal periods parameter for indicators like MACD
/// </summary>
public int? SignalPeriods { get; set; }
/// <summary>
/// Multiplier parameter for indicators like SuperTrend
/// </summary>
public double? Multiplier { get; set; }
/// <summary>
/// Smooth periods parameter
/// </summary>
public int? SmoothPeriods { get; set; }
/// <summary>
/// Stochastic periods parameter
/// </summary>
public int? StochPeriods { get; set; }
/// <summary>
/// Cycle periods parameter
/// </summary>
public int? CyclePeriods { get; set; }
}

View File

@@ -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; }
}

View File

@@ -1,15 +1,49 @@
using static Managing.Common.Enums;
using Managing.Domain.MoneyManagements;
namespace Managing.Api.Models.Requests
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for running a backtest
/// </summary>
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; }
}
/// <summary>
/// The trading bot configuration request to use for the backtest
/// </summary>
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// The start date for the backtest
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
/// The end date for the backtest
/// </summary>
public DateTime EndDate { get; set; }
/// <summary>
/// The starting balance for the backtest
/// </summary>
public decimal Balance { get; set; }
/// <summary>
/// Whether to only watch the backtest without executing trades
/// </summary>
public bool WatchOnly { get; set; } = false;
/// <summary>
/// Whether to save the backtest results
/// </summary>
public bool Save { get; set; } = false;
/// <summary>
/// The name of the money management to use (optional if MoneyManagement is provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// The money management details (optional if MoneyManagementName is provided)
/// </summary>
public MoneyManagement? MoneyManagement { get; set; }
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for scenario configuration without user information
/// </summary>
public class ScenarioRequest
{
/// <summary>
/// The name of the scenario
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// List of indicator configurations for this scenario
/// </summary>
[Required]
public List<IndicatorRequest> Indicators { get; set; } = new();
/// <summary>
/// The loopback period for the scenario
/// </summary>
public int? LoopbackPeriod { get; set; }
}

View File

@@ -1,31 +1,16 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests
{
/// <summary>
/// Request model for starting a bot
/// </summary>
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; }
/// <summary>
/// Initial trading balance in USD for the bot
/// The trading bot configuration request with primary properties
/// </summary>
[Required]
[Range(10.00, double.MaxValue, ErrorMessage = "Initial trading balance must be greater than ten")]
public decimal InitialTradingBalance { get; set; }
/// <summary>
/// Cooldown period in minutes between trades
/// </summary>
[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; }
}
}

View File

@@ -0,0 +1,119 @@
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Simplified trading bot configuration request with only primary properties
/// </summary>
public class TradingBotConfigRequest
{
/// <summary>
/// The account name to use for trading
/// </summary>
[Required]
public string AccountName { get; set; }
/// <summary>
/// The ticker/symbol to trade
/// </summary>
[Required]
public Ticker Ticker { get; set; }
/// <summary>
/// The timeframe for trading decisions
/// </summary>
[Required]
public Timeframe Timeframe { get; set; }
/// <summary>
/// Whether this bot is for watching only (no actual trading)
/// </summary>
[Required]
public bool IsForWatchingOnly { get; set; }
/// <summary>
/// The initial trading balance for the bot
/// </summary>
[Required]
public decimal BotTradingBalance { get; set; }
/// <summary>
/// The type of bot (SimpleBot, ScalpingBot, FlippingBot)
/// </summary>
[Required]
public BotType BotType { get; set; }
/// <summary>
/// The name/identifier for this bot
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// Cooldown period between trades (in candles)
/// </summary>
[Required]
public int CooldownPeriod { get; set; }
/// <summary>
/// Maximum consecutive losses before stopping the bot
/// </summary>
[Required]
public int MaxLossStreak { get; set; }
/// <summary>
/// The scenario configuration (takes precedence over ScenarioName)
/// </summary>
public ScenarioRequest? Scenario { get; set; }
/// <summary>
/// The scenario name to load from database (only used when Scenario is not provided)
/// </summary>
public string? ScenarioName { get; set; }
/// <summary>
/// The money management name to load from database (only used when MoneyManagement is not provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// The money management object to use for the bot
/// </summary>
public MoneyManagementRequest? MoneyManagement { get; set; }
/// <summary>
/// Maximum time in hours that a position can remain open before being automatically closed
/// </summary>
public decimal? MaxPositionTimeHours { get; set; }
/// <summary>
/// Whether to close positions early when they become profitable
/// </summary>
public bool CloseEarlyWhenProfitable { get; set; } = false;
/// <summary>
/// Whether to only flip positions when the current position is in profit
/// </summary>
public bool FlipOnlyWhenInProfit { get; set; } = true;
/// <summary>
/// Whether to use Synth API for predictions and risk assessment
/// </summary>
public bool UseSynthApi { get; set; } = false;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments
/// </summary>
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
public bool UseForDynamicStopLoss { get; set; } = true;
}

View File

@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.MoneyManagements;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for updating bot configuration
/// </summary>
public class UpdateBotConfigRequest
{
/// <summary>
/// The unique identifier of the bot to update
/// </summary>
[Required]
public string Identifier { get; set; }
/// <summary>
/// The new trading bot configuration request
/// </summary>
[Required]
public TradingBotConfigRequest Config { get; set; }
/// <summary>
/// Optional: Money management name to load from database (if MoneyManagement object is not provided)
/// </summary>
public string? MoneyManagementName { get; set; }
/// <summary>
/// Optional: Money management object for custom configurations (takes precedence over MoneyManagementName)
/// </summary>
public MoneyManagement? MoneyManagement { get; set; }
}

View File

@@ -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;
}

View File

@@ -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<IndicatorViewModel> Indicators { get; set; } = new();
public int? LoopbackPeriod { get; set; }
[Required]
public string UserName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,59 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Abstractions.Repositories;
/// <summary>
/// Repository interface for Synth-related data operations
/// Provides MongoDB persistence for leaderboard and individual predictions data
/// </summary>
public interface ISynthRepository
{
/// <summary>
/// Gets cached leaderboard data by cache key
/// </summary>
/// <param name="cacheKey">The cache key to search for</param>
/// <returns>Cached leaderboard data if found, null otherwise</returns>
Task<SynthMinersLeaderboard?> GetLeaderboardAsync(string cacheKey);
/// <summary>
/// Saves leaderboard data to MongoDB
/// </summary>
/// <param name="leaderboard">The leaderboard data to save</param>
Task SaveLeaderboardAsync(SynthMinersLeaderboard leaderboard);
/// <summary>
/// Gets individual cached prediction data by asset, parameters, and miner UIDs
/// </summary>
/// <param name="asset">Asset symbol</param>
/// <param name="timeIncrement">Time increment in seconds</param>
/// <param name="timeLength">Time length in seconds</param>
/// <param name="minerUids">List of miner UIDs to get predictions for</param>
/// <param name="isBacktest">Whether this is backtest data</param>
/// <param name="signalDate">Signal date for backtest data</param>
/// <returns>List of cached individual predictions</returns>
Task<List<SynthPrediction>> GetIndividualPredictionsAsync(
string asset,
int timeIncrement,
int timeLength,
List<int> minerUids,
bool isBacktest,
DateTime? signalDate);
/// <summary>
/// Saves individual prediction data to MongoDB
/// </summary>
/// <param name="prediction">The individual prediction data to save</param>
Task SaveIndividualPredictionAsync(SynthPrediction prediction);
/// <summary>
/// Saves multiple individual predictions to MongoDB in batch
/// </summary>
/// <param name="predictions">The list of individual predictions to save</param>
Task SaveIndividualPredictionsAsync(List<SynthPrediction> predictions);
/// <summary>
/// Cleans up old cached data beyond the retention period
/// </summary>
/// <param name="retentionDays">Number of days to retain data</param>
Task CleanupOldDataAsync(int retentionDays = 30);
}

View File

@@ -0,0 +1,59 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Abstractions.Services;
/// <summary>
/// Interface for communicating with the Synth API
/// </summary>
public interface ISynthApiClient
{
/// <summary>
/// Fetches the current leaderboard from Synth API
/// </summary>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of miners with their rankings and stats</returns>
Task<List<MinerInfo>> GetLeaderboardAsync(SynthConfiguration config);
/// <summary>
/// Fetches historical leaderboard data from Synth API for a specific time range
/// </summary>
/// <param name="startTime">Start time for historical data (ISO 8601 format)</param>
/// <param name="endTime">End time for historical data (ISO 8601 format)</param>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of miners with their historical rankings and stats</returns>
Task<List<MinerInfo>> GetHistoricalLeaderboardAsync(DateTime startTime, DateTime endTime, SynthConfiguration config);
/// <summary>
/// Fetches latest predictions from specified miners
/// </summary>
/// <param name="minerUids">List of miner UIDs to get predictions from</param>
/// <param name="asset">Asset symbol (e.g., "BTC", "ETH")</param>
/// <param name="timeIncrement">Time interval in seconds between each prediction point</param>
/// <param name="timeLength">Total prediction time length in seconds</param>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of predictions from the specified miners</returns>
Task<List<MinerPrediction>> GetMinerPredictionsAsync(
List<int> minerUids,
string asset,
int timeIncrement,
int timeLength,
SynthConfiguration config);
/// <summary>
/// Fetches historical predictions from specified miners for a specific time point
/// </summary>
/// <param name="minerUids">List of miner UIDs to get predictions from</param>
/// <param name="asset">Asset symbol (e.g., "BTC", "ETH")</param>
/// <param name="startTime">Start time for historical predictions (when the prediction was made)</param>
/// <param name="timeIncrement">Time interval in seconds between each prediction point</param>
/// <param name="timeLength">Total prediction time length in seconds</param>
/// <param name="config">Synth configuration containing API key and settings</param>
/// <returns>List of historical predictions from the specified miners</returns>
Task<List<MinerPrediction>> GetHistoricalMinerPredictionsAsync(
List<int> minerUids,
string asset,
DateTime startTime,
int timeIncrement,
int timeLength,
SynthConfiguration config);
}

View File

@@ -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;
/// <summary>
/// Service interface for Synth prediction business logic and probability calculations
/// </summary>
public interface ISynthPredictionService
{
/// <summary>
/// Calculates the probability of price reaching a target within a specified time horizon
/// </summary>
/// <param name="asset">Asset symbol (e.g., "BTC", "ETH")</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="targetPrice">Target price to reach</param>
/// <param name="timeHorizonSeconds">Time horizon in seconds</param>
/// <param name="isLongPosition">True for long positions (liquidation when price drops), false for short positions (liquidation when price rises)</param>
/// <param name="config">Synth configuration for this operation</param>
/// <returns>Probability as a decimal between 0.0 and 1.0</returns>
Task<decimal> GetProbabilityOfTargetPriceAsync(
string asset,
decimal currentPrice,
decimal targetPrice,
int timeHorizonSeconds,
bool isLongPosition,
SynthConfiguration config);
/// <summary>
/// Gets probabilities for multiple price thresholds at once
/// </summary>
/// <param name="asset">Asset symbol</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="priceThresholds">Dictionary of threshold names to prices</param>
/// <param name="timeHorizonSeconds">Time horizon in seconds</param>
/// <param name="isLongPosition">True for long positions, false for short positions</param>
/// <param name="config">Synth configuration for this operation</param>
/// <param name="isBacktest">Parameter for backtest</param>
/// <param name="signalDate">Signal date</param>
/// <returns>Dictionary of threshold names to probabilities</returns>
Task<Dictionary<string, decimal>> GetMultipleThresholdProbabilitiesAsync(
string asset,
decimal currentPrice,
Dictionary<string, decimal> priceThresholds,
int timeHorizonSeconds,
bool isLongPosition,
SynthConfiguration config,
bool isBacktest,
DateTime signalDate);
/// <summary>
/// Clears cached predictions (useful for testing or forced refresh)
/// </summary>
void ClearCache();
/// <summary>
/// Clears cached predictions from MongoDB asynchronously
/// </summary>
Task ClearCacheAsync();
/// <summary>
/// Validates a trading signal using Synth predictions to check for adverse price movements
/// </summary>
/// <param name="signal">The trading signal containing ticker, direction, candle data, and other context</param>
/// <param name="currentPrice">Current market price (required)</param>
/// <param name="botConfig">Bot configuration with Synth settings</param>
/// <param name="isBacktest">Whether this is a backtest</param>
/// <param name="customThresholds">Custom probability thresholds for decision-making. If null, uses default thresholds.</param>
/// <returns>Comprehensive signal validation result including confidence, probabilities, and risk analysis</returns>
Task<SignalValidationResult> ValidateSignalAsync(Signal signal, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest, Dictionary<string, decimal> customThresholds = null);
/// <summary>
/// Performs risk assessment before opening a position
/// </summary>
/// <param name="ticker">Trading ticker</param>
/// <param name="direction">Position direction</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="botConfig">Bot configuration with Synth settings</param>
/// <param name="isBacktest">Whether this is a backtest</param>
/// <returns>True if position should be allowed, false if blocked</returns>
Task<bool> AssessPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest);
/// <summary>
/// Monitors liquidation risk for an open position
/// </summary>
/// <param name="ticker">Trading ticker</param>
/// <param name="direction">Position direction</param>
/// <param name="currentPrice">Current market price</param>
/// <param name="liquidationPrice">Position liquidation price</param>
/// <param name="positionIdentifier">Position identifier for logging</param>
/// <param name="botConfig">Bot configuration with Synth settings</param>
/// <returns>Risk assessment result</returns>
Task<SynthRiskResult> MonitorPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
/// <summary>
/// Estimates liquidation price based on money management settings
/// </summary>
/// <param name="currentPrice">Current market price</param>
/// <param name="direction">Position direction</param>
/// <param name="moneyManagement">Money management settings</param>
/// <returns>Estimated liquidation price</returns>
decimal EstimateLiquidationPrice(decimal currentPrice, TradeDirection direction, MoneyManagement moneyManagement);
}

View File

@@ -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<IEnumerable<Position>> GetBrokerPositions(Account account);
Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress);
// Synth API integration methods
Task<SignalValidationResult> ValidateSynthSignalAsync(Signal signal, decimal currentPrice,
TradingBotConfig botConfig,
bool isBacktest);
Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest);
Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig);
}

View File

@@ -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<ITradingBot> GetActiveBots();

View File

@@ -9,7 +9,7 @@ namespace Managing.Application.Abstractions
{
IEnumerable<Scenario> GetScenarios();
Scenario CreateScenario(string name, List<string> strategies, int? loopbackPeriod = 1);
IEnumerable<Indicator> GetStrategies();
IEnumerable<Indicator> GetIndicators();
bool DeleteStrategy(string name);
bool DeleteScenario(string name);
Scenario GetScenario(string name);

View File

@@ -14,7 +14,6 @@ namespace Managing.Application.Abstractions
{
TradingBotConfig Config { get; set; }
Account Account { get; set; }
HashSet<IIndicator> Indicators { get; set; }
FixedSizeQueue<Candle> OptimizedCandles { get; set; }
HashSet<Candle> Candles { get; set; }
HashSet<Signal> 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<IIndicator> indicators);
void LoadScenario(string scenarioName);
void LoadScenario(Scenario scenario);
void UpdateIndicatorsValues();

View File

@@ -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<Candle>(candles);
bot.UpdateIndicatorsValues();
_logger.LogInformation("Backtest processing completed. Calculating final results...");
var strategies = _scenarioService.GetStrategies();
var strategiesValues = GetStrategiesValues(strategies, candles);
bot.Candles = new HashSet<Candle>(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<IndicatorType, IndicatorsResultBase> AggregateValues(
Dictionary<IndicatorType, IndicatorsResultBase> strategiesValues,
Dictionary<IndicatorType, IndicatorsResultBase> indicatorsValues,
Dictionary<IndicatorType, IndicatorsResultBase> 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<IndicatorType, IndicatorsResultBase>();
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<IndicatorType, IndicatorsResultBase> GetStrategiesValues(IEnumerable<Indicator> strategies,
private Dictionary<IndicatorType, IndicatorsResultBase> GetIndicatorsValues(List<Indicator> indicators,
List<Candle> candles)
{
var strategiesValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
var fixedCandles = new FixedSizeQueue<Candle>(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)

View File

@@ -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)

View File

@@ -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<IIndicator> 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<Candle> 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);
}
@@ -1173,9 +1252,9 @@ public class TradingBot : Bot, ITradingBot
// 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
@@ -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<TradingBotBackup>(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<Signal>();
Positions = data.Positions ?? new List<Position>();
WalletBalances = data.WalletBalances ?? new Dictionary<DateTime, decimal>();
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; }
/// <summary>
/// The complete trading bot configuration
/// </summary>
public TradingBotConfig Config { get; set; }
/// <summary>
/// Runtime state: Active signals for the bot
/// </summary>
public HashSet<Signal> Signals { get; set; }
/// <summary>
/// Runtime state: Open and closed positions for the bot
/// </summary>
public List<Position> 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; }
/// <summary>
/// Runtime state: Historical wallet balances over time
/// </summary>
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public MoneyManagement MoneyManagement { get; set; }
/// <summary>
/// Runtime state: When the bot was started
/// </summary>
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; }
}

View File

@@ -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<TradingBotBackup>(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<TradingBotBackup>(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);
@@ -289,7 +277,6 @@ namespace Managing.Application.ManageBot
}
public ITradingBot CreateTradingBot(TradingBotConfig config)
{
return new TradingBot(

View File

@@ -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,
@@ -112,15 +116,15 @@ 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

View File

@@ -79,7 +79,7 @@ namespace Managing.Application.Scenarios
return _tradingService.GetScenarioByName(name);
}
public IEnumerable<Indicator> GetStrategies()
public IEnumerable<Indicator> GetIndicators()
{
return _tradingService.GetStrategies();
}

View File

@@ -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;
/// <summary>
/// Client for communicating with the Synth API
/// </summary>
public class SynthApiClient : ISynthApiClient, IDisposable
{
private readonly HttpClient _httpClient;
private readonly ILogger<SynthApiClient> _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<SynthApiClient> 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
};
}
/// <summary>
/// Configures the HTTP client with API settings
/// </summary>
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);
}
/// <summary>
/// Fetches the current leaderboard from Synth API
/// </summary>
public async Task<List<MinerInfo>> 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<MinerInfo>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
_logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from leaderboard");
return miners ?? new List<MinerInfo>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error while fetching Synth leaderboard");
return new List<MinerInfo>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Timeout while fetching Synth leaderboard");
return new List<MinerInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization error while parsing Synth leaderboard");
return new List<MinerInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching Synth leaderboard");
return new List<MinerInfo>();
}
}
/// <summary>
/// Fetches historical leaderboard data from Synth API for a specific time range
/// </summary>
public async Task<List<MinerInfo>> 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<MinerInfo>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var miners = JsonSerializer.Deserialize<List<MinerInfo>>(jsonContent, _jsonOptions);
_logger.LogInformation($"📊 **Synth API** - Retrieved {miners?.Count ?? 0} miners from historical leaderboard");
return miners ?? new List<MinerInfo>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, "Timeout while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "JSON deserialization error while parsing Synth historical leaderboard");
return new List<MinerInfo>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching Synth historical leaderboard");
return new List<MinerInfo>();
}
}
/// <summary>
/// Fetches latest predictions from specified miners
/// </summary>
public async Task<List<MinerPrediction>> GetMinerPredictionsAsync(
List<int> 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<MinerPrediction>();
}
try
{
// Build URL with proper array formatting for miner parameter
var queryParams = new List<string>
{
$"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<MinerPrediction>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(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<MinerPrediction>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"HTTP error while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"Timeout while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (JsonException ex)
{
_logger.LogError(ex, $"JSON deserialization error while parsing Synth predictions for {asset}");
return new List<MinerPrediction>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error while fetching Synth predictions for {asset}");
return new List<MinerPrediction>();
}
}
/// <summary>
/// Fetches historical predictions from specified miners for a specific time point
/// </summary>
public async Task<List<MinerPrediction>> GetHistoricalMinerPredictionsAsync(
List<int> 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<MinerPrediction>();
}
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<string>
{
$"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<MinerPrediction>();
}
var jsonContent = await response.Content.ReadAsStringAsync();
var predictions = JsonSerializer.Deserialize<List<MinerPrediction>>(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<MinerPrediction>();
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, $"HTTP error while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
_logger.LogError(ex, $"Timeout while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (JsonException ex)
{
_logger.LogError(ex, $"JSON deserialization error while parsing Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Unexpected error while fetching Synth historical predictions for {asset}");
return new List<MinerPrediction>();
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}

View File

@@ -0,0 +1,169 @@
using Managing.Domain.Synth.Models;
namespace Managing.Application.Synth;
/// <summary>
/// Helper class for creating and configuring Synth API integration
/// </summary>
public static class SynthConfigurationHelper
{
/// <summary>
/// Creates a default Synth configuration for live trading
/// </summary>
/// <returns>A configured SynthConfiguration instance</returns>
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
};
}
/// <summary>
/// Creates a conservative Synth configuration with lower risk tolerances
/// </summary>
/// <returns>A conservative SynthConfiguration instance</returns>
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
};
}
/// <summary>
/// Creates an aggressive Synth configuration with higher risk tolerances
/// </summary>
/// <returns>An aggressive SynthConfiguration instance</returns>
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
};
}
/// <summary>
/// Creates a disabled Synth configuration (bot will operate without Synth predictions)
/// </summary>
/// <returns>A disabled SynthConfiguration instance</returns>
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
};
}
/// <summary>
/// Creates a Synth configuration optimized for backtesting (disabled)
/// </summary>
/// <returns>A backtesting-optimized SynthConfiguration instance</returns>
public static SynthConfiguration CreateBacktestConfig()
{
// Synth predictions are not available for historical data, so always disabled for backtests
return CreateDisabledConfig();
}
/// <summary>
/// Validates and provides suggestions for improving a Synth configuration
/// </summary>
/// <param name="config">The configuration to validate</param>
/// <returns>List of validation messages and suggestions</returns>
public static List<string> ValidateConfiguration(SynthConfiguration config)
{
var messages = new List<string>();
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<TradingService> _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<SignalValidationResult> ValidateSynthSignalAsync(Signal signal, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest)
{
return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest);
}
public async Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
TradingBotConfig botConfig, bool isBacktest)
{
return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice,
botConfig, isBacktest);
}
public async Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction,
decimal currentPrice, decimal liquidationPrice, string positionIdentifier, TradingBotConfig botConfig)
{
return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice,
positionIdentifier, botConfig);
}
}

View File

@@ -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<IPrivyService, PrivyService>();
services.AddTransient<IWeb3ProxyService, Web3ProxyService>();
services.AddTransient<IWebhookService, WebhookService>();
services.AddTransient<ISynthPredictionService, SynthPredictionService>();
services.AddTransient<ISynthApiClient, SynthApiClient>();
return services;
}
@@ -133,6 +136,7 @@ public static class ApiBootstrap
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IWorkerRepository, WorkerRepository>();
services.AddTransient<IAgentBalanceRepository, AgentBalanceRepository>();
services.AddTransient<ISynthRepository, SynthRepository>();
// Cache
services.AddDistributedMemoryCache();

View File

@@ -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<ISettingsService, SettingsService>();
services.AddSingleton<IBacktester, Backtester>();
services.AddSingleton<IBotService, BotService>();
services.AddSingleton<ISynthPredictionService, SynthPredictionService>();
services.AddTransient<ICommandHandler<OpenPositionRequest, Position>, OpenPositionCommandHandler>();
services.AddTransient<ICommandHandler<ClosePositionCommand, Position>, ClosePositionCommandHandler>();
@@ -111,6 +113,7 @@ public static class WorkersBootstrap
services.AddTransient<IBacktestRepository, BacktestRepository>();
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IUserRepository, UserRepository>();
services.AddTransient<ISynthRepository, SynthRepository>();
// Cache
services.AddDistributedMemoryCache();
@@ -126,6 +129,7 @@ public static class WorkersBootstrap
services.AddSingleton<IBinanceSocketClient, BinanceSocketClient>();
services.AddSingleton<IKrakenSocketClient, KrakenSocketClient>();
services.AddSingleton<IPrivyService, PrivyService>();
services.AddSingleton<ISynthApiClient, SynthApiClient>();
// Web3Proxy Configuration
services.Configure<Web3ProxySettings>(configuration.GetSection("Web3Proxy"));

View File

@@ -405,4 +405,14 @@ public static class Enums
Position,
MoneyManagement
}
/// <summary>
/// Risk tolerance levels for trading strategies
/// </summary>
public enum RiskToleranceLevel
{
Conservative = 1,
Moderate = 2,
Aggressive = 3
}
}

View File

@@ -1,10 +1,26 @@
using System.Text.Json.Serialization;
namespace Managing.Core.FixedSizedQueue;
public class FixedSizeQueue<T> : Queue<T>
{
private readonly int _maxSize;
/// <summary>
/// Parameterless constructor for serialization support
/// </summary>
public FixedSizeQueue() : this(500) // Default size
{
}
[JsonConstructor]
public FixedSizeQueue(int maxSize) => _maxSize = maxSize;
/// <summary>
/// Gets the maximum size of the queue (for serialization)
/// </summary>
public int MaxSize => _maxSize;
public new void Enqueue(T item)
{
while (Count >= _maxSize) Dequeue();

View File

@@ -24,7 +24,7 @@ public class Backtest
Signals = signals;
Candles = candles;
WalletBalances = new List<KeyValuePair<DateTime, decimal>>();
StrategiesValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
// 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<KeyValuePair<DateTime, decimal>> WalletBalances { get; set; }
[Required] public MoneyManagement OptimizedMoneyManagement { get; set; }
[Required] public User User { get; set; }
[Required] public Dictionary<IndicatorType, IndicatorsResultBase> StrategiesValues { get; set; }
[Required] public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; }
[Required] public double Score { get; set; }
/// <summary>

View File

@@ -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; }

View File

@@ -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; }
/// <summary>
/// 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.
/// </summary>
public RiskManagement RiskManagement { get; set; } = new();
/// <summary>
/// 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
/// </summary>
[Required]
public bool FlipOnlyWhenInProfit { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public bool UseSynthApi { get; set; } = false;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments and risk assessment
/// </summary>
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
public bool UseForDynamicStopLoss { get; set; } = true;
}

View File

@@ -0,0 +1,192 @@
using System.ComponentModel.DataAnnotations;
using Managing.Common;
namespace Managing.Domain.Risk;
/// <summary>
/// Risk management configuration for trading bots
/// Contains all configurable risk parameters for probabilistic analysis and position sizing
/// </summary>
public class RiskManagement
{
/// <summary>
/// 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%)
/// </summary>
[Range(0.05, 0.50)]
[Required]
public decimal AdverseProbabilityThreshold { get; set; } = 0.20m;
/// <summary>
/// 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%)
/// </summary>
[Range(0.10, 0.70)]
[Required]
public decimal FavorableProbabilityThreshold { get; set; } = 0.30m;
/// <summary>
/// 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)
/// </summary>
[Range(0.1, 5.0)]
[Required]
public decimal RiskAversion { get; set; } = 1.0m;
/// <summary>
/// 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%
/// </summary>
[Range(0.005, 0.10)]
[Required]
public decimal KellyMinimumThreshold { get; set; } = 0.01m;
/// <summary>
/// Maximum Kelly Criterion fraction cap for practical risk management (default: 25%)
/// Prevents over-allocation even when Kelly suggests higher percentages
/// Range: 5% to 50%
/// </summary>
[Range(0.05, 0.50)]
[Required]
public decimal KellyMaximumCap { get; set; } = 0.25m;
/// <summary>
/// Maximum acceptable liquidation probability for position risk assessment (default: 10%)
/// Positions with higher liquidation risk may be blocked or reduced
/// Range: 5% to 30%
/// </summary>
[Range(0.05, 0.30)]
[Required]
public decimal MaxLiquidationProbability { get; set; } = 0.10m;
/// <summary>
/// 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)
/// </summary>
[Range(1, 168)]
[Required]
public int SignalValidationTimeHorizonHours { get; set; } = 24;
/// <summary>
/// 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
/// </summary>
[Range(1, 48)]
[Required]
public int PositionMonitoringTimeHorizonHours { get; set; } = 6;
/// <summary>
/// Probability threshold for issuing position risk warnings (default: 20%)
/// Positions exceeding this liquidation risk will trigger warnings
/// Range: 10% to 40%
/// </summary>
[Range(0.10, 0.40)]
[Required]
public decimal PositionWarningThreshold { get; set; } = 0.20m;
/// <summary>
/// Probability threshold for automatic position closure (default: 50%)
/// Positions exceeding this liquidation risk will be automatically closed
/// Range: 30% to 80%
/// </summary>
[Range(0.30, 0.80)]
[Required]
public decimal PositionAutoCloseThreshold { get; set; } = 0.50m;
/// <summary>
/// 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
/// </summary>
[Range(0.1, 1.0)]
[Required]
public decimal KellyFractionalMultiplier { get; set; } = 1.0m;
/// <summary>
/// Risk tolerance level affecting overall risk calculations
/// </summary>
[Required]
public Enums.RiskToleranceLevel RiskTolerance { get; set; } = Enums.RiskToleranceLevel.Moderate;
/// <summary>
/// Whether to use Expected Utility Theory for decision making
/// </summary>
[Required]
public bool UseExpectedUtility { get; set; } = true;
/// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations
/// </summary>
[Required]
public bool UseKellyCriterion { get; set; } = true;
/// <summary>
/// Validates that the risk management configuration is coherent
/// </summary>
/// <returns>True if configuration is valid, false otherwise</returns>
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;
}
/// <summary>
/// Gets a preset configuration based on risk tolerance level
/// </summary>
/// <param name="tolerance">Risk tolerance level</param>
/// <returns>Configured RiskManagement instance</returns>
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)
};
}
}

View File

@@ -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<Ticker>(data.Ticker),
return ComputeSignals(strategies, latestSignalsPerIndicator, MiscExtensions.ParseEnum<Ticker>(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");
}
/// <summary>
/// Calculates the average confidence level from a list of signals
/// </summary>
private static Confidence CalculateAverageConfidence(List<Signal> 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;
}
/// <summary>

View File

@@ -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);

View File

@@ -17,7 +17,7 @@ namespace Managing.Domain.Strategies
FixedSizeQueue<Candle> Candles { get; set; }
List<Signal> Run();
IndicatorsResultBase GetStrategyValues();
IndicatorsResultBase GetIndicatorValues();
void UpdateCandles(HashSet<Candle> newCandles);
string GetName();
}

View File

@@ -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<Candle> Candles { get; set; }
[JsonIgnore] [IgnoreDataMember] public FixedSizeQueue<Candle> 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<Signal>();
}
public virtual IndicatorsResultBase GetStrategyValues()
public virtual IndicatorsResultBase GetIndicatorValues()
{
return new IndicatorsResultBase();
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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<Ticker>(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);

View File

@@ -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<Ticker>(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);

View File

@@ -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);

View File

@@ -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<Ticker>(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);

View File

@@ -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<Ticker>(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);

View File

@@ -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<Ticker>(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)
{

View File

@@ -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);

View File

@@ -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<Ticker>(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);

View File

@@ -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<Ticker>(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);

View File

@@ -52,7 +52,7 @@ namespace Managing.Domain.Strategies.Signals
}
}
public override IndicatorsResultBase GetStrategyValues()
public override IndicatorsResultBase GetIndicatorValues()
{
throw new NotImplementedException();
}

View File

@@ -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<Ticker>(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);

View File

@@ -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);

View File

@@ -0,0 +1,36 @@
using System.Text.Json.Serialization;
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents a miner on the Synth API leaderboard
/// </summary>
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; }
/// <summary>
/// Rank value from API (decimal representing the ranking score)
/// </summary>
[JsonPropertyName("rank")]
public decimal Rank { get; set; }
[JsonPropertyName("stake")]
public decimal Stake { get; set; }
[JsonPropertyName("updated_at")]
public string UpdatedAt { get; set; }
}

View File

@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents the prediction data from a single miner
/// Contains multiple simulated price paths and the miner's information
/// </summary>
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<List<PricePoint>> 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; }
/// <summary>
/// Complete miner information including rank, stake, incentive, etc.
/// This is populated after fetching predictions by mapping MinerUid to MinerInfo.NeuronUid
/// </summary>
public MinerInfo? MinerInfo { get; set; }
/// <summary>
/// Converts the StartTime string to DateTime for easier manipulation
/// </summary>
public DateTime GetStartDateTime()
{
return DateTime.TryParse(StartTime, out var dateTime) ? dateTime : DateTime.MinValue;
}
}

View File

@@ -0,0 +1,18 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents a price at a specific time within a simulated path
/// </summary>
public class PricePoint
{
public decimal Price { get; set; }
public string Time { get; set; }
/// <summary>
/// Converts the Time string to DateTime for easier manipulation
/// </summary>
public DateTime GetDateTime()
{
return DateTime.TryParse(Time, out var dateTime) ? dateTime : DateTime.MinValue;
}
}

View File

@@ -0,0 +1,403 @@
using Managing.Domain.Risk;
using static Managing.Common.Enums;
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Result of Synth signal validation containing comprehensive analysis data
/// </summary>
public class SignalValidationResult
{
/// <summary>
/// Overall confidence level of the signal based on TP vs SL probability analysis
/// </summary>
public Confidence Confidence { get; set; }
/// <summary>
/// Raw stop loss probability (0.0 to 1.0)
/// </summary>
public decimal StopLossProbability { get; set; }
/// <summary>
/// Raw take profit probability (0.0 to 1.0)
/// </summary>
public decimal TakeProfitProbability { get; set; }
/// <summary>
/// Calculated ratio of Take Profit Probability / Stop Loss Probability
/// Higher values indicate more favorable risk/reward
/// </summary>
public decimal TpSlRatio { get; set; }
/// <summary>
/// Indicates if the signal should be blocked based on risk analysis
/// True when confidence is None or adverse probability is too high
/// </summary>
public bool IsBlocked { get; set; }
/// <summary>
/// Threshold used for adverse probability evaluation
/// </summary>
public decimal AdverseProbabilityThreshold { get; set; }
/// <summary>
/// Additional context information about the validation
/// </summary>
public string ValidationContext { get; set; } = string.Empty;
/// <summary>
/// Time horizon used for the probability calculations (in seconds)
/// </summary>
public int TimeHorizonSeconds { get; set; }
/// <summary>
/// Whether custom thresholds were used in the analysis
/// </summary>
public bool UsedCustomThresholds { get; set; }
/// <summary>
/// Monetary gain if take profit is reached (positive value)
/// </summary>
public decimal TakeProfitGain { get; set; }
/// <summary>
/// Monetary loss if stop loss is hit (positive value representing loss amount)
/// </summary>
public decimal StopLossLoss { get; set; }
/// <summary>
/// Expected Monetary Value: (TP_Gain * TP_Prob) - (SL_Loss * SL_Prob)
/// Positive values indicate favorable expected outcomes
/// </summary>
public decimal ExpectedMonetaryValue { get; set; }
/// <summary>
/// Expected Utility using logarithmic utility function for risk-adjusted decision making
/// Higher values indicate more desirable risk-adjusted outcomes
/// </summary>
public decimal ExpectedUtility { get; set; }
/// <summary>
/// Risk-adjusted return ratio (Expected Utility / Risk)
/// Higher values indicate better risk-adjusted opportunities
/// </summary>
public decimal UtilityRiskRatio { get; set; }
/// <summary>
/// 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
/// </summary>
public decimal KellyFraction { get; set; }
/// <summary>
/// Capped Kelly Fraction for practical risk management (typically max 25% of capital)
/// </summary>
public decimal KellyCappedFraction { get; set; }
/// <summary>
/// Win/Loss ratio used in Kelly calculation (TakeProfitGain / StopLossLoss)
/// </summary>
public decimal WinLossRatio { get; set; }
/// <summary>
/// Kelly Criterion assessment indicating the quality of the opportunity
/// </summary>
public string KellyAssessment { get; set; } = string.Empty;
/// <summary>
/// Risk tolerance level affecting overall risk calculations
/// </summary>
public RiskToleranceLevel RiskTolerance { get; set; } = RiskToleranceLevel.Moderate;
/// <summary>
/// Whether to use Expected Utility Theory for decision making
/// </summary>
public bool UseExpectedUtility { get; set; } = true;
/// <summary>
/// Whether to use Kelly Criterion for position sizing recommendations
/// </summary>
public bool UseKellyCriterion { get; set; } = true;
/// <summary>
/// Trading balance used for utility calculations (from TradingBotConfig.BotTradingBalance)
/// Represents the actual capital allocated to this trading bot
/// </summary>
public decimal TradingBalance { get; private set; } = 10000m;
/// <summary>
/// Risk aversion parameter used for utility calculations (configured from RiskManagement)
/// </summary>
public decimal ConfiguredRiskAversion { get; private set; } = 1.0m;
/// <summary>
/// Calculates Expected Monetary Value and Expected Utility using configured risk parameters
/// </summary>
/// <param name="tradingBalance">Actual trading balance allocated to the bot</param>
/// <param name="riskConfig">Complete risk management configuration</param>
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);
}
/// <summary>
/// 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
/// </summary>
/// <returns>Expected utility value</returns>
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;
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="riskConfig">Complete risk management configuration</param>
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";
}
}
/// <summary>
/// Generates a descriptive assessment of the Kelly Criterion result
/// </summary>
/// <param name="riskConfig">Risk management configuration containing Kelly thresholds</param>
/// <returns>Human-readable Kelly assessment</returns>
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)";
}
/// <summary>
/// Gets detailed Kelly Criterion analysis including fractional betting recommendations
/// </summary>
/// <param name="totalCapital">Total available capital for position sizing</param>
/// <returns>Detailed Kelly analysis with dollar amounts</returns>
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;
}
/// <summary>
/// Calculates fractional Kelly betting for more conservative position sizing
/// </summary>
/// <param name="fraction">Fraction of Kelly to use (e.g., 0.5 for half-Kelly)</param>
/// <returns>Fractional Kelly allocation percentage</returns>
public decimal GetFractionalKelly(decimal fraction = 0.5m)
{
if (fraction < 0 || fraction > 1) fraction = 0.5m; // Default to half-Kelly
return KellyFraction * fraction;
}
/// <summary>
/// Validates if the Kelly Criterion suggests this is a profitable opportunity
/// </summary>
/// <param name="riskConfig">Risk management configuration containing Kelly thresholds</param>
/// <returns>True if Kelly fraction is above the configured threshold</returns>
public bool IsKellyFavorable(RiskManagement riskConfig)
{
return KellyFraction > riskConfig.KellyMinimumThreshold;
}
/// <summary>
/// Alternative utility calculation using square root utility (less risk-averse than logarithmic)
/// Uses the actual trading balance from the bot configuration
/// </summary>
/// <returns>Expected utility using square root function</returns>
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;
}
}
/// <summary>
/// Gets a risk assessment based on Expected Utility Theory
/// </summary>
/// <returns>Descriptive risk assessment</returns>
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";
}
/// <summary>
/// Creates a result indicating Synth is disabled
/// </summary>
public static SignalValidationResult CreateDisabledResult(Confidence originalConfidence)
{
return new SignalValidationResult
{
Confidence = originalConfidence,
IsBlocked = false,
ValidationContext = "Synth API disabled"
};
}
/// <summary>
/// Creates a result for error scenarios
/// </summary>
public static SignalValidationResult CreateErrorResult(Confidence fallbackConfidence, string errorContext)
{
return new SignalValidationResult
{
Confidence = fallbackConfidence,
IsBlocked = false,
ValidationContext = $"Error in validation: {errorContext}"
};
}
}

View File

@@ -0,0 +1,64 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Configuration settings for Synth API integration
/// </summary>
public class SynthConfiguration
{
/// <summary>
/// Whether to enable Synth API integration
/// </summary>
public bool IsEnabled { get; set; } = false;
/// <summary>
/// Number of top miners to fetch predictions from (default: 10)
/// </summary>
public int TopMinersCount { get; set; } = 10;
/// <summary>
/// Time increment in seconds for predictions (default: 300 = 5 minutes)
/// </summary>
public int TimeIncrement { get; set; } = 300;
/// <summary>
/// Default time length in seconds for predictions (default: 86400 = 24 hours)
/// </summary>
public int DefaultTimeLength { get; set; } = 86400;
/// <summary>
/// Maximum acceptable liquidation probability threshold (0.0 to 1.0)
/// If liquidation probability exceeds this, position opening may be blocked
/// </summary>
public decimal MaxLiquidationProbability { get; set; } = 0.10m; // 10%
/// <summary>
/// Cache duration for predictions in minutes (default: 5 minutes)
/// </summary>
public int PredictionCacheDurationMinutes { get; set; } = 5;
/// <summary>
/// Whether to use Synth predictions for position sizing adjustments
/// </summary>
public bool UseForPositionSizing { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for signal filtering
/// </summary>
public bool UseForSignalFiltering { get; set; } = true;
/// <summary>
/// Whether to use Synth predictions for dynamic stop-loss/take-profit adjustments
/// </summary>
public bool UseForDynamicStopLoss { get; set; } = true;
/// <summary>
/// Validates the configuration
/// </summary>
public bool IsValid()
{
return !IsEnabled || (TopMinersCount > 0 &&
TimeIncrement > 0 &&
DefaultTimeLength > 0 &&
MaxLiquidationProbability >= 0 && MaxLiquidationProbability <= 1);
}
}

View File

@@ -0,0 +1,57 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents a cached leaderboard entry for Synth miners
/// Used for MongoDB persistence to avoid repeated API calls
/// </summary>
public class SynthMinersLeaderboard
{
/// <summary>
/// Unique identifier for this leaderboard entry
/// </summary>
public string Id { get; set; }
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for this leaderboard data
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Signal date for which this leaderboard was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// List of miners in the leaderboard
/// </summary>
public List<MinerInfo> Miners { get; set; } = new();
/// <summary>
/// When this leaderboard data was created/stored
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Generates a cache key for this leaderboard entry
/// </summary>
public string GetCacheKey()
{
var key = $"{Asset}_{TimeIncrement}";
if (IsBacktest && SignalDate.HasValue)
{
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
}
return key;
}
}

View File

@@ -0,0 +1,72 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents cached prediction data from Synth miners
/// Used for MongoDB persistence to avoid repeated API calls
/// </summary>
public class SynthMinersPredictions
{
/// <summary>
/// Unique identifier for this predictions entry
/// </summary>
public string Id { get; set; }
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for these predictions
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for these predictions in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which these predictions were retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// List of miner UIDs these predictions are from
/// </summary>
public List<int> MinerUids { get; set; } = new();
/// <summary>
/// The actual prediction data from miners
/// </summary>
public List<MinerPrediction> Predictions { get; set; } = new();
/// <summary>
/// When this prediction data was fetched from the API
/// </summary>
public DateTime FetchedAt { get; set; }
/// <summary>
/// When this prediction data was created/stored
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Generates a cache key for this predictions entry
/// </summary>
public string GetCacheKey()
{
var key = $"{Asset}_{TimeIncrement}_{TimeLength}";
if (IsBacktest && SignalDate.HasValue)
{
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
}
return key;
}
}

View File

@@ -0,0 +1,67 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Represents cached prediction data from a single Synth miner
/// Used for MongoDB persistence to avoid repeated API calls and reduce document size
/// </summary>
public class SynthPrediction
{
/// <summary>
/// Unique identifier for this prediction entry
/// </summary>
public string Id { get; set; }
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Miner UID that provided this prediction
/// </summary>
public int MinerUid { get; set; }
/// <summary>
/// Time increment used for this prediction
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for this prediction in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which this prediction was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// The actual prediction data from the miner
/// </summary>
public MinerPrediction Prediction { get; set; }
/// <summary>
/// When this prediction data was created/stored
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Generates a cache key for this prediction entry
/// </summary>
public string GetCacheKey()
{
var key = $"{Asset}_{TimeIncrement}_{TimeLength}_{MinerUid}";
if (IsBacktest && SignalDate.HasValue)
{
key += $"_backtest_{SignalDate.Value:yyyy-MM-dd-HH}";
}
return key;
}
}

View File

@@ -0,0 +1,13 @@
namespace Managing.Domain.Synth.Models;
/// <summary>
/// Result of Synth risk monitoring
/// </summary>
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; }
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,42 @@
using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
namespace Managing.Infrastructure.Databases.MongoDb.Collections;
/// <summary>
/// MongoDB DTO for storing Synth miners leaderboard data
/// </summary>
[BsonCollection("SynthMinersLeaderboard")]
public class SynthMinersLeaderboardDto : Document
{
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for this leaderboard data
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Signal date for which this leaderboard was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// Serialized JSON of miners list
/// </summary>
public string MinersData { get; set; }
/// <summary>
/// Cache key for quick lookup
/// </summary>
public string CacheKey { get; set; }
}

View File

@@ -0,0 +1,57 @@
using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
namespace Managing.Infrastructure.Databases.MongoDb.Collections;
/// <summary>
/// MongoDB DTO for storing Synth miners predictions data
/// </summary>
[BsonCollection("SynthMinersPredictions")]
public class SynthMinersPredictionsDto : Document
{
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Time increment used for these predictions
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for these predictions in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which these predictions were retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// Serialized JSON of miner UIDs list
/// </summary>
public string MinerUidsData { get; set; }
/// <summary>
/// Serialized JSON of predictions data
/// </summary>
public string PredictionsData { get; set; }
/// <summary>
/// When this prediction data was fetched from the API
/// </summary>
public DateTime FetchedAt { get; set; }
/// <summary>
/// Cache key for quick lookup
/// </summary>
public string CacheKey { get; set; }
}

View File

@@ -0,0 +1,52 @@
using Managing.Infrastructure.Databases.MongoDb.Attributes;
using Managing.Infrastructure.Databases.MongoDb.Configurations;
namespace Managing.Infrastructure.Databases.MongoDb.Collections;
/// <summary>
/// MongoDB DTO for storing individual Synth miner prediction data
/// </summary>
[BsonCollection("SynthPredictions")]
public class SynthPredictionDto : Document
{
/// <summary>
/// Asset symbol (e.g., "BTC", "ETH")
/// </summary>
public string Asset { get; set; }
/// <summary>
/// Miner UID that provided this prediction
/// </summary>
public int MinerUid { get; set; }
/// <summary>
/// Time increment used for this prediction
/// </summary>
public int TimeIncrement { get; set; }
/// <summary>
/// Time length (horizon) for this prediction in seconds
/// </summary>
public int TimeLength { get; set; }
/// <summary>
/// Signal date for which this prediction was retrieved (for backtests)
/// Null for live trading data
/// </summary>
public DateTime? SignalDate { get; set; }
/// <summary>
/// Whether this is backtest data or live data
/// </summary>
public bool IsBacktest { get; set; }
/// <summary>
/// Serialized JSON of the prediction data
/// </summary>
public string PredictionData { get; set; }
/// <summary>
/// Cache key for quick lookup
/// </summary>
public string CacheKey { get; set; }
}

View File

@@ -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
/// <summary>
/// Maps domain SynthMinersLeaderboard to MongoDB DTO
/// </summary>
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()
};
}
/// <summary>
/// Maps MongoDB DTO to domain SynthMinersLeaderboard
/// </summary>
internal static SynthMinersLeaderboard Map(SynthMinersLeaderboardDto dto)
{
if (dto == null) return null;
var miners = string.IsNullOrEmpty(dto.MinersData)
? new List<MinerInfo>()
: JsonSerializer.Deserialize<List<MinerInfo>>(dto.MinersData) ?? new List<MinerInfo>();
return new SynthMinersLeaderboard
{
Id = dto.Id.ToString(),
Asset = dto.Asset,
TimeIncrement = dto.TimeIncrement,
SignalDate = dto.SignalDate,
IsBacktest = dto.IsBacktest,
Miners = miners,
CreatedAt = dto.CreatedAt
};
}
/// <summary>
/// Maps domain SynthMinersPredictions to MongoDB DTO
/// </summary>
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()
};
}
/// <summary>
/// Maps MongoDB DTO to domain SynthMinersPredictions
/// </summary>
internal static SynthMinersPredictions Map(SynthMinersPredictionsDto dto)
{
if (dto == null) return null;
var minerUids = string.IsNullOrEmpty(dto.MinerUidsData)
? new List<int>()
: JsonSerializer.Deserialize<List<int>>(dto.MinerUidsData) ?? new List<int>();
var predictions = string.IsNullOrEmpty(dto.PredictionsData)
? new List<MinerPrediction>()
: JsonSerializer.Deserialize<List<MinerPrediction>>(dto.PredictionsData) ?? new List<MinerPrediction>();
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
};
}
/// <summary>
/// Maps domain SynthPrediction to MongoDB DTO
/// </summary>
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()
};
}
/// <summary>
/// Maps MongoDB DTO to domain SynthPrediction
/// </summary>
internal static SynthPrediction Map(SynthPredictionDto dto)
{
if (dto == null) return null;
var prediction = string.IsNullOrEmpty(dto.PredictionData)
? null
: JsonSerializer.Deserialize<MinerPrediction>(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
}

View File

@@ -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;
/// <summary>
/// Repository implementation for Synth-related data operations using MongoDB
/// Provides persistence for leaderboard and individual predictions data
/// </summary>
public class SynthRepository : ISynthRepository
{
private readonly IMongoRepository<SynthMinersLeaderboardDto> _leaderboardRepository;
private readonly IMongoRepository<SynthPredictionDto> _individualPredictionsRepository;
private readonly ILogger<SynthRepository> _logger;
public SynthRepository(
IMongoRepository<SynthMinersLeaderboardDto> leaderboardRepository,
IMongoRepository<SynthPredictionDto> individualPredictionsRepository,
ILogger<SynthRepository> logger)
{
_leaderboardRepository = leaderboardRepository;
_individualPredictionsRepository = individualPredictionsRepository;
_logger = logger;
}
/// <summary>
/// Gets cached leaderboard data by cache key
/// </summary>
public async Task<SynthMinersLeaderboard?> 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;
}
}
/// <summary>
/// Saves leaderboard data to MongoDB
/// </summary>
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()}");
}
}
/// <summary>
/// Gets individual cached prediction data by asset, parameters, and miner UIDs
/// </summary>
public async Task<List<SynthPrediction>> GetIndividualPredictionsAsync(
string asset,
int timeIncrement,
int timeLength,
List<int> minerUids,
bool isBacktest,
DateTime? signalDate)
{
try
{
var results = new List<SynthPrediction>();
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<SynthPrediction>();
}
}
/// <summary>
/// Saves individual prediction data to MongoDB
/// </summary>
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}");
}
}
/// <summary>
/// Saves multiple individual predictions to MongoDB in batch
/// </summary>
public async Task SaveIndividualPredictionsAsync(List<SynthPrediction> predictions)
{
if (!predictions.Any())
{
return;
}
try
{
var saveTasks = new List<Task>();
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");
}
}
/// <summary>
/// Cleans up old cached data beyond the retention period
/// </summary>
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");
}
}
}

View File

@@ -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;
}
/// <summary>
/// Helper method to create a test signal with realistic candle data
/// </summary>
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<SynthPredictionService>();
var synthApiClient = new SynthApiClient(httpClient, new TestLogger<SynthApiClient>());
var mockSynthRepository = new Mock<ISynthRepository>();
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<SynthPredictionService>();
var synthApiClient = new SynthApiClient(httpClient, new TestLogger<SynthApiClient>());
var mockSynthRepository = new Mock<ISynthRepository>();
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<string, Dictionary<string, SignalValidationResult>>();
// 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<string, SignalValidationResult>();
// 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!");
}
/// <summary>
/// Helper method to display confidence levels with emojis
/// </summary>
/// <param name="confidence">Confidence level</param>
/// <returns>Formatted confidence display</returns>
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<T> : ILogger<T>
{
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
// Silent logger for tests - output goes to Console.WriteLine
}
}

View File

@@ -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);
File.WriteAllText(Path.Combine(targetDirectory, "ManagingApiTypes.ts"), codeTypes);

View File

@@ -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,17 +8,18 @@ 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
# 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 ./
# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts
RUN npm install --legacy-peer-deps
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

View File

@@ -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",

View File

@@ -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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
}))
}
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<BotConfigModalProps> = ({
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<BotConfigModalProps> = ({
// 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<BotConfigModalProps> = ({
<div className="form-control">
<label className="label">
<span className="label-text">Timeframe</span>
<div className="flex items-center gap-2">
<span className="label-text">Timeframe</span>
<div className="tooltip tooltip-top" data-tip="Chart timeframe for analysis. Lower timeframes (1m, 5m) for scalping, higher timeframes (1h, 4h) for swing trading">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<select
className="select select-bordered"
@@ -390,7 +432,12 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
<div className="form-control">
<label className="label">
<span className="label-text">Bot Type</span>
<div className="flex items-center gap-2">
<span className="label-text">Bot Type</span>
<div className="tooltip tooltip-top" data-tip="ScalpingBot: Quick trades based on short-term signals. FlippingBot: Position reversal strategy that flips between long/short">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<select
className="select select-bordered"
@@ -408,7 +455,12 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
<div className="form-control">
<label className="label">
<span className="label-text">Trading Balance</span>
<div className="flex items-center gap-2">
<span className="label-text">Trading Balance</span>
<div className="tooltip tooltip-top" data-tip="Amount of capital allocated for this bot's trading activities. This determines maximum position sizes">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
@@ -420,51 +472,14 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Cooldown Period (candles)</span>
</label>
<input
type="number"
className="input input-bordered"
value={formData.cooldownPeriod}
onChange={(e) => handleInputChange('cooldownPeriod', parseInt(e.target.value))}
min="1"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Max Loss Streak</span>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxLossStreak}
onChange={(e) => handleInputChange('maxLossStreak', parseInt(e.target.value))}
min="0"
/>
</div>
<div className="form-control">
<label className="label">
<span className="label-text">Max Position Time (hours)</span>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxPositionTimeHours || ''}
onChange={(e) => handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)}
min="0.1"
step="0.1"
placeholder="Optional"
/>
</div>
{/* Checkboxes */}
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Watch Only Mode</span>
<div className="flex items-center gap-2">
<span className="label-text">Watch Only Mode</span>
<div className="tooltip tooltip-top" data-tip="Bot will analyze and generate signals but won't execute actual trades. Good for testing strategies">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
@@ -473,51 +488,161 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Flip Only When In Profit</span>
<input
type="checkbox"
className="checkbox"
checked={formData.flipOnlyWhenInProfit}
onChange={(e) => handleInputChange('flipOnlyWhenInProfit', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Enable Position Flipping</span>
<input
type="checkbox"
className="checkbox"
checked={formData.flipPosition}
onChange={(e) => handleInputChange('flipPosition', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Close Early When Profitable</span>
<input
type="checkbox"
className="checkbox"
checked={formData.closeEarlyWhenProfitable}
onChange={(e) => handleInputChange('closeEarlyWhenProfitable', e.target.checked)}
disabled={!formData.maxPositionTimeHours}
/>
</label>
</div>
</div>
{/* Advanced Parameters Dropdown */}
<div className="divider">
<button
type="button"
className="btn btn-outline btn-sm normal-case"
onClick={() => setShowAdvancedParams(!showAdvancedParams)}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Advanced Parameters
<svg
className={`w-4 h-4 ml-2 transition-transform duration-200 ${showAdvancedParams ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
{showAdvancedParams && (
<div className="space-y-4 border border-primary rounded-lg p-4 bg-base-100">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label">
<div className="flex items-center gap-2">
<span className="label-text">Cooldown Period (candles)</span>
<div className="tooltip tooltip-top" data-tip="Number of candles to wait before allowing another trade after closing a position. Prevents overtrading and allows market to settle">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
className="input input-bordered"
value={formData.cooldownPeriod}
onChange={(e) => handleInputChange('cooldownPeriod', parseInt(e.target.value))}
min="1"
/>
</div>
<div className="form-control">
<label className="label">
<div className="flex items-center gap-2">
<span className="label-text">Max Loss Streak</span>
<div className="tooltip tooltip-top" data-tip="Maximum number of consecutive losing trades before stopping the bot. Set to 0 for no limit">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxLossStreak}
onChange={(e) => handleInputChange('maxLossStreak', parseInt(e.target.value))}
min="0"
/>
</div>
<div className="form-control">
<label className="label">
<div className="flex items-center gap-2">
<span className="label-text">Max Position Time (hours)</span>
<div className="tooltip tooltip-top" data-tip="Maximum time to hold a position before force closing. Leave empty to disable time-based position closure">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
</label>
<input
type="number"
className="input input-bordered"
value={formData.maxPositionTimeHours || ''}
onChange={(e) => handleInputChange('maxPositionTimeHours', e.target.value ? parseFloat(e.target.value) : null)}
min="0.1"
step="0.1"
placeholder="Optional"
/>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Flip Only When In Profit</span>
<div className="tooltip tooltip-top" data-tip="If enabled, positions will only flip when current position is profitable. Helps avoid flipping during losing streaks">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.flipOnlyWhenInProfit}
onChange={(e) => handleInputChange('flipOnlyWhenInProfit', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Enable Position Flipping</span>
<div className="tooltip tooltip-top" data-tip="Allow the bot to flip between long and short positions based on signals. More aggressive trading strategy">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.flipPosition}
onChange={(e) => handleInputChange('flipPosition', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Close Early When Profitable</span>
<div className="tooltip tooltip-top" data-tip="If enabled, positions will close early when they become profitable. Conservative approach to lock in gains">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.closeEarlyWhenProfitable}
onChange={(e) => handleInputChange('closeEarlyWhenProfitable', e.target.checked)}
disabled={!formData.maxPositionTimeHours}
/>
</label>
</div>
</div>
</div>
)}
{/* Money Management Section */}
<div className="divider">Money Management</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">Use Custom Money Management</span>
<div className="flex items-center gap-2">
<span className="label-text">Use Custom Money Management</span>
<div className="tooltip tooltip-top" data-tip="Create custom risk management settings instead of using saved presets. Allows fine-tuning stop loss, take profit, and leverage">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
@@ -592,6 +717,84 @@ const BotConfigModal: React.FC<BotConfigModalProps> = ({
</div>
)}
{/* Synth API Section */}
<div className="divider">Synth API Configuration</div>
<div className="grid grid-cols-1 gap-4">
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Enable Synth API</span>
<div className="tooltip tooltip-top" data-tip="Enable AI-powered probabilistic price forecasts and risk assessment using advanced machine learning models for enhanced trading decisions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useSynthApi}
onChange={(e) => handleSynthApiToggle(e.target.checked)}
/>
</label>
</div>
</div>
{/* Show sub-options only when Synth API is enabled */}
{formData.useSynthApi && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Use for Position Sizing</span>
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for position sizing adjustments and risk assessment. Optimizes trade size based on confidence levels">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useForPositionSizing}
onChange={(e) => handleInputChange('useForPositionSizing', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Use for Signal Filtering</span>
<div className="tooltip tooltip-top" data-tip="Use Synth predictions to filter trading signals. Only executes trades when AI confidence is high">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useForSignalFiltering}
onChange={(e) => handleInputChange('useForSignalFiltering', e.target.checked)}
/>
</label>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<div className="flex items-center gap-2">
<span className="label-text">Use for Dynamic Stop Loss</span>
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for dynamic stop-loss/take-profit adjustments. Adapts levels based on market conditions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
<input
type="checkbox"
className="checkbox"
checked={formData.useForDynamicStopLoss}
onChange={(e) => handleInputChange('useForDynamicStopLoss', e.target.checked)}
/>
</label>
</div>
</div>
)}
{/* Validation Messages */}
{formData.closeEarlyWhenProfitable && !formData.maxPositionTimeHours && (
<div className="alert alert-warning mt-4">

View File

@@ -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<BacktestModalProps> = ({
const [startDate, setStartDate] = useState<string>(defaultStartDateString);
const [endDate, setEndDate] = useState<string>(defaultEndDateString);
const { register, handleSubmit, setValue } = useForm<IBacktestsFormInput>({
const { register, handleSubmit, setValue, watch } = useForm<IBacktestsFormInput>({
defaultValues: {
startDate: defaultStartDateString,
endDate: defaultEndDateString,
@@ -51,9 +51,30 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
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<string>('')
const [selectedTimeframe, setSelectedTimeframe] = useState<Timeframe>(Timeframe.OneHour)
const [selectedLoopQuantity, setLoopQuantity] = React.useState<number>(
@@ -127,36 +148,48 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
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<BacktestModalProps> = ({
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<BacktestModalProps> = ({
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<BacktestModalProps> = ({
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<BacktestModalProps> = ({
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<BacktestModalProps> = ({
},
})}
>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
{moneyManagements.length === 0 ? (
<option value="" disabled>No money management available - create a custom one below</option>
) : (
<>
{moneyManagements.map((item) => (
<option key={item.name} value={item.name}>
{item.name}
</option>
))}
</>
)}
<option key="custom" value="custom">
Custom
{moneyManagements.length === 0 ? 'Create Custom Money Management' : 'Custom'}
</option>
</select>
</FormInput>
@@ -405,18 +455,31 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
},
})}
>
<option value="" disabled>Select a scenario</option>
{scenarios.length === 0 ? (
<option value="" disabled>No scenarios available - create a custom one below</option>
) : (
<option value="" disabled>Select a scenario</option>
)}
{scenarios.map((item) => (
<option key={item.name || 'unnamed'} value={item.name || ''}>
{item.name || 'Unnamed Scenario'}
</option>
))}
<option key="custom" value="custom">
Custom
{scenarios.length === 0 ? 'Create Custom Scenario' : 'Custom'}
</option>
</select>
</FormInput>
{showCustomScenario && (
<div className="mt-6">
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={showCustomScenario}
></CustomScenario>
</div>
)}
<FormInput label="Tickers" htmlFor="tickers">
<select
className="select select-bordered w-full"
@@ -433,17 +496,8 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</FormInput>
</div>
{showCustomScenario && (
<div className="mt-6">
<CustomScenario
onCreateScenario={setCustomScenario}
showCustomScenario={showCustomScenario}
></CustomScenario>
</div>
)}
{/* Fourth Row: Balance & Cooldown Period */}
<div className="grid grid-cols-2 gap-4">
{/* Fourth Row: Balance */}
<div className="grid grid-cols-1 gap-4">
<FormInput label="Balance" htmlFor="balance">
<input
type="number"
@@ -455,125 +509,310 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
}}
/>
</FormInput>
<FormInput label="Cooldown Period (candles)" htmlFor="cooldownPeriod">
<input
type="number"
className="input input-bordered w-full"
min="1"
step="1"
{...register('cooldownPeriod', { valueAsNumber: true })}
/>
</FormInput>
</div>
{/* Fifth Row: Max Loss Streak & Max Position Time */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Max Loss Streak" htmlFor="maxLossStreak">
<input
type="number"
className="input input-bordered w-full"
min="0"
step="1"
{...register('maxLossStreak', { valueAsNumber: true })}
/>
</FormInput>
{/* Advanced Parameters Dropdown */}
<div className="divider">
<button
type="button"
className="btn btn-outline btn-sm normal-case"
onClick={() => setShowAdvancedParams(!showAdvancedParams)}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Advanced Parameters
<svg
className={`w-4 h-4 ml-2 transition-transform duration-200 ${showAdvancedParams ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
<FormInput label="Max Position Time (hours)" htmlFor="maxPositionTimeHours">
<input
type="number"
className="input input-bordered w-full"
min="0"
step="0.5"
placeholder="Leave empty to disable"
{...register('maxPositionTimeHours', { valueAsNumber: true })}
/>
<div className="text-xs text-gray-500 mt-1">
Leave empty to disable time-based position closure
{showAdvancedParams && (
<div className="space-y-4 border border-primary rounded-lg p-4 bg-base-100">
{/* Cooldown Period & Dates */}
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Cooldown Period (candles)
<div className="tooltip tooltip-top" data-tip="Number of candles to wait before allowing another trade after closing a position. Prevents overtrading and allows market to settle">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="cooldownPeriod"
>
<input
type="number"
className="input input-bordered w-full"
min="1"
step="1"
{...register('cooldownPeriod', { valueAsNumber: true })}
/>
</FormInput>
</div>
</FormInput>
</div>
{/* Sixth Row: Flip Only When In Profit & Close Early When Profitable */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Flip Only When In Profit" htmlFor="flipOnlyWhenInProfit">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('flipOnlyWhenInProfit')}
/>
<div className="text-xs text-gray-500 mt-1">
If enabled, positions will only flip when current position is profitable
{/* Start Date & End Date */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor="startDate">
<input
type="date"
className="input input-bordered w-full"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setValue('startDate', e.target.value);
}}
/>
</FormInput>
<FormInput label="End Date" htmlFor="endDate">
<input
type="date"
className="input input-bordered w-full"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setValue('endDate', e.target.value);
}}
/>
</FormInput>
</div>
</FormInput>
<FormInput label="Close Early When Profitable" htmlFor="closeEarlyWhenProfitable">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('closeEarlyWhenProfitable')}
/>
<div className="text-xs text-gray-500 mt-1">
If enabled, positions will close early when they become profitable
{/* Loop Slider (if enabled) */}
{showLoopSlider && (
<FormInput
label={
<div className="flex items-center gap-2">
Loop
<div className="tooltip tooltip-top" data-tip="Number of optimization loops to run for money management. Each loop uses the optimized parameters from the previous iteration">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="loop"
>
<Slider
id="loopSlider"
min="1"
max="10"
value={selectedLoopQuantity.toString()}
onChange={(e) => setLoopQuantity(Number(e.target.value))}
></Slider>
</FormInput>
)}
{/* Max Loss Streak & Max Position Time */}
<div className="grid grid-cols-2 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Max Loss Streak
<div className="tooltip tooltip-top" data-tip="Maximum number of consecutive losing trades before stopping the bot. Set to 0 for no limit">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="maxLossStreak"
>
<input
type="number"
className="input input-bordered w-full"
min="0"
step="1"
{...register('maxLossStreak', { valueAsNumber: true })}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Max Position Time (hours)
<div className="tooltip tooltip-top" data-tip="Maximum time to hold a position before force closing. Leave empty to disable time-based position closure">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="maxPositionTimeHours"
>
<input
type="number"
className="input input-bordered w-full"
min="0"
step="0.5"
placeholder="Leave empty to disable"
{...register('maxPositionTimeHours', { valueAsNumber: true })}
/>
</FormInput>
</div>
</FormInput>
</div>
{/* Seventh Row: Save */}
<div className="grid grid-cols-1 gap-4">
<FormInput label="Save" htmlFor="save">
<input
type="checkbox"
className="toggle toggle-primary"
{...register('save')}
/>
</FormInput>
</div>
{/* Flip Only When In Profit & Close Early When Profitable */}
<div className="grid grid-cols-2 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Flip Only When In Profit
<div className="tooltip tooltip-top" data-tip="If enabled, positions will only flip when current position is profitable. Helps avoid flipping during losing streaks">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="flipOnlyWhenInProfit"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('flipOnlyWhenInProfit')}
/>
</FormInput>
{/* Eighth Row: Start Date & End Date */}
<div className="grid grid-cols-2 gap-4">
<FormInput label="Start Date" htmlFor="startDate">
<input
type="date"
className="input input-bordered w-full"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
setValue('startDate', e.target.value);
}}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Close Early When Profitable
<div className="tooltip tooltip-top" data-tip="If enabled, positions will close early when they become profitable. Conservative approach to lock in gains">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="closeEarlyWhenProfitable"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('closeEarlyWhenProfitable')}
/>
</FormInput>
</div>
<FormInput label="End Date" htmlFor="endDate">
<input
type="date"
className="input input-bordered w-full"
value={endDate}
onChange={(e) => {
setEndDate(e.target.value);
setValue('endDate', e.target.value);
}}
/>
</FormInput>
</div>
{/* Save Option */}
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Save Backtest Results
<div className="tooltip tooltip-top" data-tip="Save the backtest results to your account for future reference and analysis">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="save"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('save')}
/>
</FormInput>
</div>
{/* Loop Slider (if enabled) */}
{showLoopSlider && (
<FormInput label="Loop" htmlFor="loop">
<Slider
id="loopSlider"
min="1"
max="10"
value={selectedLoopQuantity.toString()}
onChange={(e) => setLoopQuantity(Number(e.target.value))}
></Slider>
</FormInput>
{/* Synth API Section */}
<div className="divider">Synth API Configuration</div>
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Enable Synth API
<div className="tooltip tooltip-top" data-tip="Enable AI-powered probabilistic price forecasts and risk assessment using advanced machine learning models for enhanced trading decisions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useSynthApi"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useSynthApi')}
/>
</FormInput>
</div>
{/* Show sub-options only when Synth API is enabled */}
{useSynthApi && (
<div className="grid grid-cols-1 gap-4">
<FormInput
label={
<div className="flex items-center gap-2">
Use for Position Sizing
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for position sizing adjustments and risk assessment. Optimizes trade size based on confidence levels">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useForPositionSizing"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useForPositionSizing')}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Use for Signal Filtering
<div className="tooltip tooltip-top" data-tip="Use Synth predictions to filter trading signals. Only executes trades when AI confidence is high">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useForSignalFiltering"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useForSignalFiltering')}
/>
</FormInput>
<FormInput
label={
<div className="flex items-center gap-2">
Use for Dynamic Stop Loss
<div className="tooltip tooltip-top" data-tip="Use Synth predictions for dynamic stop-loss/take-profit adjustments. Adapts levels based on market conditions">
<span className="badge badge-info badge-xs">i</span>
</div>
</div>
}
htmlFor="useForDynamicStopLoss"
>
<input
type="checkbox"
className="toggle toggle-primary"
{...register('useForDynamicStopLoss')}
/>
</FormInput>
</div>
)}
</div>
)}
</div>
<div className="modal-action">
<button type="submit" className="btn">
Run
<button
type="button"
className="btn btn-ghost"
onClick={closeModal}
>
Cancel
</button>
<button type="submit" className="btn btn-primary">
Run Backtest
</button>
</div>
</Modal>

View File

@@ -4,21 +4,16 @@ import {CardPosition, CardText} from '../../mollecules'
interface IBacktestRowDetailsProps {
backtest: Backtest;
optimizedMoneyManagement: {
stopLoss: number;
takeProfit: number;
};
}
const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
backtest,
optimizedMoneyManagement
backtest
}) => {
const {
candles,
positions,
walletBalances,
strategiesValues,
indicatorsValues,
signals,
statistics,
config
@@ -364,7 +359,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
candles={candles}
positions={positions}
walletBalances={walletBalances}
strategiesValues={strategiesValues}
indicatorsValues={indicatorsValues}
signals={signals}
></TradeChart>
</figure>

View File

@@ -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<IBacktestCards> = ({ list, isFetching, setBacktest
const [showBotConfigModal, setShowBotConfigModal] = useState(false)
const [selectedBacktest, setSelectedBacktest] = useState<Backtest | null>(null)
// Backtest configuration modal state
const [showBacktestConfigModal, setShowBacktestConfigModal] = useState(false)
const [selectedBacktestForRerun, setSelectedBacktestForRerun] = useState<Backtest | null>(null)
const handleOpenBotConfigModal = (backtest: Backtest) => {
setSelectedBacktest(backtest)
setShowBotConfigModal(true)
@@ -42,6 +46,16 @@ const BacktestTable: React.FC<IBacktestCards> = ({ 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<IBacktestCards> = ({ list, isFetching, setBacktest
accessor: 'id',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
<div className="tooltip" data-tip="Re-run backtest with same config">
<button
data-value={cell.row.values.name}
onClick={() => handleOpenBacktestConfigModal(cell.row.original as Backtest)}
>
<CogIcon className="text-info w-4"></CogIcon>
</button>
</div>
</>
),
Header: '',
accessor: 'rerun',
disableFilters: true,
},
{
Cell: ({ cell }: any) => (
<>
@@ -429,18 +460,29 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
renderRowSubCompontent={({ row }: any) => (
<BacktestRowDetails
backtest={row.original}
optimizedMoneyManagement={optimizedMoneyManagement}
/>
)}
/>
{/* Bot Configuration Modal */}
{selectedBacktest && (
<BotConfigModal
<UnifiedTradingModal
showModal={showBotConfigModal}
mode="create"
mode="createBot"
backtest={selectedBacktest}
onClose={handleCloseBotConfigModal}
closeModal={handleCloseBotConfigModal}
/>
)}
{/* Backtest Configuration Modal */}
{selectedBacktestForRerun && (
<UnifiedTradingModal
showModal={showBacktestConfigModal}
mode="backtest"
backtest={selectedBacktestForRerun}
closeModal={handleCloseBacktestConfigModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
)}
</>

View File

@@ -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'},

View File

@@ -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'
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="backtest"
setBacktests={setBacktests}
showLoopSlider={true} // Optional: enable loop optimization
/>
```
### 2. Create Bot Mode
```tsx
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="createBot"
/>
```
### 3. Create Bot from Backtest
```tsx
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="createBot"
backtest={selectedBacktest} // Initialize from backtest
/>
```
### 4. Update Bot Mode
```tsx
<UnifiedTradingModal
showModal={showModal}
closeModal={() => setShowModal(false)}
mode="updateBot"
existingBot={{
identifier: bot.identifier,
config: bot.config
}}
/>
```
## Migration Guide
### From BacktestModal
```tsx
// Old
<BacktestModal
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
// New
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktests}
showLoopSlider={true}
/>
```
### From BotConfigModal
```tsx
// Old
<BotConfigModal
showModal={showModal}
mode="create"
backtest={backtest}
onClose={closeModal}
/>
// New
<UnifiedTradingModal
mode="createBot"
showModal={showModal}
closeModal={closeModal}
backtest={backtest}
/>
```
## 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.

View File

@@ -0,0 +1 @@
export { default } from './UnifiedTradingModal'

View File

@@ -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'

View File

@@ -337,6 +337,45 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
backtest_Map(moneyManagementRequest: MoneyManagementRequest): Promise<MoneyManagement> {
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<MoneyManagement> {
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<MoneyManagement>(null as any);
}
backtest_Backtest(id: string): Promise<Backtest> {
let url_ = this.baseUrl + "/Backtest/{id}";
if (id === undefined || id === null)
@@ -726,6 +765,45 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<TradingBotResponse[]>(null as any);
}
bot_Map(moneyManagementRequest: MoneyManagementRequest): Promise<MoneyManagement> {
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<MoneyManagement> {
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<MoneyManagement>(null as any);
}
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
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[]> {
scenario_GetScenarios(): Promise<ScenarioViewModel[]> {
let url_ = this.baseUrl + "/Scenario";
url_ = url_.replace(/[?&]$/, "");
@@ -1450,13 +1528,13 @@ export class ScenarioClient extends AuthorizedApiBase {
});
}
protected processScenario_GetScenarios(response: Response): Promise<Scenario[]> {
protected processScenario_GetScenarios(response: Response): Promise<ScenarioViewModel[]> {
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<Scenario[]>(null as any);
return Promise.resolve<ScenarioViewModel[]>(null as any);
}
scenario_CreateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise<Scenario> {
scenario_CreateScenario(name: string | null | undefined, loopbackPeriod: number | null | undefined, strategies: string[]): Promise<ScenarioViewModel> {
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<Scenario> {
protected processScenario_CreateScenario(response: Response): Promise<ScenarioViewModel> {
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<Scenario>(null as any);
return Promise.resolve<ScenarioViewModel>(null as any);
}
scenario_DeleteScenario(name: string | null | undefined): Promise<FileResponse> {
@@ -1600,7 +1678,7 @@ export class ScenarioClient extends AuthorizedApiBase {
return Promise.resolve<FileResponse>(null as any);
}
scenario_GetIndicators(): Promise<Indicator[]> {
scenario_GetIndicators(): Promise<IndicatorViewModel[]> {
let url_ = this.baseUrl + "/Scenario/indicator";
url_ = url_.replace(/[?&]$/, "");
@@ -1618,13 +1696,13 @@ export class ScenarioClient extends AuthorizedApiBase {
});
}
protected processScenario_GetIndicators(response: Response): Promise<Indicator[]> {
protected processScenario_GetIndicators(response: Response): Promise<IndicatorViewModel[]> {
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<Indicator[]>(null as any);
return Promise.resolve<IndicatorViewModel[]>(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<Indicator> {
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<IndicatorViewModel> {
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<Indicator> {
protected processScenario_CreateIndicator(response: Response): Promise<IndicatorViewModel> {
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<Indicator>(null as any);
return Promise.resolve<IndicatorViewModel>(null as any);
}
scenario_DeleteIndicator(name: string | null | undefined): Promise<FileResponse> {
@@ -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",

View File

@@ -0,0 +1,887 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* 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 };
}

View File

@@ -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<React.SetStateAction<Backtest[]>>
// For bot creation/update
backtest?: Backtest // Backtest object when creating from backtest
existingBot?: {
identifier: string
config: TradingBotConfig // TradingBotConfig from API
}
}

View File

@@ -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<React.SetStateAction<Scenario[]>>
list: ScenarioViewModel[]
indicators?: IndicatorViewModel[]
setScenarios?: React.Dispatch<React.SetStateAction<ScenarioViewModel[]>>
}
export type IMoneyManagementList = {

View File

@@ -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<Backtest[]>([])
const [backtestingResult, setBacktest] = useState<Backtest[]>([])
const [showModal, setShowModal] = useState(false)
@@ -20,13 +20,13 @@ const BacktestLoop: React.FC = () => {
return (
<div className="container mx-auto">
<button className="btn" onClick={openModal}>
Run optimized loop
Run Backtest with Loop
</button>
<BacktestTable list={backtestingResult} isFetching={false} />
<BacktestModal
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktestingResult}
setBacktests={setBacktest}
showLoopSlider={true}
/>
</div>

View File

@@ -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<Backtest[]>([])
@@ -23,7 +23,8 @@ const BacktestPlayground: React.FC = () => {
Run New Backtest
</button>
<BacktestCards list={backtestingResult} setBacktests={setBacktest} />
<BacktestModal
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktest}

View File

@@ -6,7 +6,7 @@ import 'react-toastify/dist/ReactToastify.css'
import useApiUrlStore from '../../app/store/apiStore'
import {Loader} from '../../components/atoms'
import {Modal, Toast} from '../../components/mollecules'
import {BacktestModal, BacktestTable} from '../../components/organism'
import {BacktestTable, UnifiedTradingModal} from '../../components/organism'
import type {Backtest} from '../../generated/ManagingApi'
import {BacktestClient} from '../../generated/ManagingApi'
@@ -101,7 +101,8 @@ const BacktestScanner: React.FC = () => {
<BacktestTable list={backtestingResult} isFetching={isLoading} setBacktests={setBacktest} />
<BacktestModal
<UnifiedTradingModal
mode="backtest"
showModal={showModal}
closeModal={closeModal}
setBacktests={setBacktest}

View File

@@ -5,8 +5,7 @@ import useApiUrlStore from '../../app/store/apiStore'
import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules'
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
import TradesModal from '../../components/mollecules/TradesModal/TradesModal'
import BotConfigModal from '../../components/mollecules/BotConfigModal/BotConfigModal'
import {TradeChart} from '../../components/organism'
import {TradeChart, UnifiedTradingModal} from '../../components/organism'
import type {BotType, MoneyManagement, Position, TradingBotResponse} from '../../generated/ManagingApi'
import {BotClient} from '../../generated/ManagingApi'
import type {IBotList} from '../../global/type'
@@ -40,7 +39,7 @@ const BotList: React.FC<IBotList> = ({ 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<IBotList> = ({ 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<IBotList> = ({ list }) => {
setSelectedBotForTrades(null)
}}
/>
<BotConfigModal
showModal={showBotConfigModal}
<UnifiedTradingModal
mode={botConfigModalMode}
existingBot={selectedBotForUpdate || undefined}
onClose={() => {
showModal={showBotConfigModal}
closeModal={() => {
setShowBotConfigModal(false)
setSelectedBotForUpdate(null)
}}
existingBot={selectedBotForUpdate ? {
identifier: selectedBotForUpdate.identifier,
config: selectedBotForUpdate.config
} : undefined}
/>
</div>
)

View File

@@ -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>(
IndicatorType.RsiDivergence
)
const [indicators, setIndicators] = useState<Indicator[]>([])
const [indicators, setIndicators] = useState<IndicatorViewModel[]>([])
const [showModal, setShowModal] = useState(false)
const { register, handleSubmit } = useForm<IIndicatorFormInput>()
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)
})
}, [])

View File

@@ -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<IIndicatorList> = ({ list }) => {
const [rows, setRows] = useState<Indicator[]>([])
const [rows, setRows] = useState<IndicatorViewModel[]>([])
const { apiUrl } = useApiUrlStore()
async function deleteIndicator(name: string) {
@@ -41,12 +41,6 @@ const IndicatorTable: React.FC<IIndicatorList> = ({ list }) => {
accessor: 'type',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Timeframe',
accessor: 'timeframe',
disableSortBy: true,
},
{
Filter: SelectColumnFilter,
Header: 'Signal',

View File

@@ -17,7 +17,7 @@ const tabs: TabsType = [
{
Component: IndicatorList,
index: 2,
label: 'Strategies',
label: 'Indicators',
},
]

Some files were not shown because too many files have changed in this diff Show More