Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.0
2.0.1
92 changes: 92 additions & 0 deletions alembic/versions/a62763117ed4_add_fund_ledger_system.py
Original file line number Diff line number Diff line change
@@ -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 ###
36 changes: 36 additions & 0 deletions database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =======================
Expand Down
29 changes: 26 additions & 3 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
48 changes: 48 additions & 0 deletions router/trading_execution_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1881,3 +1882,50 @@
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

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.

Copilot Autofix

AI 2 months ago

General approach: Do not expose raw exception messages (or derived text like stack traces) in HTTP responses. Instead, log the full exception server‑side and return a generic, user‑safe error message and optionally a stable error code. Internally, service methods should not return raw str(e) to API layers; they should either raise suitable application exceptions or return generic error fields without implementation details.

Best concrete fix here, without changing existing behavior structure:

  1. In FundManagementService.add_paper_funds, change the except block so that:

    • It still rolls back the transaction and logs the detailed error (including e) using the existing logger.
    • But instead of returning {"success": False, "error": str(e)}, it returns a generic error message, e.g. {"success": False, "error": "Failed to add paper funds. Please try again later."}. This preserves the response shape (success + error string) so existing callers don’t break, but removes sensitive details.
  2. In the router’s /funds/add-paper-funds endpoint, the current pattern of catching exceptions and raising HTTPException(status_code=500, detail=str(e)) can also leak str(e) if something fails before reaching the service or if the service raises instead of returning an error dict. Change that HTTPException detail to a generic message, e.g. "Internal server error while adding funds". Logging already captures the detailed error.

These two changes ensure that neither direct exceptions nor service-returned errors expose internal exception text to clients, while leaving overall control flow and result structure intact.

Specific edits:

  • File services/trading_execution/fund_manager.py, within FundManagementService.add_paper_funds, lines 155–158.
  • File router/trading_execution_router.py, within add_paper_funds route, lines 1914–1916.

No new methods or imports are required beyond what’s already present; we continue using the existing logger instances.

Suggested changeset 2
router/trading_execution_router.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/router/trading_execution_router.py b/router/trading_execution_router.py
--- a/router/trading_execution_router.py
+++ b/router/trading_execution_router.py
@@ -1913,7 +1913,11 @@
         return result
     except Exception as e:
         logger.error(f"Error adding funds: {e}")
-        raise HTTPException(status_code=500, detail=str(e))
+        # Do not expose internal exception details to the client
+        raise HTTPException(
+            status_code=500,
+            detail="Internal server error while adding funds."
+        )
 
 @router.get("/funds/statement")
 async def get_fund_statement(
EOF
@@ -1913,7 +1913,11 @@
return result
except Exception as e:
logger.error(f"Error adding funds: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Do not expose internal exception details to the client
raise HTTPException(
status_code=500,
detail="Internal server error while adding funds."
)

@router.get("/funds/statement")
async def get_fund_statement(
services/trading_execution/fund_manager.py
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/services/trading_execution/fund_manager.py b/services/trading_execution/fund_manager.py
--- a/services/trading_execution/fund_manager.py
+++ b/services/trading_execution/fund_manager.py
@@ -155,7 +155,11 @@
         except Exception as e:
             db.rollback()
             logger.error(f"❌ Error adding paper funds: {e}")
-            return {"success": False, "error": str(e)}
+            # Return a generic error message to avoid exposing internal details
+            return {
+                "success": False,
+                "error": "Failed to add paper funds. Please try again later."
+            }
 
     def block_margin(self, user_id: int, amount: float, trading_mode: str, reference_id: str, db: Session) -> bool:
         """Block margin for a new trade"""
EOF
@@ -155,7 +155,11 @@
except Exception as e:
db.rollback()
logger.error(f"❌ Error adding paper funds: {e}")
return {"success": False, "error": str(e)}
# Return a generic error message to avoid exposing internal details
return {
"success": False,
"error": "Failed to add paper funds. Please try again later."
}

def block_margin(self, user_id: int, amount: float, trading_mode: str, reference_id: str, db: Session) -> bool:
"""Block margin for a new trade"""
Copilot is powered by AI and may make mistakes. Always verify output.
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))
25 changes: 3 additions & 22 deletions services/paper_trading_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading