update refactor

This commit is contained in:
KienVT9 2025-07-08 18:03:36 +07:00
parent 01abc7e446
commit 55ad0607f3
39 changed files with 4304 additions and 584 deletions

37
.gitignore vendored
View file

@ -1,26 +1,27 @@
# Python # Node modules
__pycache__/ node_modules/
*.py[cod]
*.pyo
*.pyd
*.pyc
# Virtual env # Logs
venv/ logs/
ENV/ *.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Env
.env .env
.env.* .env.*
# VSCode
.vscode/
# Jupyter
.ipynb_checkpoints
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Log, data # VSCode
*.log .vscode/
*.sqlite3
# Build
/dist/
/build/
# Others
*.local

View file

@ -1,20 +0,0 @@
FROM python:3.10-slim
# Cài đặt các thư viện hệ thống cần thiết
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
# Tạo thư mục app
WORKDIR /app
# Copy requirements và cài đặt
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Copy toàn bộ mã nguồn
COPY . .
# Expose port
EXPOSE 8000
# Lệnh chạy uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View file

@ -124,3 +124,17 @@ For open source projects, say how it is licensed.
## Project status ## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
# AI Trading System (NodeJS)
## Hướng dẫn cài đặt
```bash
npm install
```
## Chạy server
```bash
node src/app.js
```

View file

@ -1,20 +0,0 @@
[
{
"id": "user1",
"bybit_api_key": "your_bybit_api_key_1",
"bybit_api_secret": "your_bybit_api_secret_1",
"user_name": "Nguyen Van A"
},
{
"id": "user2",
"bybit_api_key": "your_bybit_api_key_2",
"bybit_api_secret": "your_bybit_api_secret_2",
"user_name": "Tran Thi B"
},
{
"id": "1",
"bybit_api_key": "dqGCPAJzLKoTRfgCMq",
"bybit_api_secret": "aDYziLWN2jvdWNuz4QhWrM1O65JZ5f1NtEUO",
"user_name": "Key-test"
}
]

View file

@ -1,34 +0,0 @@
from fastapi import APIRouter, Query, HTTPException
from app.schemas.account import AccountResponseSchema
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("/account", response_model=AccountResponseSchema, tags=["Account"])
def get_account_info(userId: str = Query(..., description="User ID to get 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"System error: {e}")

View file

@ -1,107 +0,0 @@
from fastapi import APIRouter, Query, HTTPException
from typing import Optional
from app.services.bybit_service import get_bybit_client
from app.schemas.candle import CandleResponseSchema
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", response_model=CandleResponseSchema, response_model_exclude_none=True, tags=["Candle"])
def get_candles(
userId: str = Query(..., description="User ID to get API key/secret"),
symbol: str = Query(..., description="Trading symbol, e.g. BTCUSDT"),
interval: str = Query("1", description="Candle interval, e.g. 1, 3, 5, 15, 30, 60, 240, D, W, M"),
limit: Optional[int] = Query(200, description="Number of candles to return (max 1000)"),
start: Optional[int] = Query(None, description="Start timestamp (milliseconds)"),
end: Optional[int] = Query(None, description="End timestamp (milliseconds)")
):
api_key, api_secret = get_user_api(userId)
client = get_bybit_client(api_key, api_secret)
params = {
"category": "linear", # or "spot" for 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": {}}
# Convert to 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")
# Calculate indicators
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)
# Return result
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
}
}

View file

