diff --git a/app/api/api_v1/api.py b/app/api/api_v1/api.py index 5ed011e..0d11dd2 100644 --- a/app/api/api_v1/api.py +++ b/app/api/api_v1/api.py @@ -1,8 +1,9 @@ from fastapi import APIRouter -from app.api.api_v1.endpoints import status, opportunities, trades +from app.api.api_v1.endpoints import status, opportunities, trades, events api_router = APIRouter(prefix="/api/v1") api_router.include_router(status.router, prefix="/status", tags=["status"]) api_router.include_router(opportunities.router, prefix="/opportunities", tags=["opportunities"]) -api_router.include_router(trades.router, prefix="/trades", tags=["trades"]) \ No newline at end of file +api_router.include_router(trades.router, prefix="/trades", tags=["trades"]) +api_router.include_router(events.router, prefix="/events", tags=["events"]) \ No newline at end of file diff --git a/app/api/api_v1/endpoints/events.py b/app/api/api_v1/endpoints/events.py new file mode 100644 index 0000000..37790e9 --- /dev/null +++ b/app/api/api_v1/endpoints/events.py @@ -0,0 +1,84 @@ +from typing import Any, Optional, List +from fastapi import APIRouter, Depends, Query, HTTPException, status +from sqlalchemy.orm import Session +from sqlalchemy import desc + +from app.db.session import get_db +from app.models.arbitrage import SystemEvent +from app.schemas.arbitrage import SystemEventCreate, SystemEvent as SystemEventSchema + +router = APIRouter() + + +@router.get("", response_model=List[SystemEventSchema]) +async def get_system_events( + event_type: Optional[str] = Query(None, description="Filter by event type (startup, shutdown, error, warning, info)"), + component: Optional[str] = Query(None, description="Filter by component"), + limit: int = Query(50, ge=1, le=200, description="Number of events to return"), + offset: int = Query(0, ge=0, description="Pagination offset"), + db: Session = Depends(get_db) +) -> Any: + """ + Retrieve system events with optional filtering. + """ + query = db.query(SystemEvent) + + # Apply filters + if event_type: + query = query.filter(SystemEvent.event_type == event_type) + + if component: + query = query.filter(SystemEvent.component == component) + + # Get paginated results + events = query.order_by(desc(SystemEvent.timestamp)).offset(offset).limit(limit).all() + + return events + + +@router.get("/{event_id}", response_model=SystemEventSchema) +async def get_system_event( + event_id: int, + db: Session = Depends(get_db) +) -> Any: + """ + Get a specific system event by ID. + """ + event = db.query(SystemEvent).filter(SystemEvent.id == event_id).first() + if not event: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"System event with ID {event_id} not found" + ) + return event + + +@router.post("", response_model=SystemEventSchema, status_code=status.HTTP_201_CREATED) +async def create_system_event( + event_in: SystemEventCreate, + db: Session = Depends(get_db) +) -> Any: + """ + Create a new system event. + """ + # Validate event type + valid_event_types = ["startup", "shutdown", "error", "warning", "info"] + if event_in.event_type not in valid_event_types: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid event type. Must be one of: {', '.join(valid_event_types)}" + ) + + # Create the system event + db_event = SystemEvent( + event_type=event_in.event_type, + component=event_in.component, + message=event_in.message, + details=event_in.details + ) + + db.add(db_event) + db.commit() + db.refresh(db_event) + + return db_event \ No newline at end of file diff --git a/app/api/api_v1/endpoints/opportunities.py b/app/api/api_v1/endpoints/opportunities.py index 63e938a..89d6e10 100644 --- a/app/api/api_v1/endpoints/opportunities.py +++ b/app/api/api_v1/endpoints/opportunities.py @@ -1,12 +1,12 @@ from datetime import datetime from typing import Any, Optional -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, HTTPException, status from sqlalchemy.orm import Session from sqlalchemy import desc from app.db.session import get_db from app.models.arbitrage import ArbitrageOpportunity -from app.schemas.arbitrage import OpportunitiesList +from app.schemas.arbitrage import OpportunitiesList, ArbitrageOpportunityCreate, ArbitrageOpportunity as ArbitrageOpportunitySchema router = APIRouter() @@ -45,4 +45,53 @@ async def get_arbitrage_opportunities( "opportunities": opportunities, "count": total_count, "timestamp": datetime.utcnow() - } \ No newline at end of file + } + + +@router.get("/{opportunity_id}", response_model=ArbitrageOpportunitySchema) +async def get_arbitrage_opportunity( + opportunity_id: int, + db: Session = Depends(get_db) +) -> Any: + """ + Get a specific arbitrage opportunity by ID. + """ + opportunity = db.query(ArbitrageOpportunity).filter(ArbitrageOpportunity.id == opportunity_id).first() + if not opportunity: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Arbitrage opportunity with ID {opportunity_id} not found" + ) + return opportunity + + +@router.post("", response_model=ArbitrageOpportunitySchema, status_code=status.HTTP_201_CREATED) +async def create_arbitrage_opportunity( + opportunity_in: ArbitrageOpportunityCreate, + db: Session = Depends(get_db) +) -> Any: + """ + Create a new arbitrage opportunity. + """ + db_opportunity = ArbitrageOpportunity( + token_address=opportunity_in.token_address, + token_symbol=opportunity_in.token_symbol, + source_dex=opportunity_in.source_dex, + target_dex=opportunity_in.target_dex, + source_price=opportunity_in.source_price, + target_price=opportunity_in.target_price, + price_difference=opportunity_in.price_difference, + price_difference_percent=opportunity_in.price_difference_percent, + estimated_profit_usd=opportunity_in.estimated_profit_usd, + estimated_profit_token=opportunity_in.estimated_profit_token, + max_trade_amount_usd=opportunity_in.max_trade_amount_usd, + max_trade_amount_token=opportunity_in.max_trade_amount_token, + slippage_estimate=opportunity_in.slippage_estimate, + fees_estimate=opportunity_in.fees_estimate, + is_viable=opportunity_in.is_viable, + was_executed=False + ) + db.add(db_opportunity) + db.commit() + db.refresh(db_opportunity) + return db_opportunity \ No newline at end of file diff --git a/app/api/api_v1/endpoints/trades.py b/app/api/api_v1/endpoints/trades.py index 11aa7a8..a972f9b 100644 --- a/app/api/api_v1/endpoints/trades.py +++ b/app/api/api_v1/endpoints/trades.py @@ -1,12 +1,12 @@ from datetime import datetime, timedelta from typing import Any, Optional -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, HTTPException, status from sqlalchemy.orm import Session from sqlalchemy import desc, func from app.db.session import get_db -from app.models.arbitrage import Trade -from app.schemas.arbitrage import TradesList +from app.models.arbitrage import Trade, ArbitrageOpportunity +from app.schemas.arbitrage import TradesList, TradeCreate, Trade as TradeSchema router = APIRouter() @@ -53,4 +53,70 @@ async def get_trades( "count": total_count, "timestamp": datetime.utcnow(), "total_profit_usd": total_profit - } \ No newline at end of file + } + + +@router.get("/{trade_id}", response_model=TradeSchema) +async def get_trade( + trade_id: int, + db: Session = Depends(get_db) +) -> Any: + """ + Get a specific trade by ID. + """ + trade = db.query(Trade).filter(Trade.id == trade_id).first() + if not trade: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Trade with ID {trade_id} not found" + ) + return trade + + +@router.post("", response_model=TradeSchema, status_code=status.HTTP_201_CREATED) +async def create_trade( + trade_in: TradeCreate, + db: Session = Depends(get_db) +) -> Any: + """ + Create a new trade record. + """ + # Check if the opportunity exists + opportunity = db.query(ArbitrageOpportunity).filter( + ArbitrageOpportunity.id == trade_in.opportunity_id + ).first() + + if not opportunity: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Arbitrage opportunity with ID {trade_in.opportunity_id} not found" + ) + + # Create the trade + db_trade = Trade( + opportunity_id=trade_in.opportunity_id, + token_address=trade_in.token_address, + token_symbol=trade_in.token_symbol, + source_dex=trade_in.source_dex, + target_dex=trade_in.target_dex, + input_amount=trade_in.input_amount, + input_amount_usd=trade_in.input_amount_usd, + output_amount=trade_in.output_amount, + output_amount_usd=trade_in.output_amount_usd, + profit_amount=trade_in.profit_amount, + profit_amount_usd=trade_in.profit_amount_usd, + profit_percent=trade_in.profit_percent, + tx_signature=trade_in.tx_signature, + tx_status=trade_in.tx_status, + tx_error=trade_in.tx_error + ) + + db.add(db_trade) + + # Mark the opportunity as executed + opportunity.was_executed = True + + db.commit() + db.refresh(db_trade) + + return db_trade \ No newline at end of file diff --git a/app/services/solana.py b/app/services/solana.py index 0e91318..de25f90 100644 --- a/app/services/solana.py +++ b/app/services/solana.py @@ -3,6 +3,7 @@ import logging from typing import Dict, List, Optional, Any, Tuple import base64 import base58 +from app.core.config import settings logger = logging.getLogger(__name__) @@ -54,8 +55,6 @@ except ImportError: class TxOpts: pass -from app.core.config import settings - # Initialize Solana client solana_client = Client(settings.SOLANA_RPC_URL)