update refactor
This commit is contained in:
parent
01abc7e446
commit
55ad0607f3
39 changed files with 4304 additions and 584 deletions
37
.gitignore
vendored
37
.gitignore
vendored
|
|
@ -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
|
||||||
20
Dockerfile
20
Dockerfile
|
|
@ -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"]
|
|
||||||
14
README.md
14
README.md
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -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}")
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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}")
|
|
||||||
|
|
@ -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")
|
|
||||||
21
app/main.py
21
app/main.py
|
|
@ -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")
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
|
|
@ -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
15
example.env
Normal 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
94
openapi.yaml
Normal 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
3710
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
39
package.json
Normal file
39
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
8
src/api/candles.ts
Normal 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
10
src/api/orders.ts
Normal 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
8
src/api/positions.ts
Normal 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
9
src/api/user.ts
Normal 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
27
src/app.ts
Normal 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
0
src/dao/analysis.ts
Normal file
10
src/dao/candles.ts
Normal file
10
src/dao/candles.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
export interface Candle {
|
||||||
|
time: string;
|
||||||
|
timestamp: number;
|
||||||
|
open: number;
|
||||||
|
high: number;
|
||||||
|
low: number;
|
||||||
|
close: number;
|
||||||
|
volume: number;
|
||||||
|
turnover: number;
|
||||||
|
}
|
||||||
11
src/handlers/candleHandler.ts
Normal file
11
src/handlers/candleHandler.ts
Normal 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);
|
||||||
|
};
|
||||||
21
src/handlers/orderHandler.ts
Normal file
21
src/handlers/orderHandler.ts
Normal 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);
|
||||||
|
};
|
||||||
8
src/handlers/positionHandler.ts
Normal file
8
src/handlers/positionHandler.ts
Normal 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);
|
||||||
|
};
|
||||||
14
src/handlers/userHandler.ts
Normal file
14
src/handlers/userHandler.ts
Normal 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);
|
||||||
|
};
|
||||||
26
src/schedule/candleAnalysisSchedule.ts
Normal file
26
src/schedule/candleAnalysisSchedule.ts
Normal 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');
|
||||||
|
});
|
||||||
69
src/services/bybitService.ts
Normal file
69
src/services/bybitService.ts
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/services/indicatorService.ts
Normal file
27
src/services/indicatorService.ts
Normal 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] };
|
||||||
|
}
|
||||||
22
src/services/messageService.ts
Normal file
22
src/services/messageService.ts
Normal 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 };
|
||||||
|
}
|
||||||
16
src/services/supabaseService.ts
Normal file
16
src/services/supabaseService.ts
Normal 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
14
test.ts
Normal 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
113
tsconfig.json
Normal 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. */
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue