diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb8f3ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.pyc + +# Virtual env +venv/ +ENV/ +.env +.env.* + +# VSCode +.vscode/ + +# Jupyter +.ipynb_checkpoints + +# OS +.DS_Store +Thumbs.db + +# Log, data +*.log +*.sqlite3 +accounts.json \ No newline at end of file diff --git a/README.md b/README.md index b9800d1..8e9d925 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,36 @@ +# AI Trading System Backend + +## Yêu cầu +- Python 3.8+ +- Tài khoản Supabase +- API key Bybit + +## Cài đặt + +```bash +pip install -r requirements.txt +``` + +Tạo file `.env` ở thư mục gốc với nội dung: + +``` +SUPABASE_URL=your_supabase_url +SUPABASE_KEY=your_supabase_key +BYBIT_API_KEY=your_bybit_api_key +BYBIT_API_SECRET=your_bybit_api_secret +``` + +## Chạy server + +```bash +uvicorn app.main:app --reload +``` + +## Các endpoint +- GET `/api/candles?symbol=BTCUSDT&interval=1` : Lấy dữ liệu nến +- GET `/api/orders?symbol=BTCUSDT` : Lấy thông tin order +- GET `/api/account` : Lấy thông tin tài khoản + # AI-trading-sys diff --git a/app/api/account.py b/app/api/account.py new file mode 100644 index 0000000..383b73c --- /dev/null +++ b/app/api/account.py @@ -0,0 +1,120 @@ +from fastapi import APIRouter, Query, HTTPException, Body +from app.services.bybit_service import get_bybit_client +import json +import os +from typing import List, Optional + +router = APIRouter() + +ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json') + +def get_user_api(user_id: str): + try: + with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f: + accounts = json.load(f) + for acc in accounts: + if acc['id'] == user_id: + return acc['bybit_api_key'], acc['bybit_api_secret'] + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}") + raise HTTPException(status_code=404, detail="Không tìm thấy userId") + +# API lấy danh sách user +@router.get("/users") +def list_users(): + try: + with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f: + accounts = json.load(f) + return accounts + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}") + +# API thêm user +@router.post("/users") +def add_user( + id: str = Body(..., description="ID user"), + bybit_api_key: str = Body(..., description="Bybit API Key"), + bybit_api_secret: str = Body(..., description="Bybit API Secret"), + user_name: str = Body(..., description="Tên user") +): + try: + with open(ACCOUNTS_FILE, 'r+', encoding='utf-8') as f: + accounts = json.load(f) + if any(acc['id'] == id for acc in accounts): + raise HTTPException(status_code=400, detail="ID đã tồn tại") + new_user = { + "id": id, + "bybit_api_key": bybit_api_key, + "bybit_api_secret": bybit_api_secret, + "user_name": user_name + } + accounts.append(new_user) + f.seek(0) + json.dump(accounts, f, ensure_ascii=False, indent=2) + f.truncate() + return {"message": "Thêm user thành công"} + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi ghi file accounts: {e}") + +# API sửa user +@router.put("/users/{user_id}") +def update_user( + user_id: str, + bybit_api_key: Optional[str] = Body(None), + bybit_api_secret: Optional[str] = Body(None), + user_name: Optional[str] = Body(None) +): + try: + with open(ACCOUNTS_FILE, 'r+', encoding='utf-8') as f: + accounts = json.load(f) + for acc in accounts: + if acc['id'] == user_id: + if bybit_api_key is not None: + acc['bybit_api_key'] = bybit_api_key + if bybit_api_secret is not None: + acc['bybit_api_secret'] = bybit_api_secret + if user_name is not None: + acc['user_name'] = user_name + f.seek(0) + json.dump(accounts, f, ensure_ascii=False, indent=2) + f.truncate() + return {"message": "Cập nhật user thành công"} + raise HTTPException(status_code=404, detail="Không tìm thấy userId") + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi ghi file accounts: {e}") + +# API xóa user +@router.delete("/users/{user_id}") +def delete_user(user_id: str): + try: + with open(ACCOUNTS_FILE, 'r+', encoding='utf-8') as f: + accounts = json.load(f) + new_accounts = [acc for acc in accounts if acc['id'] != user_id] + if len(new_accounts) == len(accounts): + raise HTTPException(status_code=404, detail="Không tìm thấy userId") + f.seek(0) + json.dump(new_accounts, f, ensure_ascii=False, indent=2) + f.truncate() + return {"message": "Xóa user thành công"} + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi ghi file accounts: {e}") + +@router.get("/account") +def get_account_info(userId: str = Query(..., description="ID của user để lấy API key/secret")): + try: + api_key, api_secret = get_user_api(userId) + client = get_bybit_client(api_key, api_secret) + res = client.get_wallet_balance(accountType="UNIFIED") + if 'retCode' in res and res['retCode'] != 0: + raise HTTPException(status_code=400, detail=f"Bybit API error: {res.get('retMsg', 'Unknown error')}") + return res + except HTTPException as e: + raise e + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi hệ thống: {e}") diff --git a/app/api/candles.py b/app/api/candles.py new file mode 100644 index 0000000..76318f5 --- /dev/null +++ b/app/api/candles.py @@ -0,0 +1,106 @@ +from fastapi import APIRouter, Query, HTTPException +from typing import Optional +from app.services.bybit_service import get_bybit_client +import json +import os +import pandas as pd +import numpy as np + +router = APIRouter() + +ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json') + +def get_user_api(user_id: str): + try: + with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f: + accounts = json.load(f) + for acc in accounts: + if acc['id'] == user_id: + return acc['bybit_api_key'], acc['bybit_api_secret'] + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}") + raise HTTPException(status_code=404, detail="Không tìm thấy userId") + +def ema(series, period): + return series.ewm(span=period, adjust=False).mean() + +def macd(series, fast=45, slow=90, signal=9): + ema_fast = ema(series, fast) + ema_slow = ema(series, slow) + macd_line = ema_fast - ema_slow + signal_line = macd_line.ewm(span=signal, adjust=False).mean() + macd_hist = macd_line - signal_line + return macd_line, signal_line, macd_hist + +def rsi(series, period=14): + delta = series.diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period, min_periods=1).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period, min_periods=1).mean() + loss = loss.replace(0, np.nan) # tránh chia cho 0 + rs = gain / loss + rsi = 100 - (100 / (1 + rs)) + rsi = rsi.replace([np.inf, -np.inf], np.nan) + return rsi + +@router.get("/candles") +def get_candles( + userId: str = Query(..., description="ID của user để lấy API key/secret"), + symbol: str = Query(..., description="Mã giao dịch, ví dụ: BTCUSDT"), + interval: str = Query("1", description="Khung thời gian nến, ví dụ: 1, 3, 5, 15, 30, 60, 240, D, W, M"), + limit: Optional[int] = Query(200, description="Số lượng nến trả về (tối đa 1000)"), + start: Optional[int] = Query(None, description="Timestamp bắt đầu (miliseconds)"), + end: Optional[int] = Query(None, description="Timestamp kết thúc (miliseconds)") +): + api_key, api_secret = get_user_api(userId) + client = get_bybit_client(api_key, api_secret) + params = { + "category": "linear", # hoặc "spot" nếu lấy spot + "symbol": symbol, + "interval": interval, + "limit": limit + } + if start: + params["start"] = start + if end: + params["end"] = end + res = client.get_kline(**params) + if 'retCode' in res and res['retCode'] != 0: + raise HTTPException(status_code=400, detail=f"Bybit API error: {res.get('retMsg', 'Unknown error')}") + candles = res.get('result', {}).get('list', []) + if not candles: + return {"data": [], "indicators": {}} + # Chuyển đổi sang DataFrame + df = pd.DataFrame(candles, columns=[ + "timestamp", "open", "high", "low", "close", "volume", "turnover" + ]) + df = df.astype({ + "timestamp": np.int64, + "open": np.float64, + "high": np.float64, + "low": np.float64, + "close": np.float64, + "volume": np.float64, + "turnover": np.float64 + }) + df = df.sort_values("timestamp", ascending=False) + # Tính chỉ báo + close = df["close"] + macd_line, macd_signal, macd_hist = macd(close, fast=45, slow=90, signal=9) + df["macd"] = macd_line + df["macdsignal"] = macd_signal + df["macdhist"] = macd_hist + df["ema34"] = ema(close, 34) + df["ema50"] = ema(close, 50) + df["ema100"] = ema(close, 100) + df["ema200"] = ema(close, 200) + df["rsi"] = rsi(close) + # Trả về kết quả + data = df.replace({np.nan: None}).to_dict(orient="records") + return { + "data": data, + "indicators": { + "macd": {"fast": 45, "slow": 90, "signal": 9}, + "ema": [34, 50, 100, 200], + "rsi": True + } + } diff --git a/app/api/orders.py b/app/api/orders.py new file mode 100644 index 0000000..03b9fbc --- /dev/null +++ b/app/api/orders.py @@ -0,0 +1,78 @@ +from fastapi import APIRouter, Query, HTTPException, Body +from typing import Optional +from app.services.bybit_service import get_bybit_client +import json +import os + +router = APIRouter() + +ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json') + +def get_user_api(user_id: str): + try: + with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f: + accounts = json.load(f) + for acc in accounts: + if acc['id'] == user_id: + return acc['bybit_api_key'], acc['bybit_api_secret'] + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}") + raise HTTPException(status_code=404, detail="Không tìm thấy userId") + +@router.get("/orders") +def get_orders( + userId: str = Query(..., description="ID của user để lấy API key/secret"), + symbol: str = Query(..., description="Mã giao dịch, ví dụ: BTCUSDT"), + category: str = Query("linear", description="Loại lệnh: linear (future) hoặc spot") +): + api_key, api_secret = get_user_api(userId) + client = get_bybit_client(api_key, api_secret) + res = client.get_open_orders( + category=category, # "linear" cho future, "spot" cho spot + symbol=symbol + ) + return res + +# Submit order +@router.post("/orders") +def submit_order( + userId: str = Body(..., embed=True, description="ID của user để lấy API key/secret"), + symbol: str = Body(..., embed=True, description="Mã giao dịch, ví dụ: BTCUSDT"), + side: str = Body(..., embed=True, description="side: Buy hoặc Sell"), + orderType: str = Body(..., embed=True, description="Loại lệnh: Market hoặc Limit"), + qty: float = Body(..., embed=True, description="Số lượng"), + category: str = Body("linear", embed=True, description="Loại lệnh: linear (future) hoặc spot"), + price: Optional[float] = Body(None, embed=True, description="Giá (bắt buộc với Limit)") +): + api_key, api_secret = get_user_api(userId) + client = get_bybit_client(api_key, api_secret) + params = { + "category": category, + "symbol": symbol, + "side": side, + "orderType": orderType, + "qty": qty + } + if orderType.lower() == "limit": + if price is None: + raise HTTPException(status_code=400, detail="Thiếu giá cho lệnh Limit") + params["price"] = price + res = client.place_order(**params) + return res + +# Cancel order +@router.delete("/orders/{order_id}") +def cancel_order( + order_id: str, + userId: str = Query(..., description="ID của user để lấy API key/secret"), + symbol: str = Query(..., description="Mã giao dịch, ví dụ: BTCUSDT"), + category: str = Query("linear", description="Loại lệnh: linear (future) hoặc spot") +): + api_key, api_secret = get_user_api(userId) + client = get_bybit_client(api_key, api_secret) + res = client.cancel_order( + category=category, + symbol=symbol, + orderId=order_id + ) + return res diff --git a/app/api/positions.py b/app/api/positions.py new file mode 100644 index 0000000..f5e7548 --- /dev/null +++ b/app/api/positions.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, Query, HTTPException +from typing import Optional +from app.services.bybit_service import get_bybit_client +import json +import os + +router = APIRouter() + +ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json') + +def get_user_api(user_id: str): + try: + with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f: + accounts = json.load(f) + for acc in accounts: + if acc['id'] == user_id: + return acc['bybit_api_key'], acc['bybit_api_secret'] + except Exception as e: + raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}") + raise HTTPException(status_code=404, detail="Không tìm thấy userId") + +@router.get("/positions") +def get_positions( + userId: str = Query(..., description="ID của user để lấy API key/secret"), + symbol: Optional[str] = Query(None, description="Mã giao dịch, ví dụ: BTCUSDT"), + category: str = Query("linear", description="Loại lệnh: linear (future) hoặc spot") +): + api_key, api_secret = get_user_api(userId) + client = get_bybit_client(api_key, api_secret) + params = { + "category": category + } + if symbol: + params["symbol"] = symbol + res = client.get_positions(**params) + return res \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..247365d --- /dev/null +++ b/app/config.py @@ -0,0 +1,9 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +SUPABASE_URL = os.getenv("SUPABASE_URL") +SUPABASE_KEY = os.getenv("SUPABASE_KEY") +BYBIT_API_KEY = os.getenv("BYBIT_API_KEY") +BYBIT_API_SECRET = os.getenv("BYBIT_API_SECRET") diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..166a6bb --- /dev/null +++ b/app/main.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from app.api import candles, orders, account, positions + +app = FastAPI() + +app.include_router(candles.router, prefix="/api") +app.include_router(orders.router, prefix="/api") +app.include_router(account.router, prefix="/api") +app.include_router(positions.router, prefix="/api") diff --git a/app/services/bybit_service.py b/app/services/bybit_service.py new file mode 100644 index 0000000..7bbfd22 --- /dev/null +++ b/app/services/bybit_service.py @@ -0,0 +1,8 @@ +from pybit.unified_trading import HTTP + +def get_bybit_client(api_key: str, api_secret: str): + return HTTP( + testnet=False, # Đổi thành False nếu dùng môi trường thật + api_key=api_key, + api_secret=api_secret + ) diff --git a/app/services/supabase_service.py b/app/services/supabase_service.py new file mode 100644 index 0000000..3a45cee --- /dev/null +++ b/app/services/supabase_service.py @@ -0,0 +1,5 @@ +from supabase import create_client, Client +from app.config import SUPABASE_URL, SUPABASE_KEY + +def get_supabase_client() -> Client: + return create_client(SUPABASE_URL, SUPABASE_KEY) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e12f7d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +supabase +pybit +python-dotenv