Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0dfedca
Create .env.example for environment variable setup
Akram-alhaddad Mar 15, 2026
8634cb3
Update .gitignore to streamline ignored files
Akram-alhaddad Mar 15, 2026
80cfc76
Add requirements.txt with project dependencies
Akram-alhaddad Mar 15, 2026
7412845
Add market_stream.py for WebSocket data handling
Akram-alhaddad Mar 15, 2026
0a38304
Add CandleBuilder class for processing market data
Akram-alhaddad Mar 15, 2026
e24cbd1
Create FastAPI application with WebSocket and CORS
Akram-alhaddad Mar 15, 2026
5bea862
Add prediction model loading and prediction functions
Akram-alhaddad Mar 15, 2026
550b722
Switch base image to Python and configure app
Akram-alhaddad Mar 15, 2026
18a85b9
Update Dockerfile
Akram-alhaddad Mar 16, 2026
0d1be4f
Update requirements.txt
Akram-alhaddad Mar 16, 2026
524ccf4
Switch to Maven build and Java runtime in Dockerfile
Akram-alhaddad Mar 16, 2026
15fc6bb
Create docker-image.yml
Akram-alhaddad Mar 16, 2026
666719c
Create maven-publish.yml
Akram-alhaddad Mar 16, 2026
d3c4639
Create python-package-conda.yml
Akram-alhaddad Mar 16, 2026
356d596
Delete .github/workflows directory
Akram-alhaddad Mar 16, 2026
aeebbd6
Change base image to eclipse-temurin:17-jre
Akram-alhaddad Mar 16, 2026
bbda809
Update Dockerfile for build and runtime configurations
Akram-alhaddad Mar 16, 2026
f1d897b
Update Dockerfile
Akram-alhaddad Mar 16, 2026
26652b8
Update Dockerfile
Akram-alhaddad Mar 16, 2026
dce4199
Modify Dockerfile to include WAR file and CMD
Akram-alhaddad Mar 16, 2026
83978c4
Update Dockerfile for build and run stages
Akram-alhaddad Mar 16, 2026
795cbbe
Update Dockerfile for improved build and runtime commands
Akram-alhaddad Mar 16, 2026
2cea4fa
Update Dockerfile comments and formatting
Akram-alhaddad Mar 16, 2026
9e42f3d
Update Dockerfile
Akram-alhaddad Mar 16, 2026
0d35907
Refactor Dockerfile for improved caching and flexibility
Akram-alhaddad Mar 16, 2026
ac0c289
Refactor Dockerfile for improved build process
Akram-alhaddad Mar 16, 2026
a90bad9
Update Dockerfile to use Java 11 and optimize build
Akram-alhaddad Mar 16, 2026
d26c8fe
Update Dockerfile
Akram-alhaddad Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# .env.example — نموذج متغيرات البيئة (لا يحتوي أسراراً)
# ضع القيم الحقيقية في Render -> Environment (أو ملف .env محلياً)

# ---------- Database & Cache ----------
DATABASE_URL=
DB_POOL_SIZE=10
DB_TIMEOUT=30

REDIS_URL=
REDIS_TOKEN=
REDIS_CACHE_TTL=5

# ---------- Data Feed ----------
# Dukascopy (أو استخدم بروكسي dukascopy-api-websocket)
DUKASCOPY_USER=
DUKASCOPY_PASS=
DUKASCOPY_WS_URL=wss://datafeed.dukascopy.com/

# Fallback providers
TWELVEDATA_API_KEY=
OANDA_API_KEY=
OANDA_ACCOUNT_ID=

# Symbols and timeframes
SYMBOLS=XAUUSD,EURUSD,GBPUSD,USDJPY,USDCHF,AUDUSD,USDCAD,US30,NAS100,GER40,USOIL
DEFAULT_SYMBOL=XAUUSD
DEFAULT_TIMEFRAME=1m
AVAILABLE_TIMEFRAMES=1m,3m,5m,15m,30m,1h,4h,1d

# ---------- MT5/Execution (optional) ----------
BROKER_ENVIRONMENT=paper
MT5_SERVER_IP=
MT5_SERVER_PORT=
MT5_ACCOUNT_LOGIN=
MT5_ACCOUNT_PASSWORD=
MAX_RISK_PER_TRADE_PERCENT=2.0

# ---------- AI Engine ----------
MODEL_ENABLED=true
MODEL_TYPE=xgboost
MODEL_PATH=models/market_model.pkl
USE_GPU_ACCELERATION=false
CONFIDENCE_THRESHOLD=0.65
PREDICTION_INTERVAL_SECONDS=60

# ---------- API & Performance ----------
API_HOST=0.0.0.0
API_PORT=8000
API_WORKERS=2
TICK_BATCH_SIZE=500
BATCH_INTERVAL=1.0
MAX_QUEUE=20000
WORKER_THREADS=4

# ---------- Security ----------
ENV=production
CORS_ORIGINS=* # مؤقتًا أثناء التطوير؛ غيّره للـ production لاحقًا
API_KEY=
JWT_SECRET=
ENCRYPTION_KEY=
RATE_LIMIT_REQUESTS=120
RATE_LIMIT_WINDOW_SECONDS=60

# ---------- Monitoring ----------
LOG_LEVEL=INFO
SENTRY_DSN=
74 changes: 29 additions & 45 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,45 +1,29 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.mvn

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

# batch files
*.bat

# svn
.svn

# mvn
mvnw
mvnw.cmd


### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/
# Environment files
.env
.env.local
.env.production
.env.development

# Python
__pycache__/
*.pyc
.pytest_cache/

# Node / frontend
node_modules/
.next/

# Data / ML artifacts (do not upload large datasets or model weights)
data/*.csv
data/*.parquet
data/*.json
data/ticks/

models/*.pkl
models/*.h5
models/*.pt
models/*.onnx

# logs
*.log
logs/
46 changes: 40 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,40 @@
FROM openjdk:17-jdk-alpine
EXPOSE 7080
EXPOSE 7081
ARG JAR_FILE=target/dukascopy-api-websocket-1.0.war
ADD ${JAR_FILE} dukascopy-api-websocket.war
ENTRYPOINT ["java","-jar","dukascopy-api-websocket.war","--dukascopy.credential-username=username", "--dukascopy.credential-password=password"]
# ============================
# مرحلة البناء (Build) باستخدام Java 11
# ============================
FROM maven:3.8-openjdk-11 AS builder
WORKDIR /build

# نسخ ملف POM أولاً للاستفادة من التخزين المؤقت للتبعيات
COPY pom.xml .
RUN mvn dependency:go-offline -B || true

# نسخ باقي الكود المصدري
COPY src ./src

# بناء التطبيق (إنشاء ملف WAR)
RUN mvn clean package -DskipTests

# ============================
# مرحلة التشغيل (Runtime) باستخدام Eclipse Temurin (Java 11)
# ============================
FROM eclipse-temurin:11-jre
WORKDIR /app

# نسخ ملف WAR الناتج من مرحلة البناء
COPY --from=builder /build/target/*.war app.war

# تعريف المنفذ (سيتم تجاوزه بواسطة Render عبر متغير PORT)
EXPOSE 8080

# متغيرات البيئة الافتراضية (يتم استبدالها بقيم من خدمة Render)
ENV SERVER_PORT=${PORT:-8080} \
DUKE_USERNAME=${DUKASCOPY_USER} \
DUKE_PASSWORD=${DUKASCOPY_PASS} \
DB_URL=${DATABASE_URL}

# نقطة الدخول: تشغيل التطبيق مع تمرير المتغيرات
CMD java -jar app.war \
--server.port=$SERVER_PORT \
--dukascopy.credential-username=$DUKE_USERNAME \
--dukascopy.credential-password=$DUKE_PASSWORD \
--spring.datasource.url=$DB_URL
56 changes: 56 additions & 0 deletions api/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# api/main.py
import os, asyncio, json
from fastapi import FastAPI, WebSocket, Depends, HTTPException
import asyncpg
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv

load_dotenv()
app = FastAPI(title="Market API")

origins = os.getenv("CORS_ORIGINS","*")
if origins == "*":
allow_origins = ["*"]
else:
allow_origins = [o.strip() for o in origins.split(",")]

app.add_middleware(
CORSMiddleware,
allow_origins=allow_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

DB_DSN = os.getenv("DATABASE_URL")

@app.on_event("startup")
async def startup():
app.state.pool = await asyncpg.create_pool(dsn=DB_DSN, min_size=1, max_size=10)
app.state.subscribers = set()

@app.on_event("shutdown")
async def shutdown():
await app.state.pool.close()

@app.get("/api/v1/candles/{symbol}/{timeframe}")
async def get_candles(symbol: str, timeframe: str, limit: int = 200):
pool = app.state.pool
rows = await pool.fetch(
"SELECT timestamp, open, high, low, close, volume FROM candles WHERE symbol=$1 AND timeframe=$2 ORDER BY timestamp DESC LIMIT $3",
symbol, timeframe, limit
)
return [dict(r) for r in rows]

@app.websocket("/ws/market")
async def ws_market(ws: WebSocket):
await ws.accept()
app.state.subscribers.add(ws)
try:
while True:
await asyncio.sleep(60) # placeholder; actual pushes from collector via Redis/pubsub
except Exception:
pass
finally:
app.state.subscribers.remove(ws)
await ws.close()
102 changes: 102 additions & 0 deletions data_feed/market_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# data_feed/market_stream.py
import os, asyncio, json, traceback, time
from datetime import datetime, timezone
import asyncpg
import websockets
from dotenv import load_dotenv

load_dotenv()

PROXY_WS = os.getenv("PROXY_WS_URL") or os.getenv("DUKASCOPY_WS_URL")
DB_DSN = os.getenv("DATABASE_URL")
BATCH_SIZE = int(os.getenv("TICK_BATCH_SIZE", "500"))
BATCH_INTERVAL = float(os.getenv("BATCH_INTERVAL", "1.0"))
MAX_QUEUE = int(os.getenv("MAX_QUEUE", "20000"))

queue = asyncio.Queue(maxsize=MAX_QUEUE)

def now_iso():
return datetime.now(timezone.utc).isoformat()

def normalize(msg_text):
try:
d = json.loads(msg_text)
except Exception:
return None
# try many shapes
symbol = d.get("symbol") or d.get("instrument") or d.get("pair") or d.get("s")
if symbol and "/" in symbol: symbol = symbol.replace("/", "")
bid = d.get("bid") or d.get("b") or d.get("price")
ask = d.get("ask") or d.get("a") or (d.get("price") and d.get("price"))
vol = d.get("volume") or d.get("v") or d.get("size") or 0
ts = d.get("timestamp") or d.get("ts") or d.get("time") or None
try:
if ts:
ts = int(ts)
ts_iso = datetime.fromtimestamp(ts/1000, tz=timezone.utc).isoformat() if ts>10**12 else datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
else:
ts_iso = now_iso()
except:
ts_iso = now_iso()
if not symbol or (bid is None and ask is None):
return None
if bid is None: bid = ask
if ask is None: ask = bid
try:
bid = float(bid); ask = float(ask); vol = float(vol)
except:
pass
return (symbol, ts_iso, bid, ask, vol)

async def pg_pool():
return await asyncpg.create_pool(dsn=DB_DSN, min_size=1, max_size=10)

async def writer_worker(pool):
buffer = []
last_flush = time.time()
while True:
item = await queue.get()
buffer.append(item)
if len(buffer) >= BATCH_SIZE or (time.time()-last_flush)>=BATCH_INTERVAL:
records = [(r[0], r[1], r[2], r[3], r[4]) for r in buffer]
buffer = []
last_flush = time.time()
async with pool.acquire() as conn:
try:
await conn.copy_records_to_table('ticks', records=records, columns=['symbol','timestamp','bid','ask','volume'])
except Exception:
for rec in records:
try:
await conn.execute("INSERT INTO ticks(symbol,timestamp,bid,ask,volume) VALUES($1,$2,$3,$4,$5)", *rec)
except Exception as e:
print("insert err:", e)
queue.task_done()

async def ws_consumer():
pool = await pg_pool()
workers = [asyncio.create_task(writer_worker(pool)) for _ in range(2)]
backoff = 1
while True:
try:
async with websockets.connect(PROXY_WS, max_size=None) as ws:
print("connected to", PROXY_WS)
# optional subscribe message
sub = os.getenv("PROXY_SUBSCRIBE_MESSAGE")
if sub:
await ws.send(sub)
async for msg in ws:
n = normalize(msg)
if n:
try:
await queue.put(n)
except asyncio.QueueFull:
_ = queue.get_nowait() # drop oldest
await queue.put(n)
except Exception as e:
print("ws err:", e)
traceback.print_exc()
await asyncio.sleep(backoff)
backoff = min(backoff*2, 60)

if __name__ == "__main__":
asyncio.run(ws_consumer())
19 changes: 19 additions & 0 deletions prediction/predictor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# prediction/predictor.py
import os, joblib, numpy as np
from typing import Dict

MODEL_PATH = os.getenv("MODEL_PATH","models/market_model.pkl")
_model = None

def load_model():
global _model
if _model is None:
_model = joblib.load(MODEL_PATH)
return _model

def predict(features: Dict):
model = load_model()
X = np.array([features[k] for k in sorted(features.keys())]).reshape(1,-1)
proba = model.predict_proba(X) if hasattr(model, "predict_proba") else None
pred = model.predict(X)
return {"pred":float(pred[0]), "proba": proba.tolist() if proba is not None else None}
Loading