@ -1,78 +0,0 @@
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", tags=["Order"])
def get_orders(
userId: str = Query(..., description="User ID to get API key/secret"),
symbol: str = Query(..., description="Trading symbol, e.g. BTCUSDT"),
category: str = Query("linear", description="Order type: linear (future) or 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" for future, "spot" for spot
symbol=symbol
)
return res
# Submit order
@router.post("/orders", tags=["Order"])
def submit_order(
userId: str = Body(..., embed=True, description="User ID to get API key/secret"),
symbol: str = Body(..., embed=True, description="Trading symbol, e.g. BTCUSDT"),
side: str = Body(..., embed=True, description="Order side: Buy or Sell"),
orderType: str = Body(..., embed=True, description="Order type: Market or Limit"),
qty: float = Body(..., embed=True, description="Order quantity"),
category: str = Body("linear", embed=True, description="Order type: linear (future) or spot"),
price: Optional[float] = Body(None, embed=True, description="Order price (required for 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="Missing price for Limit order")
params["price"] = price
res = client.place_order(**params)
return res
# Cancel order
@router.delete("/orders/{order_id}", tags=["Order"])
def cancel_order(
order_id: str,
userId: str = Query(..., description="User ID to get API key/secret"),
symbol: str = Query(..., description="Trading symbol, e.g. BTCUSDT"),
category: str = Query("linear", description="Order type: linear (future) or 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

View file

@ -1,36 +0,0 @@
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", tags=["Position"])
def get_positions(
userId: str = Query(..., description="User ID to get API key/secret"),
symbol: Optional[str] = Query(None, description="Trading symbol, e.g. BTCUSDT"),
category: str = Query("linear", description="Order type: linear (future) or 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

View file

@ -1,94 +0,0 @@
from fastapi import APIRouter, Query, HTTPException, Body
import json
import os
from typing import Optional
router = APIRouter()
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../../accounts.json')
# API lấy danh sách user
@router.get("/users", tags=["User"])
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"Error reading accounts file: {e}")
# API thêm user
@router.post("/users", tags=["User"])
def add_user(
id: str = Body(..., description="User ID"),
bybit_api_key: str = Body(..., description="Bybit API Key"),
bybit_api_secret: str = Body(..., description="Bybit API Secret"),
user_name: str = Body(..., description="User name")
):
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 already exists")
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": "User added successfully"}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error writing accounts file: {e}")
# API sửa user
@router.put("/users/{user_id}", tags=["User"])
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": "User updated successfully"}
raise HTTPException(status_code=404, detail="User ID not found")
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error writing accounts file: {e}")
# API xóa user
@router.delete("/users/{user_id}", tags=["User"])
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="User ID not found")
f.seek(0)
json.dump(new_accounts, f, ensure_ascii=False, indent=2)
f.truncate()
return {"message": "User deleted successfully"}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error writing accounts file: {e}")

View file

@ -1,9 +0,0 @@
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")

View file

@ -1,21 +0,0 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api import candles, orders, account, positions
from app.api.user import user
app = FastAPI()
# CORS config: allow all origins (for development)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
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")
app.include_router(user.router, prefix="/api")

View file

@ -1,46 +0,0 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
class CoinInfo(BaseModel):
availableToBorrow: Optional[str] = Field(None, description="Amount available to borrow")
bonus: Optional[str] = Field(None, description="Bonus amount")
accruedInterest: Optional[str] = Field(None, description="Accrued interest")
availableToWithdraw: Optional[str] = Field(None, description="Amount available to withdraw")
totalOrderIM: Optional[str] = Field(None, description="Initial margin for orders")
equity: Optional[str] = Field(None, description="Total coin equity")
totalPositionMM: Optional[str] = Field(None, description="Maintenance margin for positions")
usdValue: Optional[str] = Field(None, description="USD value of the coin")
unrealisedPnl: Optional[str] = Field(None, description="Unrealized PnL")
collateralSwitch: Optional[bool] = Field(None, description="Is used as collateral")
spotHedgingQty: Optional[str] = Field(None, description="Spot hedging quantity")
borrowAmount: Optional[str] = Field(None, description="Borrowed amount")
totalPositionIM: Optional[str] = Field(None, description="Initial margin for positions")
walletBalance: Optional[str] = Field(None, description="Coin wallet balance")
cumRealisedPnl: Optional[str] = Field(None, description="Cumulative realized PnL")
locked: Optional[str] = Field(None, description="Locked amount")
marginCollateral: Optional[bool] = Field(None, description="Is used as margin collateral")
coin: str = Field(..., description="Coin symbol")
class AccountInfo(BaseModel):
totalEquity: str = Field(..., description="Total equity in USD")
accountIMRate: str = Field(..., description="Initial margin rate")
totalMarginBalance: str = Field(..., description="Total margin balance")
totalInitialMargin: str = Field(..., description="Total initial margin")
accountType: str = Field(..., description="Account type (e.g. UNIFIED)")
totalAvailableBalance: str = Field(..., description="Total available balance")
accountMMRate: str = Field(..., description="Maintenance margin rate")
totalPerpUPL: str = Field(..., description="Unrealized PnL of perpetual positions")
totalWalletBalance: str = Field(..., description="Total wallet balance")
accountLTV: str = Field(..., description="Loan to value")
totalMaintenanceMargin: str = Field(..., description="Total maintenance margin")
coin: List[CoinInfo] = Field(..., description="List of coins")
class ResultInfo(BaseModel):
list: List[AccountInfo] = Field(..., description="List of accounts")
class AccountResponseSchema(BaseModel):
retCode: int = Field(..., description="Return code (0 means success)")
retMsg: str = Field(..., description="Return message")
result: ResultInfo = Field(..., description="Detailed result")
retExtInfo: Dict[str, Any] = Field(..., description="Extra information")
time: int = Field(..., description="Response timestamp")

View file

@ -1,23 +0,0 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Any
class CandleSchema(BaseModel):
timestamp: int = Field(..., description="Timestamp (milliseconds)")
open: float = Field(..., description="Open price")
high: float = Field(..., description="High price")
low: float = Field(..., description="Low price")
close: float = Field(..., description="Close price")
volume: float = Field(..., description="Volume")
turnover: float = Field(..., description="Turnover value")
macd: Optional[float] = Field(None, description="MACD value")
macdsignal: Optional[float] = Field(None, description="MACD signal")
macdhist: Optional[float] = Field(None, description="MACD histogram")
ema34: Optional[float] = Field(None, description="EMA 34")
ema50: Optional[float] = Field(None, description="EMA 50")
ema100: Optional[float] = Field(None, description="EMA 100")
ema200: Optional[float] = Field(None, description="EMA 200")
rsi: Optional[float] = Field(None, description="RSI")
class CandleResponseSchema(BaseModel):
data: List[CandleSchema]
indicators: Any

View file

@ -1,8 +0,0 @@
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
)

View file

@ -1,5 +0,0 @@
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)

15
example.env Normal file
View file

@ -0,0 +1,15 @@
# Supabase
SUPABASE_URL=https://tqsoqmohvzrbnwhvgczh.supabase.co
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRxc29xbW9odnpyYm53aHZnY3poIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE4Nzc5MzYsImV4cCI6MjA2NzQ1MzkzNn0._JXiBlYiHBmINzEr61Pzm6_GKCJJnOkvv4EUBNxfSIs
LARK_APP_ID=cli_a8b73c970679d028
LARK_APP_SECRET=fRfGwMieW59Q1B5KTyNKNh4oP1YWuQOr
# Bybit (nếu muốn dùng mặc định, có thể để trống nếu lấy theo user)
BYBIT_API_KEY=your-bybit-api-key
BYBIT_API_SECRET=your-bybit-api-secret
BYBIT_TESTNET=true
# Lark webhook
LARK_WEBHOOK_URL=https://open.larksuite.com/webhook/your-webhook-url
# Server port
PORT=3000

94
openapi.yaml Normal file
View file

@ -0,0 +1,94 @@
openapi: 3.0.0
info:
title: AI Trading System API
version: 1.0.0
paths:
/api/user/credential:
post:
summary: Lưu credential Bybit cho user
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
userId:
type: string
apiKey:
type: string
apiSecret:
type: string
responses:
'200':
description: Thành công
get:
summary: Lấy credential Bybit theo userId
parameters:
- in: path
name: userId
required: true
schema:
type: string
responses:
'200':
description: Thành công
/api/orders:
post:
summary: Tạo order mới
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
symbol:
type: string
side:
type: string
qty:
type: number
responses:
'200':
description: Thành công
get:
summary: Lấy danh sách order
responses:
'200':
description: Thành công
/api/orders/{orderId}:
delete:
summary: Huỷ order
parameters:
- in: path
name: orderId
required: true
schema:
type: string
responses:
'200':
description: Thành công
/api/positions:
get:
summary: Lấy vị thế hiện tại
responses:
'200':
description: Thành công
/api/candles/{symbol}/{interval}:
get:
summary: Phân tích nến với EMA, MACD
parameters:
- in: path
name: symbol
required: true
schema:
type: string
- in: path
name: interval
required: true
schema:
type: string
responses:
'200':
description: Thành công

3710
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

39
package.json Normal file
View file

@ -0,0 +1,39 @@
{
"name": "ai-trading-sys",
"version": "1.0.0",
"description": "## Yêu cầu - Python 3.8+ - Tài khoản Supabase - API key Bybit",
"main": "src/app.ts",
"type": "commonjs",
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.52.0",
"@supabase/supabase-js": "^2.39.7",
"axios": "^1.6.7",
"bybit-api": "^3.2.0",
"dotenv": "^17.0.1",
"express": "^4.18.2",
"node-schedule": "^2.1.1",
"swagger-ui-express": "^5.0.1",
"technicalindicators": "^3.0.0",
"yamljs": "^0.3.0"
},
"scripts": {
"start": "ts-node src/app.ts",
"build": "tsc",
"dev": "nodemon --watch src --ext ts --exec ts-node src/app.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/axios": "^0.14.4",
"@types/express": "^5.0.3",
"@types/node": "^24.0.10",
"@types/node-schedule": "^2.1.7",
"@types/swagger-ui-express": "^4.1.8",
"@types/yamljs": "^0.2.34",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}

View file

@ -1,56 +0,0 @@
aiohappyeyeballs==2.6.1
aiohttp==3.11.18
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.9.0
attrs==25.3.0
certifi==2025.4.26
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
deprecation==2.1.0
fastapi==0.115.12
frozenlist==1.6.0
gotrue==2.12.0
h11==0.16.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.9
httpx==0.28.1
hyperframe==6.1.0
idna==3.10
iniconfig==2.1.0
multidict==6.4.4
numpy==2.2.6
packaging==25.0
pandas==2.2.3
pluggy==1.6.0
postgrest==1.0.2
propcache==0.3.1
pybit==5.10.1
pycryptodome==3.23.0
pydantic==2.11.4
pydantic_core==2.33.2
PyJWT==2.10.1
pytest==8.3.5
pytest-mock==3.14.0
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
pytz==2025.2
realtime==2.4.3
requests==2.32.3
six==1.17.0
sniffio==1.3.1
starlette==0.46.2
storage3==0.11.3
StrEnum==0.4.15
supabase==2.15.1
supafunc==0.9.4
typing-inspection==0.4.1
typing_extensions==4.13.2
tzdata==2025.2
urllib3==2.4.0
uvicorn==0.34.2
websocket-client==1.8.0
websockets==14.2
yarl==1.20.0

View file

@ -1,9 +0,0 @@
#!/bin/bash
# Remove old container if exists
if [ $(docker ps -a -q -f name=ai-trading-sys) ]; then
docker rm -f ai-trading-sys
fi
docker build -t ai-trading-sys .
docker run -d -p 5100:8000 --name ai-trading-sys ai-trading-sys

8
src/api/candles.ts Normal file
View file

@ -0,0 +1,8 @@
import { Router } from 'express';
import * as candleHandler from '../handlers/candleHandler';
const router = Router();
router.get('/:symbol/:interval', candleHandler.analyzeCandles);
export default router;

10
src/api/orders.ts Normal file
View file

@ -0,0 +1,10 @@
import { Router } from 'express';
import * as orderHandler from '../handlers/orderHandler';
const router = Router();
router.post('/', orderHandler.createOrder);
router.get('/', orderHandler.listOrders);
router.delete('/:orderId', orderHandler.cancelOrder);
export default router;

8
src/api/positions.ts Normal file
View file

@ -0,0 +1,8 @@
import { Router } from 'express';
import * as positionHandler from '../handlers/positionHandler';
const router = Router();
router.get('/', positionHandler.listPositions);
export default router;

9
src/api/user.ts Normal file
View file

@ -0,0 +1,9 @@
import { Router } from 'express';
import * as userHandler from '../handlers/userHandler';
const router = Router();
router.post('/credential', userHandler.saveCredential);
router.get('/credential/:userId', userHandler.getCredential);
export default router;

27
src/app.ts Normal file
View file

@ -0,0 +1,27 @@
import 'dotenv/config';
import express, { Application } from 'express';
import userApi from './api/user';
import ordersApi from './api/orders';
import positionsApi from './api/positions';
import candlesApi from './api/candles';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import './schedule/candleAnalysisSchedule';
const app: Application = express();
app.use(express.json());
app.use('/api/user', userApi);
app.use('/api/orders', ordersApi);
app.use('/api/positions', positionsApi);
app.use('/api/candles', candlesApi);
const swaggerDocument = YAML.load('./openapi.yaml');
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
export default app;

0
src/dao/analysis.ts Normal file
View file

10
src/dao/candles.ts Normal file
View file

@ -0,0 +1,10 @@
export interface Candle {
time: string;
timestamp: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
turnover: number;
}

View file

@ -0,0 +1,11 @@
import { Request, Response } from 'express';
import { BybitService } from '../services/bybitService';
import * as indicatorService from '../services/indicatorService';
export const analyzeCandles = async (req: Request, res: Response) => {
const { symbol, interval } = req.params;
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
const candles = await bybitService.getCandles({ symbol, interval: '5', category: 'linear', limit: 200 });
const analysis = indicatorService.analyze(candles);
res.json(analysis);
};

View file

@ -0,0 +1,21 @@
import { Request, Response } from 'express';
import { BybitService } from '../services/bybitService';
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
export const createOrder = async (req: Request, res: Response) => {
// TODO: Lấy client từ credential
const order = await bybitService.createOrder(req.body);
res.json(order);
};
export const listOrders = async (req: Request, res: Response) => {
const orders = await bybitService.listOrders({ category: 'linear' });
res.json(orders);
};
export const cancelOrder = async (req: Request, res: Response) => {
const { orderId } = req.params;
const result = await bybitService.cancelOrder({ orderId, symbol: 'BTCUSDT', category: 'linear' });
res.json(result);
};

View file

@ -0,0 +1,8 @@
import { Request, Response } from 'express';
import { BybitService } from '../services/bybitService';
export const listPositions = async (req: Request, res: Response) => {
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!);
const positions = await bybitService.listPositions({ category: 'linear' });
res.json(positions);
};

View file

@ -0,0 +1,14 @@
import { Request, Response } from 'express';
import * as supabaseService from '../services/supabaseService';
export const saveCredential = async (req: Request, res: Response) => {
const { userId, apiKey, apiSecret } = req.body;
const result = await supabaseService.saveBybitCredential(userId, apiKey, apiSecret);
res.json(result);
};
export const getCredential = async (req: Request, res: Response) => {
const { userId } = req.params;
const result = await supabaseService.getBybitCredential(userId);
res.json(result);
};

View file

@ -0,0 +1,26 @@
import schedule from 'node-schedule';
import * as indicatorService from '../services/indicatorService';
import { BybitService } from '../services/bybitService';
import { KlineIntervalV3 } from 'bybit-api';
import { sendLarkMessage } from '../services/messageService';
// Hàm thực hiện phân tích nến
async function analyzeCandlesJob(symbol: string, interval: KlineIntervalV3) {
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
// TODO: Lấy client thật nếu cần
const candles = await bybitService.getCandles({ symbol, interval, category: 'linear', limit: 200 });
const analysis = indicatorService.analyze(candles);
console.log(`[${new Date().toISOString()}] Phân tích nến ${symbol} ${interval}:`, analysis);
await sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', JSON.stringify(analysis));
// Có thể gửi kết quả qua Lark, lưu DB, ...
}
// Lập lịch chạy vào 00:00 mỗi ngày
const rule = new schedule.RecurrenceRule();
rule.minute = [4, 9, 14, 19, 24, 29, 34, 39, 44, 49, 54, 59];
rule.second = 59;
schedule.scheduleJob(rule, () => {
// Có thể lặp qua nhiều symbol/interval nếu muốn
analyzeCandlesJob('BTCUSDT', '5');
});

View file

@ -0,0 +1,69 @@
import { CancelOrderParamsV5, GetAccountOrdersParamsV5, GetKlineParamsV5, OrderParamsV5, RestClientV5 } from 'bybit-api';
import { Candle } from '../dao/candles';
export class BybitService {
private client: RestClientV5;
constructor(apiKey: string, apiSecret: string, testnet: boolean = true) {
this.client = new RestClientV5({
key: apiKey,
secret: apiSecret,
testnet
});
}
async createOrder(orderData: OrderParamsV5): Promise<any> {
try {
const res = await this.client.submitOrder(orderData);
return { success: true, data: res };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async listOrders(params: GetAccountOrdersParamsV5): Promise<any[]> {
try {
const res = await this.client.getActiveOrders(params);
return res.result?.list || [];
} catch (error: any) {
return [];
}
}
async cancelOrder(params: CancelOrderParamsV5): Promise<any> {
try {
const res = await this.client.cancelOrder(params);
return { success: true, data: res };
} catch (error: any) {
return { success: false, error: error.message };
}
}
async listPositions(params: any): Promise<any[]> {
try {
const res = await this.client.getPositionInfo(params);
return res.result?.list || [];
} catch (error: any) {
return [];
}
}
async getCandles(params: GetKlineParamsV5): Promise<Candle[]> {
try {
const res = await this.client.getKline(params);
const list = res.result?.list || [];
// Bybit trả về: [timestamp, open, high, low, close, volume, turnover]
return list.map((item: any[]) => ({
time: new Date(Number(item[0])).toISOString(),
timestamp: Number(item[0]),
open: Number(item[1]),
high: Number(item[2]),
low: Number(item[3]),
close: Number(item[4]),
volume: Number(item[5]),
turnover: Number(item[6])
}));
} catch (error: any) {
return [];
}
}
}

View file

@ -0,0 +1,27 @@
import { EMA, MACD } from 'technicalindicators';
import { Candle } from '../dao/candles';
export function analyze(candles: Candle[]): { ema34: number; ema200: number; macd: any } {
let close = candles.map(c => c.close);
const ema34 = EMA.calculate({ period: 34, values: close, reversedInput: true });
const ema200 = EMA.calculate({ period: 200, values: close, reversedInput: true });
const macd = MACD.calculate({
values: close,
fastPeriod: 45,
slowPeriod: 90,
signalPeriod: 9,
SimpleMAOscillator: false,
SimpleMASignal: false,
reversedInput: true
});
const candle = candles[0];
if (candle.low < ema200[0] && candle.high > ema200[0] && candle.close > ema200[0]) {
console.log('Buy');
}
if (candle.high > ema200[0] && candle.low < ema200[0] && candle.close < ema200[0]) {
console.log('Sell');
}
return { ema34: ema34[0], ema200: ema200[0], macd: macd[0] };
}

View file

@ -0,0 +1,22 @@
import { Client } from '@larksuiteoapi/node-sdk';
export async function sendLarkMessage(receiveId: string, message: string): Promise<any> {
console.log(process.env.LARK_APP_ID!, process.env.LARK_APP_SECRET!);
const lark = new Client({
appId: process.env.LARK_APP_ID!,
appSecret: process.env.LARK_APP_SECRET!
});
const res = await lark.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: receiveId,
content: JSON.stringify({text: message}),
msg_type: 'text'
}
});
return { success: true, data: res.data };
}

