diff --git a/VERSION b/VERSION index 227cea21..38f77a65 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.0 +2.0.1 diff --git a/alembic/versions/a62763117ed4_add_fund_ledger_system.py b/alembic/versions/a62763117ed4_add_fund_ledger_system.py new file mode 100644 index 00000000..2b7728ca --- /dev/null +++ b/alembic/versions/a62763117ed4_add_fund_ledger_system.py @@ -0,0 +1,92 @@ +"""add_fund_ledger_system + +Revision ID: a62763117ed4 +Revises: 762a64d64b9d +Create Date: 2026-02-25 16:21:58.440948 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'a62763117ed4' +down_revision: Union[str, None] = '762a64d64b9d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('fund_ledger', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('trading_mode', sa.String(length=20), nullable=False), + sa.Column('transaction_type', sa.String(length=10), nullable=False), + sa.Column('category', sa.String(length=50), nullable=False), + sa.Column('amount', sa.Numeric(precision=15, scale=2), nullable=False), + sa.Column('balance_before', sa.Numeric(precision=15, scale=2), nullable=False), + sa.Column('balance_after', sa.Numeric(precision=15, scale=2), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('reference_id', sa.String(length=100), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_ledger_user_mode', 'fund_ledger', ['user_id', 'trading_mode'], unique=False) + op.create_index(op.f('ix_fund_ledger_category'), 'fund_ledger', ['category'], unique=False) + op.create_index(op.f('ix_fund_ledger_id'), 'fund_ledger', ['id'], unique=False) + op.create_index(op.f('ix_fund_ledger_reference_id'), 'fund_ledger', ['reference_id'], unique=False) + op.create_index(op.f('ix_fund_ledger_timestamp'), 'fund_ledger', ['timestamp'], unique=False) + op.create_index(op.f('ix_fund_ledger_trading_mode'), 'fund_ledger', ['trading_mode'], unique=False) + op.create_index(op.f('ix_fund_ledger_user_id'), 'fund_ledger', ['user_id'], unique=False) + op.drop_index('idx_trading_log_level_component', table_name='trading_system_logs') + op.drop_index('idx_trading_log_timestamp', table_name='trading_system_logs') + op.drop_index('ix_trading_system_logs_component', table_name='trading_system_logs') + op.drop_index('ix_trading_system_logs_id', table_name='trading_system_logs') + op.drop_index('ix_trading_system_logs_log_level', table_name='trading_system_logs') + op.drop_index('ix_trading_system_logs_timestamp', table_name='trading_system_logs') + op.drop_index('ix_trading_system_logs_trade_id', table_name='trading_system_logs') + op.drop_index('ix_trading_system_logs_user_id', table_name='trading_system_logs') + op.drop_table('trading_system_logs') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('trading_system_logs', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('log_level', sa.VARCHAR(length=20), autoincrement=False, nullable=False), + sa.Column('component', sa.VARCHAR(length=50), autoincrement=False, nullable=False), + sa.Column('message', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('trade_id', sa.VARCHAR(length=50), autoincrement=False, nullable=True), + sa.Column('symbol', sa.VARCHAR(length=20), autoincrement=False, nullable=True), + sa.Column('latency_ms', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('function_name', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('line_number', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('stack_trace', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('additional_data', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('timestamp', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='trading_system_logs_user_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='trading_system_logs_pkey') + ) + op.create_index('ix_trading_system_logs_user_id', 'trading_system_logs', ['user_id'], unique=False) + op.create_index('ix_trading_system_logs_trade_id', 'trading_system_logs', ['trade_id'], unique=False) + op.create_index('ix_trading_system_logs_timestamp', 'trading_system_logs', ['timestamp'], unique=False) + op.create_index('ix_trading_system_logs_log_level', 'trading_system_logs', ['log_level'], unique=False) + op.create_index('ix_trading_system_logs_id', 'trading_system_logs', ['id'], unique=False) + op.create_index('ix_trading_system_logs_component', 'trading_system_logs', ['component'], unique=False) + op.create_index('idx_trading_log_timestamp', 'trading_system_logs', ['timestamp'], unique=False) + op.create_index('idx_trading_log_level_component', 'trading_system_logs', ['log_level', 'component'], unique=False) + op.drop_index(op.f('ix_fund_ledger_user_id'), table_name='fund_ledger') + op.drop_index(op.f('ix_fund_ledger_trading_mode'), table_name='fund_ledger') + op.drop_index(op.f('ix_fund_ledger_timestamp'), table_name='fund_ledger') + op.drop_index(op.f('ix_fund_ledger_reference_id'), table_name='fund_ledger') + op.drop_index(op.f('ix_fund_ledger_id'), table_name='fund_ledger') + op.drop_index(op.f('ix_fund_ledger_category'), table_name='fund_ledger') + op.drop_index('idx_ledger_user_mode', table_name='fund_ledger') + op.drop_table('fund_ledger') + # ### end Alembic commands ### diff --git a/database/models.py b/database/models.py index 5a4129a1..5b76caa3 100644 --- a/database/models.py +++ b/database/models.py @@ -1485,6 +1485,42 @@ class TradingAuditTrail(Base): ) +class FundLedger(Base): + """ + Transaction ledger for tracking all fund movements (Credits/Debits) + Behaves like a real bank/brokerage statement. + """ + __tablename__ = "fund_ledger" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False) + + # paper or live + trading_mode = Column(String(20), nullable=False, index=True) + + # CREDIT (Money coming in) or DEBIT (Money going out) + transaction_type = Column(String(10), nullable=False) + + # FUND_ADDED, TRADE_MARGIN_BLOCKED, TRADE_MARGIN_RELEASED, PNL_SETTLEMENT, CHARGES, BROKERAGE + category = Column(String(50), nullable=False, index=True) + + amount = Column(Numeric(15, 2), nullable=False) + balance_before = Column(Numeric(15, 2), nullable=False) + balance_after = Column(Numeric(15, 2), nullable=False) + + description = Column(String(255)) + reference_id = Column(String(100), index=True) # trade_id or transaction_id + + timestamp = Column(DateTime, default=get_ist_now_naive, index=True) + + # Relationships + user = relationship("User") + + __table_args__ = ( + Index("idx_ledger_user_mode", "user_id", "trading_mode"), + ) + + # ======================= # PREMARKET CANDLE & GAP DETECTION SYSTEM # ======================= diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a08feaa7..b285bf1a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,29 @@ -# 📝 Changelog +# 📠Changelog All notable changes to this project will be documented in this file. -## [Unreleased] -- Ongoing improvements and bug fixes. +## [2.0.1] - 2026-02-25 +### Added +- **AI-Powered Development Agents:** + - Automated Issue Triager for bug analysis using Gemini. + - Automated PR Reviewer for code quality, complexity (radon), and risk checks. + - Automated Backtesting for strategy performance validation in pull requests. + - AI Documentation Agent for automated semantic changelogs. + - Trading Risk Guard for detecting dangerous code patterns (hardcoded secrets, unlocalized time). +- **Fund Ledger System:** + - Database-backed fund ledger tracking all deposits and withdrawals. + - Integration with `CapitalManager` for accurate balance management across brokers. + - New UI components: `AddFundsModal`, `FundStatementTable`, and `FundsTab` in profile. +- **AI Support & Telegram Integration:** + - `AISupportService` for context-aware support answering queries using project docs and trade history. + - Telegram bot for remote support and account status tracking. + +### Changed +- Refactored `execution_handler.py` and `pnl_tracker.py` to utilize the new `FundManager` for balance synchronization. +- Enhanced `AutoTradingPage` for better performance and list virtualization. +- Updated `requirements.txt` with `google-generativeai` and `PyGithub`. + +### Fixed +- Improved timezone handling for IST consistency across services. +- Corrected paper trading P&L logic for accurate balance updates. +- Refined stock selection direction logic for neutral market conditions. diff --git a/router/trading_execution_router.py b/router/trading_execution_router.py index b1242b23..ec296ae8 100644 --- a/router/trading_execution_router.py +++ b/router/trading_execution_router.py @@ -35,6 +35,7 @@ from services.trading_execution.multi_demat_capital_service import ( multi_demat_capital_service, ) +from services.trading_execution.fund_manager import fund_manager logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -1881,3 +1882,50 @@ async def get_detailed_performance( except Exception as e: logger.error(f"Error getting detailed performance: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +# ==================== FUND MANAGEMENT ENDPOINTS ==================== + +@router.get("/funds/balance") +async def get_fund_balances( + trading_mode: str = Query("paper", description="paper or live"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get available, used and total balance""" + try: + balances = fund_manager.get_balances(current_user.id, db, trading_mode) + return {"success": True, "balances": balances} + except Exception as e: + logger.error(f"Error fetching balances: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/funds/add-paper-funds") +async def add_paper_funds( + amount: float = Query(..., gt=0, description="Amount to add"), + description: str = Query("Manual top-up", description="Transaction description"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Add virtual funds to paper trading account""" + try: + result = fund_manager.add_paper_funds(current_user.id, amount, db, description) + return result + except Exception as e: + logger.error(f"Error adding funds: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/funds/statement") +async def get_fund_statement( + trading_mode: str = Query("paper", description="paper or live"), + limit: int = Query(50, description="Max entries to return"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Get transaction ledger/statement""" + try: + statement = fund_manager.get_statement(current_user.id, db, trading_mode, limit) + return {"success": True, "statement": statement} + except Exception as e: + logger.error(f"Error fetching statement: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/services/paper_trading_account.py b/services/paper_trading_account.py index cda588fc..b8ec6e1a 100644 --- a/services/paper_trading_account.py +++ b/services/paper_trading_account.py @@ -116,18 +116,8 @@ async def sync_with_db(self, user_id: int, db: Session) -> Optional[PaperAccount db_account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() if not db_account: - # Create default in DB if not exists - db_account = PaperTradingAccount( - user_id=user_id, - initial_capital=100000.0, - current_balance=100000.0, - available_margin=100000.0, - used_margin=0.0, - total_pnl=0.0 - ) - db.add(db_account) - db.commit() - db.refresh(db_account) + logger.warning(f"No paper account found in DB for user {user_id}. Fund management required.") + return None # Update in-memory account = PaperAccount( @@ -163,16 +153,7 @@ def execute_paper_trade_sync(self, user_id: int, trade_data: Dict[str, Any], db: db_acc = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() if not db_acc: - db_acc = PaperTradingAccount( - user_id=user_id, - initial_capital=100000.0, - current_balance=100000.0, - available_margin=100000.0, - used_margin=0.0, - total_pnl=0.0 - ) - db.add(db_acc) - db.flush() + raise ValueError(f"No paper trading account found for user {user_id}. Please add funds first.") invested_amount = float(trade_data['invested_amount']) entry_charges = 20.0 diff --git a/services/trading_execution/capital_manager.py b/services/trading_execution/capital_manager.py index a1dfcda4..3cba1044 100644 --- a/services/trading_execution/capital_manager.py +++ b/services/trading_execution/capital_manager.py @@ -49,7 +49,6 @@ class TradingCapitalManager: def __init__(self): """Initialize capital manager with configuration""" - self.paper_trading_capital = Decimal('100000') # 1 Lakh default for paper trading self.max_capital_per_trade_percent = Decimal('0.60') # 60% max per trade self.max_risk_per_trade_percent = Decimal('0.02') # 2% max risk per trade self.min_capital_buffer = Decimal('0.10') # Keep 10% buffer @@ -133,53 +132,16 @@ def get_available_capital( trading_mode: TradingMode = TradingMode.PAPER ) -> Decimal: """ - Get available capital for trading + Get available capital for trading using centralized Fund Manager """ if not user_id or user_id <= 0: raise ValueError("Invalid user_id provided") try: - if trading_mode == TradingMode.PAPER: - # FIX 3: Paper Trading Capital Source handled safely - from services.paper_trading_account import paper_trading_service, PaperAccount - account = paper_trading_service.accounts.get(user_id) - - if not account: - # Sync with DB synchronously - from database.models import PaperTradingAccount - db_account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() - if db_account: - account = PaperAccount( - user_id=db_account.user_id, - initial_capital=float(db_account.initial_capital), - current_balance=float(db_account.current_balance), - used_margin=float(db_account.used_margin), - available_margin=float(db_account.available_margin), - total_pnl=float(db_account.total_pnl), - daily_pnl=float(db_account.daily_pnl), - positions_count=db_account.positions_count - ) - paper_trading_service.accounts[user_id] = account - - if account: - total_capital = Decimal(str(account.available_margin)) + Decimal(str(account.used_margin)) - else: - logger.warning(f"Paper account not found for user {user_id}. Falling back to default capital.") - total_capital = self.paper_trading_capital - else: - broker_config = self.get_active_broker_config(user_id, db) - if not broker_config: - self._update_function_health("available_capital", "error", "No active broker config") - return Decimal('0') - - available_margin, used_margin = self._fetch_funds_from_broker(broker_config) - # For Live trading, available_margin already accounts for used margin - # We return it directly as the 'available' cash - return max(Decimal('0'), available_margin) - - # For Paper Trading only: JOIN check to ensure only truly active positions consume capital - allocated_capital = self._get_allocated_capital_for_active_positions(user_id, db) - available_capital = total_capital - allocated_capital + from services.trading_execution.fund_manager import fund_manager + balances = fund_manager.get_balances(user_id, db, trading_mode.value) + + available_capital = Decimal(str(balances.get("available_margin", 0))) # FIX 5: Health tracking self._update_function_health("available_capital", "success") @@ -197,46 +159,15 @@ def get_available_capital_for_new_position( trading_mode: TradingMode = TradingMode.PAPER ) -> Decimal: """ - Get capital available for opening a NEW position + Get capital available for opening a NEW position using centralized Fund Manager """ if not user_id or user_id <= 0: raise ValueError("Invalid user_id provided") try: - if trading_mode == TradingMode.PAPER: - # FIX 3: Paper Trading Capital Source handled safely - from services.paper_trading_account import paper_trading_service, PaperAccount - account = paper_trading_service.accounts.get(user_id) - - if not account: - # Sync with DB synchronously - from database.models import PaperTradingAccount - db_account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() - if db_account: - account = PaperAccount( - user_id=db_account.user_id, - initial_capital=float(db_account.initial_capital), - current_balance=float(db_account.current_balance), - used_margin=float(db_account.used_margin), - available_margin=float(db_account.available_margin), - total_pnl=float(db_account.total_pnl), - daily_pnl=float(db_account.daily_pnl), - positions_count=db_account.positions_count - ) - paper_trading_service.accounts[user_id] = account - - if account: - total_capital = Decimal(str(account.available_margin)) + Decimal(str(account.used_margin)) - else: - logger.warning(f"Paper account not found for user {user_id}. Falling back to default capital.") - total_capital = self.paper_trading_capital - else: - broker_config = self.get_active_broker_config(user_id, db) - if not broker_config: - return Decimal('0') - available_margin, used_margin = self._fetch_funds_from_broker(broker_config) - # For Live, the broker API gives us free cash directly as available_margin - total_capital = available_margin + from services.trading_execution.fund_manager import fund_manager + balances = fund_manager.get_balances(user_id, db, trading_mode.value) + total_capital = Decimal(str(balances.get("available_margin", 0))) # Check concurrent position limit active_positions_count = db.query(ActivePosition).filter( @@ -263,22 +194,14 @@ def get_total_account_size( trading_mode: TradingMode = TradingMode.PAPER ) -> Decimal: """ - Get the total base capital of the account (Available + Used) + Get the total base capital of the account (Available + Used) using centralized Fund Manager Used for risk-per-trade percentage calculations """ try: - if trading_mode == TradingMode.PAPER: - from services.paper_trading_account import paper_trading_service - account = paper_trading_service.accounts.get(user_id) - if account: - return Decimal(str(account.available_margin)) + Decimal(str(account.used_margin)) - return self.paper_trading_capital - else: - broker_config = self.get_active_broker_config(user_id, db) - if not broker_config: - return Decimal('0') - available, used = self._fetch_funds_from_broker(broker_config) - return available + used + from services.trading_execution.fund_manager import fund_manager + balances = fund_manager.get_balances(user_id, db, trading_mode.value) + + return Decimal(str(balances.get("current_balance", 0))) except Exception: return Decimal('0') @@ -507,29 +430,18 @@ def get_capital_utilization_summary( trading_mode: TradingMode = TradingMode.PAPER ) -> Dict[str, Any]: """ - Get comprehensive capital utilization summary for a user + Get comprehensive capital utilization summary for a user using centralized Fund Manager """ if not user_id or user_id <= 0: raise ValueError("Invalid user_id provided") try: - if trading_mode == TradingMode.PAPER: - from services.paper_trading_account import paper_trading_service - account = paper_trading_service.accounts.get(user_id) - if account: - total_capital = Decimal(str(account.available_margin)) + Decimal(str(account.used_margin)) - else: - total_capital = self.paper_trading_capital - else: - broker_config = self.get_active_broker_config(user_id, db) - if broker_config: - available, used = self._fetch_funds_from_broker(broker_config) - total_capital = available + used - else: - total_capital = Decimal('0') - - allocated_capital = self._get_allocated_capital_for_active_positions(user_id, db) - available_capital = total_capital - allocated_capital + from services.trading_execution.fund_manager import fund_manager + balances = fund_manager.get_balances(user_id, db, trading_mode.value) + + total_capital = Decimal(str(balances.get("current_balance", 0))) + available_capital = Decimal(str(balances.get("available_margin", 0))) + allocated_capital = Decimal(str(balances.get("used_margin", 0))) active_positions_count = db.query(ActivePosition).filter( ActivePosition.user_id == user_id, diff --git a/services/trading_execution/execution_handler.py b/services/trading_execution/execution_handler.py index a59d8148..6f6bb2f5 100644 --- a/services/trading_execution/execution_handler.py +++ b/services/trading_execution/execution_handler.py @@ -15,6 +15,7 @@ from database.models import AutoTradeExecution, ActivePosition, User, BrokerConfig from services.trading_execution.capital_manager import TradingMode from services.trading_execution.trade_prep import PreparedTrade, TradeStatus +from services.trading_execution.fund_manager import fund_manager from utils.timezone_utils import get_ist_now_naive, get_ist_isoformat from utils.logging_utils import log_structured, log_trade_result from services.notifications.telegram_service import telegram_notifier @@ -230,37 +231,17 @@ def _execute_paper_trade( user_id=str(prepared_trade.user_id) ) - # UPDATE PAPER TRADING ACCOUNT BALANCE (Sync with in-memory service and DB) - try: - from services.paper_trading_account import paper_trading_service - - trade_data = { - "symbol": prepared_trade.stock_symbol, - "instrument_key": prepared_trade.option_instrument_key, - "option_type": prepared_trade.option_type, - "strike_price": float(prepared_trade.strike_price), - "entry_price": float(entry_price), - "quantity": quantity, - "lot_size": prepared_trade.lot_size, - "invested_amount": float(total_investment), - "stop_loss": float(prepared_trade.stop_loss), - "target": float(prepared_trade.target_price) - } - - # Use synchronous method as we are in a synchronous function - paper_trading_service.execute_paper_trade_sync( - user_id=prepared_trade.user_id, - trade_data=trade_data, - db=db - ) - - # Fetch updated account for logging (from in-memory) - account = paper_trading_service.accounts.get(prepared_trade.user_id) - if account: - logger.info(f"✅ Paper account updated: Balance={account.current_balance:,.2f}, Used={account.used_margin:,.2f}") - - except Exception as e: - logger.error(f"Failed to update paper trading account balance: {e}") + # BLOCK MARGIN & CREATE LEDGER ENTRY (New Fund Management System) + margin_blocked = fund_manager.block_margin( + user_id=prepared_trade.user_id, + amount=float(total_investment), + trading_mode="paper", + reference_id=trade_id, + db=db + ) + + if not margin_blocked: + raise ValueError("Insufficient funds to block margin for this trade") # Create trade execution record trade_execution = AutoTradeExecution( @@ -403,7 +384,7 @@ def _execute_live_trade( trade_id = f"LIVE_{uuid.uuid4().hex[:12].upper()}" # Place order via broker - order_result = self._place_broker_order(broker_config, prepared_trade) + order_result = self._place_broker_order(broker_config, prepared_trade, db) if not order_result.get("success"): raise ValueError( @@ -695,7 +676,7 @@ def _close_paper_position(self, db: Session, position: ActivePosition, trade: Au db.commit() def _place_broker_order( - self, broker_config: BrokerConfig, prepared_trade: PreparedTrade + self, broker_config: BrokerConfig, prepared_trade: PreparedTrade, db: Session ) -> Dict[str, Any]: """ Place order via broker API @@ -752,6 +733,20 @@ def _place_broker_order( f"latency: {latency}ms, IDs: {order_ids}" ) + # Calculate total investment for ledger + actual_price = Decimal(str(prepared_trade.entry_price)) + total_investment = actual_price * Decimal(str(quantity)) + trade_id = f"LIVE_{primary_order_id}" + + # Record margin blocking in ledger for live trade (informational) + fund_manager.block_margin( + user_id=prepared_trade.user_id, + amount=float(total_investment), + trading_mode="live", + reference_id=trade_id, + db=db + ) + return { "success": True, "order_id": primary_order_id, diff --git a/services/trading_execution/fund_manager.py b/services/trading_execution/fund_manager.py new file mode 100644 index 00000000..112a31d8 --- /dev/null +++ b/services/trading_execution/fund_manager.py @@ -0,0 +1,338 @@ +""" +Fund Management Service +Handles capital, ledger entries, and balance tracking for Paper and Live accounts. +""" + +import logging +from decimal import Decimal +from typing import Dict, List, Any, Optional, Tuple +from sqlalchemy.orm import Session +from sqlalchemy import desc + +from database.models import User, PaperTradingAccount, BrokerConfig, FundLedger, AutoTradeExecution +from utils.timezone_utils import get_ist_now_naive + +logger = logging.getLogger("fund_manager") + +class FundManagementService: + """ + Manages all fund-related operations. + Acts as a central authority for balance updates and ledger entries. + """ + + def _add_ledger_entry( + self, + db: Session, + user_id: int, + trading_mode: str, + transaction_type: str, + category: str, + amount: Decimal, + balance_before: Decimal, + balance_after: Decimal, + description: str, + reference_id: Optional[str] = None + ) -> FundLedger: + """Helper to create a ledger entry""" + entry = FundLedger( + user_id=user_id, + trading_mode=trading_mode, + transaction_type=transaction_type, + category=category, + amount=amount, + balance_before=balance_before, + balance_after=balance_after, + description=description, + reference_id=reference_id, + timestamp=get_ist_now_naive() + ) + db.add(entry) + return entry + + def get_balances(self, user_id: int, db: Session, trading_mode: str = "paper") -> Dict[str, Any]: + """Get available, used, and total balances""" + if trading_mode == "paper": + account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() + if not account: + # Create default if not exists + account = PaperTradingAccount( + user_id=user_id, + initial_capital=100000.0, + current_balance=100000.0, + available_margin=100000.0, + used_margin=0.0 + ) + db.add(account) + db.commit() + db.refresh(account) + + return { + "available_margin": float(account.available_margin), + "used_margin": float(account.used_margin), + "current_balance": float(account.current_balance), + "total_pnl": float(account.total_pnl), + "trading_mode": "paper" + } + else: + # Live Trading + broker_config = db.query(BrokerConfig).filter( + BrokerConfig.user_id == user_id, + BrokerConfig.is_active == True + ).first() + + if not broker_config: + return { + "available_margin": 0.0, + "used_margin": 0.0, + "current_balance": 0.0, + "trading_mode": "live", + "error": "No active broker found" + } + + available = broker_config.available_margin or 0.0 + used = broker_config.used_margin or 0.0 + + return { + "available_margin": float(available), + "used_margin": float(used), + "current_balance": float(available + used), + "trading_mode": "live" + } + + def add_paper_funds(self, user_id: int, amount: float, db: Session, description: str = "Funds added by user") -> Dict[str, Any]: + """Add virtual funds to paper trading account""" + try: + account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() + if not account: + # Create default + account = PaperTradingAccount( + user_id=user_id, + initial_capital=float(amount), + current_balance=float(amount), + available_margin=float(amount), + used_margin=0.0 + ) + db.add(account) + db.flush() + balance_before = Decimal('0.0') + else: + balance_before = Decimal(str(account.current_balance)) + account.current_balance += float(amount) + account.available_margin += float(amount) + account.initial_capital += float(amount) + + balance_after = Decimal(str(account.current_balance)) + + # Sync in-memory service + from services.paper_trading_account import paper_trading_service + mem_acc = paper_trading_service.accounts.get(user_id) + if mem_acc: + mem_acc.available_margin = float(account.available_margin) + mem_acc.current_balance = float(account.current_balance) + mem_acc.initial_capital = float(account.initial_capital) + + # Ledger Entry + self._add_ledger_entry( + db=db, + user_id=user_id, + trading_mode="paper", + transaction_type="CREDIT", + category="FUND_ADDED", + amount=Decimal(str(amount)), + balance_before=balance_before, + balance_after=balance_after, + description=description + ) + + db.commit() + logger.info(f"✅ Added ₹{amount:,.2f} to paper account for user {user_id}") + + return { + "success": True, + "amount": amount, + "new_balance": float(account.current_balance) + } + except Exception as e: + db.rollback() + logger.error(f"❌ Error adding paper funds: {e}") + return {"success": False, "error": str(e)} + + def block_margin(self, user_id: int, amount: float, trading_mode: str, reference_id: str, db: Session) -> bool: + """Block margin for a new trade""" + try: + amt_decimal = Decimal(str(amount)) + if trading_mode == "paper": + account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() + if not account or account.available_margin < float(amount): + return False + + balance_before = Decimal(str(account.available_margin)) + account.available_margin -= float(amount) + account.used_margin += float(amount) + balance_after = Decimal(str(account.available_margin)) + + # Sync in-memory service if it exists + from services.paper_trading_account import paper_trading_service + mem_acc = paper_trading_service.accounts.get(user_id) + if mem_acc: + mem_acc.available_margin = float(account.available_margin) + mem_acc.used_margin = float(account.used_margin) + + # Ledger entry for margin blocking (DEBIT from available margin) + self._add_ledger_entry( + db=db, + user_id=user_id, + trading_mode="paper", + transaction_type="DEBIT", + category="TRADE_MARGIN_BLOCKED", + amount=amt_decimal, + balance_before=balance_before, + balance_after=balance_after, + description=f"Margin blocked for trade {reference_id}", + reference_id=reference_id + ) + return True + else: + # For Live, we assume broker already blocked it, but we record it for statement + broker_config = db.query(BrokerConfig).filter(BrokerConfig.user_id == user_id, BrokerConfig.is_active == True).first() + if broker_config: + # Optional: locally update used_margin if we want to track it + pass + + # Still add ledger entry for consistency in statement + # Balance for live is 'available_margin' + bal = Decimal(str(broker_config.available_margin or 0)) + self._add_ledger_entry( + db=db, + user_id=user_id, + trading_mode="live", + transaction_type="DEBIT", + category="TRADE_MARGIN_BLOCKED", + amount=amt_decimal, + balance_before=bal, + balance_after=bal - amt_decimal, # Estimated local balance + description=f"Margin blocked for live trade {reference_id}", + reference_id=reference_id + ) + return True + except Exception as e: + logger.error(f"Error blocking margin: {e}") + return False + + def release_margin_and_settle( + self, + user_id: int, + trading_mode: str, + reference_id: str, + invested_amount: float, + gross_pnl: float, + brokerage: float, + taxes: float, + db: Session + ) -> bool: + """Release margin and settle P&L after trade exit""" + try: + net_pnl = gross_pnl - brokerage - taxes + invested_decimal = Decimal(str(invested_amount)) + net_pnl_decimal = Decimal(str(net_pnl)) + total_release = invested_decimal + net_pnl_decimal + + if trading_mode == "paper": + account = db.query(PaperTradingAccount).filter(PaperTradingAccount.user_id == user_id).first() + if not account: return False + + bal_before = Decimal(str(account.available_margin)) + + # Release used margin + account.used_margin = max(0.0, account.used_margin - invested_amount) + + # Update balances + account.available_margin += float(total_release) + account.current_balance += float(net_pnl) + account.total_pnl += float(net_pnl) + + # Sync in-memory service + from services.paper_trading_account import paper_trading_service + mem_acc = paper_trading_service.accounts.get(user_id) + if mem_acc: + mem_acc.available_margin = float(account.available_margin) + mem_acc.used_margin = float(account.used_margin) + mem_acc.current_balance = float(account.current_balance) + mem_acc.total_pnl = float(account.total_pnl) + + bal_after = Decimal(str(account.available_margin)) + + # Ledger entry: Margin Released + P&L Settlement + self._add_ledger_entry( + db=db, + user_id=user_id, + trading_mode="paper", + transaction_type="CREDIT" if total_release >= 0 else "DEBIT", + category="PNL_SETTLEMENT", + amount=abs(total_release), + balance_before=bal_before, + balance_after=bal_after, + description=f"PnL Settlement for {reference_id} (Net: ₹{net_pnl:.2f})", + reference_id=reference_id + ) + + # Optional: Detail entries for Charges + if brokerage > 0 or taxes > 0: + self._add_ledger_entry( + db=db, + user_id=user_id, + trading_mode="paper", + transaction_type="DEBIT", + category="CHARGES", + amount=Decimal(str(brokerage + taxes)), + balance_before=bal_after + Decimal(str(brokerage + taxes)), + balance_after=bal_after, + description=f"Brokerage & Taxes for {reference_id}", + reference_id=reference_id + ) + + return True + else: + # Live settlement (Local record only) + broker_config = db.query(BrokerConfig).filter(BrokerConfig.user_id == user_id, BrokerConfig.is_active == True).first() + bal = Decimal(str(broker_config.available_margin or 0)) if broker_config else Decimal('0') + + self._add_ledger_entry( + db=db, + user_id=user_id, + trading_mode="live", + transaction_type="CREDIT" if total_release >= 0 else "DEBIT", + category="PNL_SETTLEMENT", + amount=abs(total_release), + balance_before=bal, + balance_after=bal + net_pnl_decimal, # Estimated + description=f"Live PnL Settlement for {reference_id} (Net: ₹{net_pnl:.2f})", + reference_id=reference_id + ) + return True + except Exception as e: + logger.error(f"Error releasing margin: {e}") + return False + + def get_statement(self, user_id: int, db: Session, trading_mode: str = "paper", limit: int = 50) -> List[Dict[str, Any]]: + """Get fund statement (ledger entries)""" + entries = db.query(FundLedger).filter( + FundLedger.user_id == user_id, + FundLedger.trading_mode == trading_mode + ).order_by(desc(FundLedger.timestamp)).limit(limit).all() + + return [ + { + "id": e.id, + "timestamp": e.timestamp.isoformat(), + "type": e.transaction_type, + "category": e.category, + "amount": float(e.amount), + "balance_before": float(e.balance_before), + "balance_after": float(e.balance_after), + "description": e.description, + "reference_id": e.reference_id + } for e in entries + ] + +fund_manager = FundManagementService() diff --git a/services/trading_execution/multi_demat_capital_service.py b/services/trading_execution/multi_demat_capital_service.py index c256b26f..e98cc0ac 100644 --- a/services/trading_execution/multi_demat_capital_service.py +++ b/services/trading_execution/multi_demat_capital_service.py @@ -69,45 +69,15 @@ async def get_user_total_capital( raise async def _get_paper_trading_capital(self, user_id: int, db: Session) -> Dict[str, Any]: - """Get paper trading virtual capital using the singleton service""" - from services.paper_trading_account import paper_trading_service + """Get paper trading virtual capital using the fund manager service""" + from services.trading_execution.fund_manager import fund_manager - # Get actual paper account state - paper_account = await paper_trading_service.get_account(user_id, db) + # Get actual paper account state from fund manager + balances = fund_manager.get_balances(user_id, db, "paper") - if paper_account: - # Calculate totals based on PaperAccount state - # total_available_capital (PaperAccount.current_balance) includes Cash + PnL - # total_used_margin (PaperAccount.used_margin) is blocked capital - # total_free_margin (PaperAccount.available_margin) is free cash for new trades - - # Note: In PaperAccount logic: - # available_margin = initial - used + pnl - # current_balance = initial - used (cash balance) -- Wait, let's verify PaperAccount logic - # Logic from execute_trade: - # used_margin += invested - # available_margin -= invested - # current_balance -= invested - # Logic from close_position: - # available_margin += invested + pnl - # current_balance += invested + pnl - - # So: - # available_margin = Free Cash (Available for trading) - # current_balance = Free Cash (Same as available_margin in this implementation) - # used_margin = Blocked Margin - # Total Equity = available_margin + used_margin - - total_free_margin = Decimal(str(paper_account.available_margin)) - total_used_margin = Decimal(str(paper_account.used_margin)) - paper_capital = total_free_margin + total_used_margin - - else: - # Fallback if account doesn't exist yet (create it implicitly) - await paper_trading_service.create_paper_account(user_id) - paper_capital = Decimal('100000') - total_used_margin = Decimal('0') - total_free_margin = paper_capital + total_free_margin = Decimal(str(balances.get("available_margin", 0))) + total_used_margin = Decimal(str(balances.get("used_margin", 0))) + paper_capital = Decimal(str(balances.get("current_balance", 0))) return { "user_id": user_id, diff --git a/services/trading_execution/pnl_tracker.py b/services/trading_execution/pnl_tracker.py index 4a24fcd3..f0505aab 100644 --- a/services/trading_execution/pnl_tracker.py +++ b/services/trading_execution/pnl_tracker.py @@ -967,59 +967,19 @@ def db_close_job(pos_id, trade_id, price, reason, order_id): position.is_active = False position.last_updated = get_ist_now_naive() - # UPDATE PAPER TRADING BALANCE ON EXIT - if trade_execution.trading_mode == "paper": - try: - from services.paper_trading_account import ( - paper_trading_service, - ) - from database.models import PaperTradingAccount - - paper_account = ( - db.query(PaperTradingAccount) - .filter(PaperTradingAccount.user_id == position.user_id) - .first() - ) - if paper_account: - release_amount = float(sell_value - total_charges) - - paper_account.available_margin += release_amount - paper_account.current_balance += release_amount - paper_account.used_margin -= float( - trade_execution.total_investment - ) - paper_account.total_pnl += float(net_pnl) - paper_account.daily_pnl += float(net_pnl) - paper_account.positions_count = max( - 0, paper_account.positions_count - 1 - ) - paper_account.updated_at = get_ist_now_naive() - - # Sync in-memory service - mem_acc = paper_trading_service.accounts.get( - position.user_id - ) - if mem_acc: - mem_acc.available_margin = float( - paper_account.available_margin - ) - mem_acc.current_balance = float( - paper_account.current_balance - ) - mem_acc.used_margin = float( - paper_account.used_margin - ) - mem_acc.total_pnl = float(paper_account.total_pnl) - mem_acc.positions_count = ( - paper_account.positions_count - ) - - logger.info( - f"✅ Paper account updated after exit: New Balance=₹{paper_account.current_balance:,.2f} (Returned ₹{release_amount:,.2f})" - ) - - except Exception as e: - logger.error(f"Failed to update paper account on exit: {e}") + # SETTLE FUNDS & CREATE LEDGER ENTRIES (New Fund Management System) + from services.trading_execution.fund_manager import fund_manager + + fund_manager.release_margin_and_settle( + user_id=position.user_id, + trading_mode=trade_execution.trading_mode, + reference_id=trade_execution.trade_id, + invested_amount=float(total_investment), + gross_pnl=float(gross_pnl), + brokerage=float(brokerage_flat), + taxes=float(taxes), + db=db + ) db.commit() diff --git a/ui/trading-bot-ui/src/components/funds/AddFundsModal.js b/ui/trading-bot-ui/src/components/funds/AddFundsModal.js new file mode 100644 index 00000000..51846d14 --- /dev/null +++ b/ui/trading-bot-ui/src/components/funds/AddFundsModal.js @@ -0,0 +1,159 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { X, Plus, ShieldCheck } from "lucide-react"; +import { toast } from "react-hot-toast"; +import api from "../../services/api"; + +const AddFundsModal = ({ isOpen, onClose, onFundAdded }) => { + const [amount, setAmount] = useState(""); + const [loading, setLoading] = useState(false); + + const presetAmounts = [10000, 50000, 100000, 500000]; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!amount || isNaN(amount) || amount <= 0) { + toast.error("Please enter a valid amount"); + return; + } + + setLoading(true); + try { + const response = await api.post( + `/v1/trading/execution/funds/add-paper-funds?amount=${amount}`, + ); + if (response.data.success) { + toast.success( + `Successfully added ₹${parseFloat(amount).toLocaleString()} to paper account`, + ); + onFundAdded(response.data.new_balance); + onClose(); + setAmount(""); + } else { + toast.error(response.data.error || "Failed to add funds"); + } + } catch (error) { + console.error("Error adding funds:", error); + toast.error("An error occurred. Please try again."); + } finally { + setLoading(false); + } + }; + + return ( + + {isOpen && ( +
+ + {/* Mobile Handle */} +
+
+
+ + {/* Header */} +
+
+
+ +
+
+

+ Add Paper Funds +

+

+ Increase your virtual capital +

+
+
+ +
+ +
+ {/* Presets */} +
+ +
+ {presetAmounts.map((amt) => ( + + ))} +
+
+ + {/* Custom Amount */} +
+ +
+
+ ₹ +
+ setAmount(e.target.value)} + placeholder="0.00" + className="tw-w-full tw-bg-slate-950 tw-border tw-border-slate-700 tw-rounded-xl tw-py-4 tw-pl-10 tw-pr-4 tw-text-white tw-text-2xl tw-font-black focus:tw-outline-none focus:tw-border-indigo-500 focus:tw-ring-1 focus:tw-ring-indigo-500 tw-transition-all" + required + /> +
+
+ + {/* Safety Info */} +
+ +

+ + Virtual Capital: + {" "} + This will update your paper trading balance instantly. No real + bank transactions are involved. +

+
+ + +
+ +
+ )} + + ); +}; + +export default AddFundsModal; diff --git a/ui/trading-bot-ui/src/components/funds/FundStatementTable.js b/ui/trading-bot-ui/src/components/funds/FundStatementTable.js new file mode 100644 index 00000000..ef6d03a8 --- /dev/null +++ b/ui/trading-bot-ui/src/components/funds/FundStatementTable.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { ArrowUpRight, ArrowDownLeft, Receipt, Calendar } from 'lucide-react'; + +const FundStatementTable = ({ statement, loading }) => { + const formatCurrency = (amount) => { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + minimumFractionDigits: 2 + }).format(amount); + }; + + const formatDate = (dateStr) => { + const date = new Date(dateStr); + return date.toLocaleDateString('en-IN', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + if (loading) { + return ( +
+ {[1, 2, 3, 4, 5].map((i) => ( +
+ ))} +
+ ); + } + + if (!statement || statement.length === 0) { + return ( +
+ +

No transactions found

+

Your fund movements will appear here.

+
+ ); + } + + return ( +
+
+ + + + + + + + + + + {statement.map((entry, index) => ( + + + + + + + ))} + +
Date & DescriptionTypeAmountRunning Balance
+
+ {entry.description} +
+ + {formatDate(entry.timestamp)} + {entry.reference_id && ( + + Ref: {entry.reference_id} + + )} +
+
+
+
+ {entry.type === 'CREDIT' ? ( +
+ +
+ ) : ( +
+ +
+ )} + + {entry.category.replace('_', ' ')} + +
+
+ + {entry.type === 'CREDIT' ? '+' : '-'}{formatCurrency(entry.amount)} + + + + {formatCurrency(entry.balance_after)} + +
+
+
+ ); +}; + +export default FundStatementTable; diff --git a/ui/trading-bot-ui/src/components/profile/FundsTab.js b/ui/trading-bot-ui/src/components/profile/FundsTab.js new file mode 100644 index 00000000..1a59d6f5 --- /dev/null +++ b/ui/trading-bot-ui/src/components/profile/FundsTab.js @@ -0,0 +1,205 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { motion } from 'framer-motion'; +import { + Wallet, + Plus, + ArrowUpRight, + ArrowDownLeft, + History, + Info, + ExternalLink +} from 'lucide-react'; +import { toast } from 'react-hot-toast'; +import api from '../../services/api'; +import AddFundsModal from '../funds/AddFundsModal'; +import FundStatementTable from '../funds/FundStatementTable'; + +const FundsTab = () => { + const [tradingMode, setTradingMode] = useState('paper'); + const [balances, setBalances] = useState({ + available_margin: 0, + used_margin: 0, + current_balance: 0, + total_pnl: 0 + }); + const [statement, setStatement] = useState([]); + const [loading, setLoading] = useState(true); + const [isAddFundsOpen, setIsAddFundsOpen] = useState(false); + + const fetchFundData = useCallback(async () => { + setLoading(true); + try { + const [balanceRes, statementRes] = await Promise.all([ + api.get(`/v1/trading/execution/funds/balance?trading_mode=${tradingMode}`), + api.get(`/v1/trading/execution/funds/statement?trading_mode=${tradingMode}&limit=20`) + ]); + + if (balanceRes.data.success) { + setBalances(balanceRes.data.balances); + } + if (statementRes.data.success) { + setStatement(statementRes.data.statement); + } + } catch (error) { + console.error("Error fetching fund data:", error); + toast.error("Failed to load fund details"); + } finally { + setLoading(false); + } + }, [tradingMode]); + + useEffect(() => { + fetchFundData(); + }, [fetchFundData]); + + const formatCurrency = (amount) => { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + minimumFractionDigits: 2 + }).format(amount || 0); + }; + + return ( +
+ {/* Trading Mode Switcher */} +
+ + +
+ + {/* Balance Overview Cards */} +
+ {/* Available Margin */} + +
+
+ +
+ {tradingMode === 'paper' && ( + + )} +
+

Available Margin

+

{formatCurrency(balances.available_margin)}

+
+ + Funds available for new positions +
+
+ + {/* Used Margin */} + +
+
+ +
+
+

Used Margin

+

{formatCurrency(balances.used_margin)}

+
+ + Capital locked in active trades +
+
+ + {/* Total P&L / Balance */} + +
+
= 0 ? 'tw-bg-emerald-500/10' : 'tw-bg-rose-500/10'}`}> + {balances.total_pnl >= 0 ? ( + + ) : ( + + )} +
+
+

Total Realized P&L

+

= 0 ? 'tw-text-emerald-400' : 'tw-text-rose-400'}`}> + {formatCurrency(balances.total_pnl)} +

