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
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.pyc
# Node modules
node_modules/
# Virtual env
venv/
ENV/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Env
.env
.env.*
# VSCode
.vscode/
# Jupyter
.ipynb_checkpoints
# OS
.DS_Store
Thumbs.db
# Log, data
*.log
*.sqlite3
# VSCode
.vscode/
# 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
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. */
}
}