View file

@ -0,0 +1,16 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
const supabase: SupabaseClient = createClient(
process.env.SUPABASE_URL || '',
process.env.SUPABASE_KEY || ''
);
export async function saveBybitCredential(userId: string, apiKey: string, apiSecret: string): Promise<any> {
// TODO: Lưu credential vào Supabase
return { success: true, message: 'Saved (mock)' };
}
export async function getBybitCredential(userId: string): Promise<any> {
// TODO: Lấy credential từ Supabase
return { userId, apiKey: 'demo', apiSecret: 'demo' };
}

14
test.ts Normal file
View file

@ -0,0 +1,14 @@
import 'dotenv/config';
import { BybitService } from './src/services/bybitService';
import * as indicatorService from './src/services/indicatorService';
import { sendLarkMessage } from './src/services/messageService';
const bybitService = new BybitService(process.env.BYBIT_API_KEY!, process.env.BYBIT_API_SECRET!, false);
(async () => {
const candles = await bybitService.getCandles({ symbol: 'BTCUSDT', interval: '15', category: 'linear', limit: 500 });
console.log(candles[0], candles[1]);
const analysis = indicatorService.analyze(candles);
console.log(analysis);
const message = JSON.stringify(analysis);
await sendLarkMessage('oc_f9b2e8f0309ecab0c94e3e134b0ddd29', message);
})();

113
tsconfig.json Normal file
View file

@ -0,0 +1,113 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}