+
+ + All-time realized profit/loss +
+
+
+ + {/* Live Broker Alert */} + {tradingMode === 'live' && ( +
+ +
+

Live Fund Sync

+

+ Live trading funds are managed by your connected broker. Balances shown here are fetched in real-time from your demat account. +

+ +
+
+ )} + + {/* Fund Statement */} +
+
+
+ +

Fund Statement

+
+ +
+
+ +
+
+ + {/* Modal */} + setIsAddFundsOpen(false)} + onFundAdded={() => fetchFundData()} + /> +
+ ); +}; + +export default FundsTab; diff --git a/ui/trading-bot-ui/src/components/profile/ProfileTabs.js b/ui/trading-bot-ui/src/components/profile/ProfileTabs.js index 54d1bde8..c5bab7d3 100644 --- a/ui/trading-bot-ui/src/components/profile/ProfileTabs.js +++ b/ui/trading-bot-ui/src/components/profile/ProfileTabs.js @@ -4,11 +4,12 @@ import { motion } from "framer-motion"; import { LayoutDashboard, LineChart, - Wallet, + Coins, + Briefcase, User, Shield, Bell, - ChevronRight + ChevronRight, } from "lucide-react"; const ProfileTabs = ({ @@ -35,11 +36,19 @@ const ProfileTabs = ({ color: "tw-text-emerald-500", alert: false, }, + { + id: "funds", + label: "Funds", + description: "Capital & Ledger", + icon: Coins, + color: "tw-text-orange-500", + alert: false, + }, { id: "brokers", label: "Brokers", description: "Manage Accounts", - icon: Wallet, + icon: Briefcase, color: "tw-text-blue-500", count: brokerCount > 0 ? brokerCount : null, alert: false, @@ -89,14 +98,20 @@ const ProfileTabs = ({ transition={{ type: "spring", bounce: 0.2, duration: 0.6 }} /> )} - + - + {tab.label} {tab.count && ( - + {tab.count} )} @@ -122,27 +137,37 @@ const ProfileTabs = ({ /> )} -
- +
+
- + {tab.label} {tab.count && ( - + {tab.count} )} @@ -164,7 +189,11 @@ const ProfileTabs = ({
{tabs.map((tab) => ( - + ))}
@@ -177,11 +206,15 @@ const ProfileTabs = ({
{tabs.map((tab) => ( - + ))}
); }; -export default ProfileTabs; \ No newline at end of file +export default ProfileTabs; diff --git a/ui/trading-bot-ui/src/pages/AutoTradingPage.js b/ui/trading-bot-ui/src/pages/AutoTradingPage.js index 511cfa99..d9235081 100644 --- a/ui/trading-bot-ui/src/pages/AutoTradingPage.js +++ b/ui/trading-bot-ui/src/pages/AutoTradingPage.js @@ -1,7 +1,9 @@ import React, { useState, useEffect, useRef, useCallback } from "react"; import api from "../services/api"; +import { Plus } from "lucide-react"; import ActivePositionCard from "../components/ActivePositionCard"; import SelectedStockCard from "../components/SelectedStockCard"; +import AddFundsModal from "../components/funds/AddFundsModal"; const AutoTradingPage = () => { const [tradingMode, setTradingMode] = useState("paper"); @@ -43,6 +45,7 @@ const AutoTradingPage = () => { const [lastUpdated, setLastUpdated] = useState(new Date()); const [wsConnected, setWsConnected] = useState(false); const [showLiveConfirmation, setShowLiveConfirmation] = useState(false); + const [isAddFundsOpen, setIsAddFundsOpen] = useState(false); // WebSocket Throttling Refs const updatesBuffer = useRef({ @@ -718,7 +721,18 @@ const AutoTradingPage = () => {

{formatCurrency(pnlSummary.total_investment)}

-

Available Capital

+
+

Available Capital

+ {tradingMode === 'paper' && ( + + )} +

{formatCurrency(capitalData.total_free_margin)}

@@ -848,11 +862,11 @@ const AutoTradingPage = () => {

Used Margin

-

{formatCurrency(capitalData.total_used_margin || 0)}

-

{capitalData.capital_utilization_percent?.toFixed(1) || 0}% utilized

+

{formatCurrency(Math.abs(capitalData.total_used_margin || 0))}

+

{Math.abs(capitalData.capital_utilization_percent || 0).toFixed(1)}% utilized

-

Free Margin

+

Available Cash

{formatCurrency(capitalData.total_free_margin || 0)}

@@ -1114,6 +1128,16 @@ const AutoTradingPage = () => {
)} + + {/* Modal */} + setIsAddFundsOpen(false)} + onFundAdded={() => { + fetchCapitalOverview(); + handleManualRefresh(); + }} + /> ); }; diff --git a/ui/trading-bot-ui/src/pages/ProfilePage.js b/ui/trading-bot-ui/src/pages/ProfilePage.js index dbde15b2..a13109ab 100644 --- a/ui/trading-bot-ui/src/pages/ProfilePage.js +++ b/ui/trading-bot-ui/src/pages/ProfilePage.js @@ -16,6 +16,7 @@ import ProfileSecurity from "../components/profile/ProfileSecurity"; import ProfileNotifications from "../components/profile/ProfileNotifications"; import EnhancedBrokerManagement from "../components/profile/EnhancedBrokerManagement"; import PerformanceTab from "../components/profile/PerformanceTab"; +import FundsTab from "../components/profile/FundsTab"; import { profileService } from "../services/profileService"; const ProfilePage = () => { @@ -116,6 +117,12 @@ const ProfilePage = () => { ); + case "funds": + return ( + + + + ); case "brokers": return ( @@ -256,6 +263,7 @@ const ProfilePage = () => {

{activeTab === "overview" && "Comprehensive view of your trading performance and account status."} {activeTab === "performance" && "Detailed analytics, P&L reports, and trade history."} + {activeTab === "funds" && "Manage your trading capital, add funds, and view transaction logs."} {activeTab === "brokers" && "Manage your connected brokerage accounts and API keys."} {activeTab === "settings" && "Update your personal information and preferences."} {activeTab === "security" && "Manage password, 2FA, and security logs."}