Compare commits
No commits in common. "dbf3b758fee3866538eef22a3554789355d8c95e" and "01abc7e4466e2096c4be53d64ed5618bbf64d672" have entirely different histories.
dbf3b758fe
...
01abc7e446
39 changed files with 584 additions and 4303 deletions
37
.gitignore
vendored
37
.gitignore
vendored
|
|
@ -1,27 +1,26 @@
|
|||
# Node modules
|
||||
node_modules/
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyc
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Env
|
||||
# Virtual env
|
||||
venv/
|
||||
ENV/
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
|
||||
# Build
|
||||
/dist/
|
||||
/build/
|
||||
|
||||
# Others
|
||||
*.local
|
||||
# Log, data
|
||||
*.log
|
||||
*.sqlite3
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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,17 +124,3 @@ 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
|
||||
```
|
||||
|
|
|
|||
20
accounts.json
Normal file
20
accounts.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
34
app/api/account.py
Normal file
34
app/api/account.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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}")
|
||||
107
app/api/candles.py
Normal file
107
app/api/candles.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
78
app/api/orders.py
Normal file
78
app/api/orders.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from fastapi import APIRouter, Query, HTTPException, Body
|
||||
from typing import Optional
|
||||
from app.services.bybit_service import get_bybit_client
|
||||
import json
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json')
|
||||
|
||||
def get_user_api(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
return acc['bybit_api_key'], acc['bybit_api_secret']
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}")
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy userId")
|
||||
|
||||
@router.get("/orders", 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
|
||||
36
app/api/positions.py
Normal file
36
app/api/positions.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from fastapi import APIRouter, Query, HTTPException
|
||||
from typing import Optional
|
||||
from app.services.bybit_service import get_bybit_client
|
||||
import json
|
||||
import os
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
ACCOUNTS_FILE = os.path.join(os.path.dirname(__file__), '../../accounts.json')
|
||||
|
||||
def get_user_api(user_id: str):
|
||||
try:
|
||||
with open(ACCOUNTS_FILE, 'r', encoding='utf-8') as f:
|
||||
accounts = json.load(f)
|
||||
for acc in accounts:
|
||||
if acc['id'] == user_id:
|
||||
return acc['bybit_api_key'], acc['bybit_api_secret']
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Lỗi đọc file accounts: {e}")
|
||||
raise HTTPException(status_code=404, detail="Không tìm thấy userId")
|
||||
|
||||
@router.get("/positions", 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
|
||||
94
app/api/user/user.py
Normal file
94
app/api/user/user.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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}")
|
||||
9
app/config.py
Normal file
9
app/config.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
SUPABASE_URL = os.getenv("SUPABASE_URL")
|
||||
SUPABASE_KEY = os.getenv("SUPABASE_KEY")
|
||||
BYBIT_API_KEY = os.getenv("BYBIT_API_KEY")
|
||||
BYBIT_API_SECRET = os.getenv("BYBIT_API_SECRET")
|
||||
21
app/main.py
Normal file
21
app/main.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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")
|
||||
46
app/schemas/account.py
Normal file
46
app/schemas/account.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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")
|
||||
23
app/schemas/candle.py
Normal file
23
app/schemas/candle.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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
|
||||
8
app/services/bybit_service.py
Normal file
8
app/services/bybit_service.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from pybit.unified_trading import HTTP
|
||||
|
||||
def get_bybit_client(api_key: str, api_secret: str):
|
||||
return HTTP(
|
||||
testnet=False, # Đổi thành False nếu dùng môi trường thật
|
||||
api_key=api_key,
|
||||
api_secret=api_secret
|
||||
)
|
||||
5
app/services/supabase_service.py
Normal file
5
app/services/supabase_service.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from supabase import create_client, Client
|
||||
from app.config import SUPABASE_URL, SUPABASE_KEY
|
||||
|
||||
def get_supabase_client() -> Client:
|
||||
return create_client(SUPABASE_URL, SUPABASE_KEY)
|
||||
15
example.env
15
example.env
|
|
@ -1,15 +0,0 @@
|
|||
# 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
94
openapi.yaml
|
|
@ -1,94 +0,0 @@
|
|||
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
3710
package-lock.json
generated
File diff suppressed because it is too large
Load diff
39
package.json
39
package.json
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
56
requirements.txt
Normal file
56
requirements.txt
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
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
|
||||
9
run-docker.sh
Normal file
9
run-docker.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Router } from 'express';
|
||||
import * as candleHandler from '../handlers/candleHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/:symbol/:interval', candleHandler.analyzeCandles);
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { Router } from 'express';
|
||||
import * as positionHandler from '../handlers/positionHandler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', positionHandler.listPositions);
|
||||
|
||||
export default router;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
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
27
src/app.ts
|
|
@ -1,27 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
export interface Candle {
|
||||
time: string;
|
||||
timestamp: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
turnover: number;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
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);
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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);
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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);
|
||||
};
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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);
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
||||
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');
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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] };
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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 };
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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
14
test.ts
|
|
@ -1,14 +0,0 @@
|
|||
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
113
tsconfig.json
|
|
@ -1,113 +0,0 @@
|
|||
{
|
||||
"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