Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,1916 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OKX QUANT TERMINAL · v2 — ELITE SIGNAL ENGINE
|
| 3 |
+
Hedge-fund-style crypto market analytics dashboard built on Gradio.
|
| 4 |
+
|
| 5 |
+
Uses ONLY OKX's public (unauthenticated) market-data REST API:
|
| 6 |
+
- /api/v5/market/candles
|
| 7 |
+
- /api/v5/market/ticker
|
| 8 |
+
- /api/v5/market/tickers
|
| 9 |
+
- /api/v5/market/books
|
| 10 |
+
- /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı)
|
| 11 |
+
|
| 12 |
+
No API key, no order placement, no account access. This is a read-only
|
| 13 |
+
analytics / signal-generation terminal, not an execution bot.
|
| 14 |
+
|
| 15 |
+
v2 SİNYAL MOTORU — YENİLİKLER
|
| 16 |
+
* Yeni faktörler: Supertrend, Ichimoku bulutu, Donchian kırılımı (hacim teyitli),
|
| 17 |
+
piyasa yapısı (HH/HL – LH/LL), CMF, MFI, mum formasyonları, Bollinger/Keltner sıkışması
|
| 18 |
+
* Rejim ağırlıkları artık ADX ile SÜREKLİ (yumuşak) geçiş yapar (eski: sert eşik)
|
| 19 |
+
* Skor tanh ile ±10'a yumuşak sıkıştırılır; "GÜÇLÜ" sinyal için faktör konsensüsü + MTF şartı
|
| 20 |
+
* 2 kademeli çoklu zaman dilimi (MTF) teyidi
|
| 21 |
+
* Canlı mikro-yapı katmanı: emir defteri dengesizliği, funding rate, yakın destek/direnç
|
| 22 |
+
* Yapısal (S/R) stop + ATR stop, 3 kademeli hedef, hesap büyüklüğüne göre pozisyon boyutu
|
| 23 |
+
* Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor,
|
| 24 |
+
beklenti, R çarpanı, SQN, drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi
|
| 25 |
+
* Bileşenli GÜVEN skoru + A+ … D kalite notu, faktör katkı grafiği, sinyal işaretli fiyat grafiği
|
| 26 |
+
* "Sadece kapanmış mumlar" modu (repaint yok), veri önbelleği, paralel veri çekimi
|
| 27 |
+
* Gann HiLo Activator (trend yönü + dönüş) ve Fibonacci düzeltme seviyeleri (0.236–0.786,
|
| 28 |
+
altın bölge 0.382–0.618) artık kompozit skoru ve dolayısıyla AL/SAT sinyalini değiştirir
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
import math
|
| 32 |
+
import threading
|
| 33 |
+
import time
|
| 34 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 35 |
+
|
| 36 |
+
import numpy as np
|
| 37 |
+
import pandas as pd
|
| 38 |
+
import plotly.graph_objects as go
|
| 39 |
+
from plotly.subplots import make_subplots
|
| 40 |
+
import requests
|
| 41 |
+
import gradio as gr
|
| 42 |
+
|
| 43 |
+
try: # Hugging Face Spaces (ZeroGPU) — yerelde yoksa sessizce atla
|
| 44 |
+
import spaces
|
| 45 |
+
_gpu = spaces.GPU(duration=30)
|
| 46 |
+
except Exception:
|
| 47 |
+
def _gpu(fn):
|
| 48 |
+
return fn
|
| 49 |
+
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
# CONFIG
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
OKX_BASE = "https://www.okx.com"
|
| 54 |
+
TIMEOUT = 10
|
| 55 |
+
CACHE_TTL = 8 # saniye — aynı mum verisini sekmeler arasında tekrar çekmemek için
|
| 56 |
+
|
| 57 |
+
POPULAR_SYMBOLS = [
|
| 58 |
+
"BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT",
|
| 59 |
+
"DOGE-USDT", "ADA-USDT", "AVAX-USDT", "LINK-USDT", "TON-USDT",
|
| 60 |
+
"TRX-USDT", "DOT-USDT", "MATIC-USDT", "LTC-USDT", "NEAR-USDT",
|
| 61 |
+
"SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT",
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
BAR_OPTIONS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W"]
|
| 65 |
+
|
| 66 |
+
# approximate periods-per-year for each bar size, used to annualize risk stats
|
| 67 |
+
BAR_TO_PERIODS_PER_YEAR = {
|
| 68 |
+
"1m": 365 * 24 * 60, "3m": 365 * 24 * 20, "5m": 365 * 24 * 12,
|
| 69 |
+
"15m": 365 * 24 * 4, "30m": 365 * 24 * 2, "1H": 365 * 24,
|
| 70 |
+
"2H": 365 * 12, "4H": 365 * 6, "6H": 365 * 4, "12H": 365 * 2,
|
| 71 |
+
"1D": 365, "1W": 52,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
CHART_BG = "#0a0e14"
|
| 75 |
+
|
| 76 |
+
# her zaman dilimi için "bir üst" zaman dilimi -> çoklu zaman dilimi (MTF) teyidi için
|
| 77 |
+
MTF_MAP = {
|
| 78 |
+
"1m": "15m", "3m": "30m", "5m": "1H", "15m": "4H", "30m": "4H",
|
| 79 |
+
"1H": "4H", "2H": "1D", "4H": "1D", "6H": "1D", "12H": "1W",
|
| 80 |
+
"1D": "1W", "1W": "1W",
|
| 81 |
+
}
|
| 82 |
+
SLOW_BARS = {"4H", "6H", "12H", "1D", "1W"} # bu vadelerde emir defteri etkisi yarıya iner
|
| 83 |
+
|
| 84 |
+
# --- Sinyal motoru parametreleri ---
|
| 85 |
+
SIGNAL_THRESHOLD = 2.0 # |skor| >= bu değer -> AL / SAT
|
| 86 |
+
STRONG_THRESHOLD = 4.5 # |skor| >= bu değer + konsensüs -> GÜÇLÜ
|
| 87 |
+
WARMUP_BARS = 80 # Ichimoku (52+26) ısınması için minimum bar
|
| 88 |
+
BACKTEST_LOOKBACK = 220 # walk-forward test penceresi (bar)
|
| 89 |
+
SL_ATR, TP_ATR = 1.5, 2.5 # backtest ve varsayılan işlem planı (ATR çarpanları)
|
| 90 |
+
MAX_HOLD_BARS = 24 # zaman çıkışı
|
| 91 |
+
HILO_PERIOD = 10 # Gann HiLo Activator ortalama periyodu
|
| 92 |
+
FIB_LOOKBACK = 89 # Fibonacci swing penceresi (bar) — bir Fibonacci sayısı
|
| 93 |
+
FIB_MIN_BARS = 34 # pencere dolmadan da hesaplamaya başlamak için minimum bar
|
| 94 |
+
FIB_MIN_RANGE_ATR = 2.5 # swing aralığı bu kadar ATR'den küçükse Fib anlamsız sayılır
|
| 95 |
+
FIB_RATIOS = (0.236, 0.382, 0.5, 0.618, 0.786)
|
| 96 |
+
FEE_PER_SIDE = 0.001 # OKX spot taker ≈ %0.1 / yön
|
| 97 |
+
TP_R_MULTIPLES = (TP_ATR / SL_ATR, 2.5, 4.0) # TP1/TP2/TP3 (R cinsinden)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ---------------------------------------------------------------------------
|
| 101 |
+
# OKX PUBLIC API WRAPPERS (önbellek + yeniden deneme)
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
_CACHE = {}
|
| 104 |
+
_CACHE_LOCK = threading.Lock()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _cached(key, ttl, loader):
|
| 108 |
+
now = time.time()
|
| 109 |
+
with _CACHE_LOCK:
|
| 110 |
+
hit = _CACHE.get(key)
|
| 111 |
+
if hit and now - hit[0] < ttl:
|
| 112 |
+
return hit[1]
|
| 113 |
+
val = loader()
|
| 114 |
+
with _CACHE_LOCK:
|
| 115 |
+
_CACHE[key] = (time.time(), val)
|
| 116 |
+
if len(_CACHE) > 200: # eski kayıtları buda
|
| 117 |
+
for k in sorted(_CACHE, key=lambda x: _CACHE[x][0])[:60]:
|
| 118 |
+
_CACHE.pop(k, None)
|
| 119 |
+
return val
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _get(path, params=None, retries=2):
|
| 123 |
+
for attempt in range(retries + 1):
|
| 124 |
+
try:
|
| 125 |
+
r = requests.get(f"{OKX_BASE}{path}", params=params, timeout=TIMEOUT)
|
| 126 |
+
r.raise_for_status()
|
| 127 |
+
data = r.json()
|
| 128 |
+
code = str(data.get("code"))
|
| 129 |
+
if code == "50011" and attempt < retries: # OKX rate-limit
|
| 130 |
+
time.sleep(0.5 * (attempt + 1))
|
| 131 |
+
continue
|
| 132 |
+
if code != "0":
|
| 133 |
+
raise RuntimeError(data.get("msg") or "OKX API hata döndürdü.")
|
| 134 |
+
return data["data"]
|
| 135 |
+
except requests.exceptions.RequestException as e:
|
| 136 |
+
if attempt < retries:
|
| 137 |
+
time.sleep(0.3 * (attempt + 1))
|
| 138 |
+
continue
|
| 139 |
+
raise RuntimeError(f"Ağ hatası: {e}")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def fetch_candles(inst_id, bar="1H", limit=300):
|
| 143 |
+
def _load():
|
| 144 |
+
raw = _get("/api/v5/market/candles", {"instId": inst_id, "bar": bar, "limit": limit})
|
| 145 |
+
if not raw:
|
| 146 |
+
raise RuntimeError(f"{inst_id} için mum verisi bulunamadı (sembolü kontrol edin).")
|
| 147 |
+
cols = ["ts", "o", "h", "l", "c", "vol", "volCcy", "volCcyQuote", "confirm"]
|
| 148 |
+
df = pd.DataFrame(raw, columns=cols)
|
| 149 |
+
for c in ["o", "h", "l", "c", "vol"]:
|
| 150 |
+
df[c] = df[c].astype(float)
|
| 151 |
+
df["confirm"] = df["confirm"].astype(int)
|
| 152 |
+
df["ts"] = pd.to_datetime(df["ts"].astype(np.int64), unit="ms")
|
| 153 |
+
return df.sort_values("ts").reset_index(drop=True)
|
| 154 |
+
|
| 155 |
+
return _cached(("candles", inst_id, bar, limit), CACHE_TTL, _load).copy()
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def fetch_ticker(inst_id):
|
| 159 |
+
raw = _get("/api/v5/market/ticker", {"instId": inst_id})
|
| 160 |
+
return raw[0] if raw else {}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def fetch_all_tickers(inst_type="SPOT"):
|
| 164 |
+
return _cached(("tickers", inst_type), 20, lambda: _get("/api/v5/market/tickers", {"instType": inst_type}))
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def fetch_orderbook(inst_id, depth=20):
|
| 168 |
+
raw = _get("/api/v5/market/books", {"instId": inst_id, "sz": depth})
|
| 169 |
+
return raw[0] if raw else {}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def fetch_funding_rate(inst_id):
|
| 173 |
+
"""Spot paritenin (BTC-USDT) sürekli vadeli karşılığının (BTC-USDT-SWAP) funding oranı."""
|
| 174 |
+
parts = inst_id.split("-")
|
| 175 |
+
if len(parts) != 2:
|
| 176 |
+
return None
|
| 177 |
+
raw = _get("/api/v5/public/funding-rate", {"instId": f"{inst_id}-SWAP"})
|
| 178 |
+
if not raw:
|
| 179 |
+
return None
|
| 180 |
+
return float(raw[0].get("fundingRate") or 0.0)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# ---------------------------------------------------------------------------
|
| 184 |
+
# TECHNICAL INDICATORS
|
| 185 |
+
# ---------------------------------------------------------------------------
|
| 186 |
+
def ema(s, p):
|
| 187 |
+
return s.ewm(span=p, adjust=False).mean()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def sma(s, p):
|
| 191 |
+
return s.rolling(p).mean()
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def rsi(s, p=14):
|
| 195 |
+
d = s.diff()
|
| 196 |
+
gain = d.clip(lower=0)
|
| 197 |
+
loss = -d.clip(upper=0)
|
| 198 |
+
ag = gain.ewm(alpha=1 / p, adjust=False).mean()
|
| 199 |
+
al = loss.ewm(alpha=1 / p, adjust=False).mean()
|
| 200 |
+
rs = ag / al.replace(0, np.nan)
|
| 201 |
+
return (100 - 100 / (1 + rs)).fillna(50)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def macd(s, fast=12, slow=26, signal=9):
|
| 205 |
+
line = ema(s, fast) - ema(s, slow)
|
| 206 |
+
sig = ema(line, signal)
|
| 207 |
+
return line, sig, line - sig
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def bollinger(s, p=20, k=2):
|
| 211 |
+
mid = sma(s, p)
|
| 212 |
+
std = s.rolling(p).std()
|
| 213 |
+
return mid + k * std, mid, mid - k * std
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def atr(df, p=14):
|
| 217 |
+
h, l, c = df["h"], df["l"], df["c"]
|
| 218 |
+
pc = c.shift(1)
|
| 219 |
+
tr = pd.concat([h - l, (h - pc).abs(), (l - pc).abs()], axis=1).max(axis=1)
|
| 220 |
+
return tr.ewm(alpha=1 / p, adjust=False).mean()
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def stochastic(df, k_p=14, d_p=3):
|
| 224 |
+
lo = df["l"].rolling(k_p).min()
|
| 225 |
+
hi = df["h"].rolling(k_p).max()
|
| 226 |
+
k = 100 * (df["c"] - lo) / (hi - lo).replace(0, np.nan)
|
| 227 |
+
d = k.rolling(d_p).mean()
|
| 228 |
+
return k.fillna(50), d.fillna(50)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def vwap_window(df):
|
| 232 |
+
tp = (df["h"] + df["l"] + df["c"]) / 3
|
| 233 |
+
return (tp * df["vol"]).cumsum() / df["vol"].cumsum().replace(0, np.nan)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def adx(df, p=14):
|
| 237 |
+
"""ADX + yönsel indikatörler (+DI / -DI). Trend gücünü ölçer (0-100)."""
|
| 238 |
+
h, l, c = df["h"], df["l"], df["c"]
|
| 239 |
+
up_move = h.diff()
|
| 240 |
+
down_move = -l.diff()
|
| 241 |
+
plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
|
| 242 |
+
minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
|
| 243 |
+
tr = pd.concat([h - l, (h - c.shift(1)).abs(), (l - c.shift(1)).abs()], axis=1).max(axis=1)
|
| 244 |
+
atr_ = tr.ewm(alpha=1 / p, adjust=False).mean()
|
| 245 |
+
plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
|
| 246 |
+
minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
|
| 247 |
+
dx = ((plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)) * 100
|
| 248 |
+
adx_ = dx.ewm(alpha=1 / p, adjust=False).mean()
|
| 249 |
+
return adx_.fillna(0), plus_di.fillna(0), minus_di.fillna(0)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def obv(df):
|
| 253 |
+
"""On-Balance Volume: hacmin fiyat yönüyle kümülatif toplamı."""
|
| 254 |
+
direction = np.sign(df["c"].diff()).fillna(0)
|
| 255 |
+
return (direction * df["vol"]).cumsum()
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def supertrend(df, period=10, mult=3.0):
|
| 259 |
+
"""Supertrend: ATR bantlı trend takipçisi. dir=+1 yükseliş, -1 düşüş. (nedensel, geleceğe bakmaz)"""
|
| 260 |
+
h, l, c = df["h"].to_numpy(), df["l"].to_numpy(), df["c"].to_numpy()
|
| 261 |
+
a = atr(df, period).to_numpy()
|
| 262 |
+
hl2 = (h + l) / 2
|
| 263 |
+
ub, lb = hl2 + mult * a, hl2 - mult * a
|
| 264 |
+
n = len(c)
|
| 265 |
+
fub, flb = ub.copy(), lb.copy()
|
| 266 |
+
direction = np.ones(n)
|
| 267 |
+
line = np.full(n, np.nan)
|
| 268 |
+
if n:
|
| 269 |
+
line[0] = flb[0]
|
| 270 |
+
for i in range(1, n):
|
| 271 |
+
fub[i] = ub[i] if (ub[i] < fub[i - 1] or c[i - 1] > fub[i - 1]) else fub[i - 1]
|
| 272 |
+
flb[i] = lb[i] if (lb[i] > flb[i - 1] or c[i - 1] < flb[i - 1]) else flb[i - 1]
|
| 273 |
+
if c[i] > fub[i - 1]:
|
| 274 |
+
direction[i] = 1
|
| 275 |
+
elif c[i] < flb[i - 1]:
|
| 276 |
+
direction[i] = -1
|
| 277 |
+
else:
|
| 278 |
+
direction[i] = direction[i - 1]
|
| 279 |
+
line[i] = flb[i] if direction[i] == 1 else fub[i]
|
| 280 |
+
return pd.Series(line, index=df.index), pd.Series(direction, index=df.index)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def ichimoku(df):
|
| 284 |
+
"""Tenkan/Kijun + bulut. Bulut, 26 bar önce hesaplanan Senkou A/B'nin bugüne kaydırılmış halidir
|
| 285 |
+
(yani nedensel: gelecek veri kullanılmaz)."""
|
| 286 |
+
h, l = df["h"], df["l"]
|
| 287 |
+
tenkan = (h.rolling(9).max() + l.rolling(9).min()) / 2
|
| 288 |
+
kijun = (h.rolling(26).max() + l.rolling(26).min()) / 2
|
| 289 |
+
span_a = (tenkan + kijun) / 2
|
| 290 |
+
span_b = (h.rolling(52).max() + l.rolling(52).min()) / 2
|
| 291 |
+
return tenkan, kijun, span_a.shift(26), span_b.shift(26)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def hilo_activator(df, period=HILO_PERIOD):
|
| 295 |
+
"""Gann HiLo Activator. Kapanış önceki barın SMA(High) değerini aşarsa YÜKSELİŞ (çizgi=SMA(Low)),
|
| 296 |
+
önceki barın SMA(Low) değerinin altına inerse DÜŞÜŞ (çizgi=SMA(High)) moduna geçer; arada mod korunur.
|
| 297 |
+
Dönüş: (çizgi, yön) yön: +1 / -1 / 0 (henüz belirsiz). Nedensel — geleceğe bakmaz."""
|
| 298 |
+
hi_ma = df["h"].rolling(period).mean()
|
| 299 |
+
lo_ma = df["l"].rolling(period).mean()
|
| 300 |
+
c = df["c"].to_numpy(dtype=float)
|
| 301 |
+
hi_p, lo_p = hi_ma.shift(1).to_numpy(), lo_ma.shift(1).to_numpy()
|
| 302 |
+
hi_c, lo_c = hi_ma.to_numpy(), lo_ma.to_numpy()
|
| 303 |
+
n = len(c)
|
| 304 |
+
direction = np.zeros(n)
|
| 305 |
+
line = np.full(n, np.nan)
|
| 306 |
+
cur = 0.0
|
| 307 |
+
for i in range(n):
|
| 308 |
+
if hi_p[i] == hi_p[i] and c[i] > hi_p[i]:
|
| 309 |
+
cur = 1.0
|
| 310 |
+
elif lo_p[i] == lo_p[i] and c[i] < lo_p[i]:
|
| 311 |
+
cur = -1.0
|
| 312 |
+
direction[i] = cur
|
| 313 |
+
if cur > 0:
|
| 314 |
+
line[i] = lo_c[i]
|
| 315 |
+
elif cur < 0:
|
| 316 |
+
line[i] = hi_c[i]
|
| 317 |
+
return pd.Series(line, index=df.index), pd.Series(direction, index=df.index)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def fib_swing(df, lookback=FIB_LOOKBACK, min_bars=FIB_MIN_BARS):
|
| 321 |
+
"""Her bar için son `lookback` bardaki en yüksek/en düşük nokta ve dalga yönü.
|
| 322 |
+
yön=+1: dip tepeden ÖNCE (yükseliş dalgası -> düzeltmede destek aranır)
|
| 323 |
+
yön=-1: tepe dipten ÖNCE (düşüş dalgası -> tepkide direnç aranır). Nedensel."""
|
| 324 |
+
h, l = df["h"], df["l"]
|
| 325 |
+
hi = h.rolling(lookback, min_periods=min_bars).max()
|
| 326 |
+
lo = l.rolling(lookback, min_periods=min_bars).min()
|
| 327 |
+
hi_pos = h.rolling(lookback, min_periods=min_bars).apply(np.argmax, raw=True)
|
| 328 |
+
lo_pos = l.rolling(lookback, min_periods=min_bars).apply(np.argmin, raw=True)
|
| 329 |
+
direction = pd.Series(np.where(lo_pos < hi_pos, 1.0, -1.0), index=df.index)
|
| 330 |
+
direction = direction.where(hi_pos.notna() & lo_pos.notna())
|
| 331 |
+
return hi, lo, direction
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def fib_levels(hi, lo, direction):
|
| 335 |
+
"""Dalga yönüne göre Fibonacci düzeltme fiyatları: [(oran, fiyat), ...]"""
|
| 336 |
+
rng = hi - lo
|
| 337 |
+
if direction > 0:
|
| 338 |
+
return [(r, hi - r * rng) for r in FIB_RATIOS]
|
| 339 |
+
return [(r, lo + r * rng) for r in FIB_RATIOS]
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def mfi(df, p=14):
|
| 343 |
+
"""Money Flow Index: hacim ağırlıklı RSI."""
|
| 344 |
+
tp = (df["h"] + df["l"] + df["c"]) / 3
|
| 345 |
+
rmf = tp * df["vol"]
|
| 346 |
+
d = tp.diff()
|
| 347 |
+
pos = rmf.where(d > 0, 0.0).rolling(p).sum()
|
| 348 |
+
neg = rmf.where(d < 0, 0.0).rolling(p).sum()
|
| 349 |
+
out = 100 - 100 / (1 + pos / neg.replace(0, np.nan))
|
| 350 |
+
out = out.where(~((neg == 0) & (pos > 0)), 100.0)
|
| 351 |
+
return out.fillna(50)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def cmf(df, p=20):
|
| 355 |
+
"""Chaikin Money Flow: kapanışın bar aralığındaki konumuna göre hacim akışı (-1..+1)."""
|
| 356 |
+
rng = (df["h"] - df["l"]).replace(0, np.nan)
|
| 357 |
+
mfm = ((df["c"] - df["l"]) - (df["h"] - df["c"])) / rng
|
| 358 |
+
mfv = mfm.fillna(0) * df["vol"]
|
| 359 |
+
return (mfv.rolling(p).sum() / df["vol"].rolling(p).sum().replace(0, np.nan)).fillna(0)
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def market_structure(df, w=10):
|
| 363 |
+
"""+1: Higher-High & Higher-Low (yükseliş yapısı), -1: Lower-High & Lower-Low, 0: karışık."""
|
| 364 |
+
hi, lo = df["h"].rolling(w).max(), df["l"].rolling(w).min()
|
| 365 |
+
up = (hi > hi.shift(w)) & (lo > lo.shift(w))
|
| 366 |
+
dn = (hi < hi.shift(w)) & (lo < lo.shift(w))
|
| 367 |
+
return pd.Series(np.where(up, 1.0, np.where(dn, -1.0, 0.0)), index=df.index)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def squeeze_indicator(df, ind):
|
| 371 |
+
"""TTM tarzı sıkışma: Bollinger bantları Keltner kanalının İÇİNE girerse volatilite sıkışmıştır.
|
| 372 |
+
Dönüş: (sıkışma_açık, son 3 barda sıkışma çözüldü mü, momentum)"""
|
| 373 |
+
kc_mid = ind["ema20"]
|
| 374 |
+
kc_u, kc_l = kc_mid + 1.5 * ind["atr"], kc_mid - 1.5 * ind["atr"]
|
| 375 |
+
on = (ind["bb_lower"] > kc_l) & (ind["bb_upper"] < kc_u)
|
| 376 |
+
release = (~on) & on.shift(1, fill_value=False)
|
| 377 |
+
rel3 = release.astype(float).rolling(3, min_periods=1).max()
|
| 378 |
+
hh, ll = df["h"].rolling(20).max(), df["l"].rolling(20).min()
|
| 379 |
+
mom = df["c"] - ((hh + ll) / 2 + sma(df["c"], 20)) / 2
|
| 380 |
+
return on.astype(float), rel3, mom
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
CANDLE_PATTERNS = [
|
| 384 |
+
("pat_hammer", "Çekiç", 1),
|
| 385 |
+
("pat_bull_eng", "Yükseliş Yutan Mum", 1),
|
| 386 |
+
("pat_morning", "Sabah Yıldızı", 1),
|
| 387 |
+
("pat_shooting", "Kayan Yıldız", -1),
|
| 388 |
+
("pat_bear_eng", "Düşüş Yutan Mum", -1),
|
| 389 |
+
("pat_evening", "Akşam Yıldızı", -1),
|
| 390 |
+
]
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def candle_patterns(df):
|
| 394 |
+
"""Vektörel mum formasyonu tespiti (yalnızca geçmiş barlar kullanılır)."""
|
| 395 |
+
o, h, l, c = df["o"], df["h"], df["l"], df["c"]
|
| 396 |
+
body = (c - o).abs()
|
| 397 |
+
rng = (h - l).replace(0, np.nan)
|
| 398 |
+
upper = h - np.maximum(o, c)
|
| 399 |
+
lower = np.minimum(o, c) - l
|
| 400 |
+
avg_body = body.rolling(10, min_periods=3).mean()
|
| 401 |
+
prior_down = c.shift(1) < c.shift(4)
|
| 402 |
+
prior_up = c.shift(1) > c.shift(4)
|
| 403 |
+
|
| 404 |
+
hammer = (lower >= 0.6 * rng) & (body <= 0.3 * rng) & (upper <= 0.15 * rng) & prior_down
|
| 405 |
+
shooting = (upper >= 0.6 * rng) & (body <= 0.3 * rng) & (lower <= 0.15 * rng) & prior_up
|
| 406 |
+
bull_eng = ((c.shift(1) < o.shift(1)) & (c > o) & (c >= o.shift(1)) & (o <= c.shift(1))
|
| 407 |
+
& (body > body.shift(1)) & prior_down)
|
| 408 |
+
bear_eng = ((c.shift(1) > o.shift(1)) & (c < o) & (c <= o.shift(1)) & (o >= c.shift(1))
|
| 409 |
+
& (body > body.shift(1)) & prior_up)
|
| 410 |
+
big2 = body.shift(2) > avg_body.shift(2)
|
| 411 |
+
small1 = body.shift(1) < 0.35 * body.shift(2)
|
| 412 |
+
mid2 = (o.shift(2) + c.shift(2)) / 2
|
| 413 |
+
morning = (c.shift(2) < o.shift(2)) & big2 & small1 & (c > o) & (c > mid2)
|
| 414 |
+
evening = (c.shift(2) > o.shift(2)) & big2 & small1 & (c < o) & (c < mid2)
|
| 415 |
+
|
| 416 |
+
raw = {"pat_hammer": hammer, "pat_shooting": shooting, "pat_bull_eng": bull_eng,
|
| 417 |
+
"pat_bear_eng": bear_eng, "pat_morning": morning, "pat_evening": evening}
|
| 418 |
+
return {k: v.fillna(False).astype(float) for k, v in raw.items()}
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def find_swings(series, window=3):
|
| 422 |
+
"""Yerel tepe/dip (swing high/low) indekslerini bulur."""
|
| 423 |
+
n = len(series)
|
| 424 |
+
highs, lows = [], []
|
| 425 |
+
for i in range(window, n - window):
|
| 426 |
+
seg = series.iloc[i - window:i + window + 1]
|
| 427 |
+
if series.iloc[i] == seg.max():
|
| 428 |
+
highs.append(i)
|
| 429 |
+
if series.iloc[i] == seg.min():
|
| 430 |
+
lows.append(i)
|
| 431 |
+
return highs, lows
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
def detect_divergence(df, ind, lookback=60, window=3):
|
| 435 |
+
"""Fiyat ile RSI arasındaki son swing'leri karşılaştırarak klasik uyumsuzluğu tespit eder."""
|
| 436 |
+
n = len(df)
|
| 437 |
+
lb = min(lookback, n)
|
| 438 |
+
close = df["c"].iloc[-lb:].reset_index(drop=True)
|
| 439 |
+
rsi_s = ind["rsi"].iloc[-lb:].reset_index(drop=True)
|
| 440 |
+
highs, lows = find_swings(close, window)
|
| 441 |
+
result = {"bullish": False, "bearish": False}
|
| 442 |
+
if len(highs) >= 2:
|
| 443 |
+
h1, h2 = highs[-2], highs[-1]
|
| 444 |
+
if close.iloc[h2] > close.iloc[h1] and rsi_s.iloc[h2] < rsi_s.iloc[h1]:
|
| 445 |
+
result["bearish"] = True
|
| 446 |
+
if len(lows) >= 2:
|
| 447 |
+
l1, l2 = lows[-2], lows[-1]
|
| 448 |
+
if close.iloc[l2] < close.iloc[l1] and rsi_s.iloc[l2] > rsi_s.iloc[l1]:
|
| 449 |
+
result["bullish"] = True
|
| 450 |
+
return result
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def find_sr_levels(df, atr_v, window=5, lookback=200, max_levels=4):
|
| 454 |
+
"""Swing tepe/diplerini kümeleyerek destek/direnç seviyeleri çıkarır.
|
| 455 |
+
Dönüş: (destekler, dirençler) — her biri [(seviye, temas_sayısı), ...], fiyata yakınlık sırasıyla."""
|
| 456 |
+
sub = df.iloc[-lookback:].reset_index(drop=True)
|
| 457 |
+
highs, _ = find_swings(sub["h"], window)
|
| 458 |
+
_, lows = find_swings(sub["l"], window)
|
| 459 |
+
pts = [float(sub["h"].iloc[i]) for i in highs] + [float(sub["l"].iloc[i]) for i in lows]
|
| 460 |
+
if not pts:
|
| 461 |
+
return [], []
|
| 462 |
+
price = float(df["c"].iloc[-1])
|
| 463 |
+
tol = max(0.6 * (atr_v if atr_v and atr_v > 0 else 0.0), price * 0.002)
|
| 464 |
+
pts.sort()
|
| 465 |
+
clusters, cur = [], [pts[0]]
|
| 466 |
+
for p in pts[1:]:
|
| 467 |
+
if p - float(np.mean(cur)) <= tol:
|
| 468 |
+
cur.append(p)
|
| 469 |
+
else:
|
| 470 |
+
clusters.append(cur)
|
| 471 |
+
cur = [p]
|
| 472 |
+
clusters.append(cur)
|
| 473 |
+
levels = [(float(np.mean(c)), len(c)) for c in clusters]
|
| 474 |
+
supports = sorted([lv for lv in levels if lv[0] < price], key=lambda x: -x[0])[:max_levels]
|
| 475 |
+
resists = sorted([lv for lv in levels if lv[0] > price], key=lambda x: x[0])[:max_levels]
|
| 476 |
+
return supports, resists
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def compute_indicators(df):
|
| 480 |
+
close = df["c"]
|
| 481 |
+
ind = {}
|
| 482 |
+
ind["rsi"] = rsi(close)
|
| 483 |
+
ind["macd_line"], ind["macd_signal"], ind["macd_hist"] = macd(close)
|
| 484 |
+
ind["ema20"] = ema(close, 20)
|
| 485 |
+
ind["ema50"] = ema(close, 50)
|
| 486 |
+
ind["ema200"] = ema(close, 200 if len(close) >= 200 else max(5, len(close) // 2))
|
| 487 |
+
ind["bb_upper"], ind["bb_mid"], ind["bb_lower"] = bollinger(close)
|
| 488 |
+
ind["atr"] = atr(df)
|
| 489 |
+
ind["stoch_k"], ind["stoch_d"] = stochastic(df)
|
| 490 |
+
ind["vwap"] = vwap_window(df)
|
| 491 |
+
ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df)
|
| 492 |
+
ind["obv"] = obv(df)
|
| 493 |
+
ind["obv_ema"] = ema(ind["obv"], 20)
|
| 494 |
+
# --- v2 ek faktörler ---
|
| 495 |
+
ind["st_line"], ind["st_dir"] = supertrend(df)
|
| 496 |
+
ind["tenkan"], ind["kijun"], ind["ich_a"], ind["ich_b"] = ichimoku(df)
|
| 497 |
+
ind["hilo_line"], ind["hilo_dir"] = hilo_activator(df)
|
| 498 |
+
ind["fib_hi"], ind["fib_lo"], ind["fib_dir"] = fib_swing(df)
|
| 499 |
+
ind["mfi"] = mfi(df)
|
| 500 |
+
ind["cmf"] = cmf(df)
|
| 501 |
+
ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0)
|
| 502 |
+
ind["dc_hi20"] = df["h"].rolling(20).max().shift(1)
|
| 503 |
+
ind["dc_lo20"] = df["l"].rolling(20).min().shift(1)
|
| 504 |
+
ind["struct"] = market_structure(df)
|
| 505 |
+
ind["sq_on"], ind["sq_rel3"], ind["sq_mom"] = squeeze_indicator(df, ind)
|
| 506 |
+
ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply(
|
| 507 |
+
lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5)
|
| 508 |
+
for name, ser in candle_patterns(df).items():
|
| 509 |
+
ind[name] = ser
|
| 510 |
+
# hızlı, nedensel erişim için numpy önbelleği (backtest döngüsünde .iloc yerine kullanılır)
|
| 511 |
+
arr = {k: v.to_numpy(dtype=float) for k, v in ind.items()}
|
| 512 |
+
for k in ("o", "h", "l", "c", "vol"):
|
| 513 |
+
arr[k] = df[k].to_numpy(dtype=float)
|
| 514 |
+
ind["_arr"] = arr
|
| 515 |
+
return ind
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# ---------------------------------------------------------------------------
|
| 519 |
+
# COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli)
|
| 520 |
+
# ---------------------------------------------------------------------------
|
| 521 |
+
def _score_at(ind, i, verbose=False):
|
| 522 |
+
"""Çoklu faktör puanlamasını verilen bar indeksinde hesaplar (yalnızca o ana kadarki veri).
|
| 523 |
+
|
| 524 |
+
İki faktör ailesi vardır:
|
| 525 |
+
T (TREND) : MACD, EMA dizilimi, VWAP, OBV, Supertrend, HiLo Activator, Ichimoku,
|
| 526 |
+
Donchian kırılımı, piyasa yapısı, CMF, sıkışma kırılımı, ADX/DI yönü
|
| 527 |
+
F (FİBONACCI) : düzeltme seviyeleri (rejimden bağımsız, ağırlık 1.0)
|
| 528 |
+
R (REVERSION) : RSI, Bollinger, Stokastik, MFI, mum formasyonları
|
| 529 |
+
|
| 530 |
+
ADX'e göre iki ailenin ağırlığı SÜREKLİ olarak kayar (w: 0=yatay … 1=güçlü trend).
|
| 531 |
+
Güçlü trendde karşı-trend (aşırı alım/satım) sinyalleri bastırılır; yatay piyasada trend takibi bastırılır.
|
| 532 |
+
Sonuç, tanh ile ±10 aralığına yumuşak sıkıştırılır.
|
| 533 |
+
Canlı sinyal ve tarihsel backtest AYNI fonksiyonu kullanır (tutarlılık + gelecek sızıntısı yok)."""
|
| 534 |
+
A = ind["_arr"]
|
| 535 |
+
price = A["c"][i]
|
| 536 |
+
items = [] # (etiket, grup, ham_puan, açıklama)
|
| 537 |
+
|
| 538 |
+
def add(label, group, raw, text):
|
| 539 |
+
items.append((label, group, float(raw), text))
|
| 540 |
+
|
| 541 |
+
# --- RSI (R) ---
|
| 542 |
+
rsi_v = A["rsi"][i]
|
| 543 |
+
if rsi_v < 30:
|
| 544 |
+
add("RSI", "R", 2, f"RSI aşırı satım bölgesinde ({rsi_v:.1f})")
|
| 545 |
+
elif rsi_v > 70:
|
| 546 |
+
add("RSI", "R", -2, f"RSI aşırı alım bölgesinde ({rsi_v:.1f})")
|
| 547 |
+
else:
|
| 548 |
+
add("RSI", "R", 0, f"RSI nötr bölgede ({rsi_v:.1f})")
|
| 549 |
+
|
| 550 |
+
# --- MACD histogramı (T) ---
|
| 551 |
+
hist_v = A["macd_hist"][i]
|
| 552 |
+
hist_prev = A["macd_hist"][i - 1] if i > 0 else 0.0
|
| 553 |
+
if hist_v > 0 and hist_v > hist_prev:
|
| 554 |
+
add("MACD", "T", 1.5, "MACD histogramı pozitif ve güçleniyor")
|
| 555 |
+
elif hist_v > 0:
|
| 556 |
+
add("MACD", "T", 0.5, "MACD histogramı pozitif")
|
| 557 |
+
elif hist_v < 0 and hist_v < hist_prev:
|
| 558 |
+
add("MACD", "T", -1.5, "MACD histogramı negatif ve zayıflıyor")
|
| 559 |
+
else:
|
| 560 |
+
add("MACD", "T", -0.5, "MACD histogramı negatif")
|
| 561 |
+
|
| 562 |
+
# --- EMA dizilimi (T) ---
|
| 563 |
+
e20, e50, e200 = A["ema20"][i], A["ema50"][i], A["ema200"][i]
|
| 564 |
+
if price > e20 > e50 > e200:
|
| 565 |
+
add("EMA Dizilimi", "T", 2, "Güçlü yükseliş trendi (Fiyat > EMA20 > EMA50 > EMA200)")
|
| 566 |
+
elif price > e20 and e20 > e50:
|
| 567 |
+
add("EMA Dizilimi", "T", 1, "Yükseliş trendi (kısa vadeli ortalamaların üzerinde)")
|
| 568 |
+
elif price < e20 < e50 < e200:
|
| 569 |
+
add("EMA Dizilimi", "T", -2, "Güçlü düşüş trendi (Fiyat < EMA20 < EMA50 < EMA200)")
|
| 570 |
+
elif price < e20 and e20 < e50:
|
| 571 |
+
add("EMA Dizilimi", "T", -1, "Düşüş trendi (kısa vadeli ortalamaların altında)")
|
| 572 |
+
else:
|
| 573 |
+
add("EMA Dizilimi", "T", 0, "Trend yatay / belirsiz")
|
| 574 |
+
|
| 575 |
+
# --- Bollinger konumu (R) ---
|
| 576 |
+
bb_u, bb_l = A["bb_upper"][i], A["bb_lower"][i]
|
| 577 |
+
if bb_u == bb_u and bb_l == bb_l and bb_u != bb_l:
|
| 578 |
+
bb_pos = (price - bb_l) / (bb_u - bb_l)
|
| 579 |
+
if bb_pos < 0.15:
|
| 580 |
+
add("Bollinger", "R", 1, "Fiyat Bollinger alt bandına yakın (olası tepki)")
|
| 581 |
+
elif bb_pos > 0.85:
|
| 582 |
+
add("Bollinger", "R", -1, "Fiyat Bollinger üst bandına yakın (olası tepki)")
|
| 583 |
+
|
| 584 |
+
# --- Stokastik (R) ---
|
| 585 |
+
k_v = A["stoch_k"][i]
|
| 586 |
+
if k_v < 20:
|
| 587 |
+
add("Stokastik", "R", 1, f"Stokastik aşırı satım bölgesinde ({k_v:.1f})")
|
| 588 |
+
elif k_v > 80:
|
| 589 |
+
add("Stokastik", "R", -1, f"Stokastik aşırı alım bölgesinde ({k_v:.1f})")
|
| 590 |
+
|
| 591 |
+
# --- MFI (R) ---
|
| 592 |
+
mfi_v = A["mfi"][i]
|
| 593 |
+
if mfi_v < 20:
|
| 594 |
+
add("MFI", "R", 1, f"MFI (hacimli RSI) aşırı satımda ({mfi_v:.1f})")
|
| 595 |
+
elif mfi_v > 80:
|
| 596 |
+
add("MFI", "R", -1, f"MFI (hacimli RSI) aşırı alımda ({mfi_v:.1f})")
|
| 597 |
+
|
| 598 |
+
# --- Mum formasyonları (R) ---
|
| 599 |
+
hits = []
|
| 600 |
+
for key, name, sgn in CANDLE_PATTERNS:
|
| 601 |
+
if A[key][i] > 0:
|
| 602 |
+
hits.append((name, sgn))
|
| 603 |
+
if hits:
|
| 604 |
+
raw = max(-1.5, min(1.5, float(sum(s for _, s in hits))))
|
| 605 |
+
add("Mum Formasyonu", "R", raw, "Mum formasyonu: " + ", ".join(n for n, _ in hits))
|
| 606 |
+
|
| 607 |
+
# --- VWAP (T) ---
|
| 608 |
+
vwap_v = A["vwap"][i]
|
| 609 |
+
if vwap_v == vwap_v:
|
| 610 |
+
if price > vwap_v:
|
| 611 |
+
add("VWAP", "T", 0.5, "Fiyat pencere VWAP'ının üzerinde")
|
| 612 |
+
else:
|
| 613 |
+
add("VWAP", "T", -0.5, "Fiyat pencere VWAP'ının altında")
|
| 614 |
+
|
| 615 |
+
# --- OBV (T) ---
|
| 616 |
+
obv_v, obv_e = A["obv"][i], A["obv_ema"][i]
|
| 617 |
+
if obv_e == obv_e:
|
| 618 |
+
if obv_v > obv_e:
|
| 619 |
+
add("OBV", "T", 0.5, "OBV hacim akışı yükseliş yönünde")
|
| 620 |
+
else:
|
| 621 |
+
add("OBV", "T", -0.5, "OBV hacim akışı düşüş yönünde")
|
| 622 |
+
|
| 623 |
+
# --- CMF (T) ---
|
| 624 |
+
cmf_v = A["cmf"][i]
|
| 625 |
+
if cmf_v > 0.05:
|
| 626 |
+
add("CMF", "T", 0.5, f"Chaikin para akışı pozitif ({cmf_v:+.2f}) — alıcılar baskın")
|
| 627 |
+
elif cmf_v < -0.05:
|
| 628 |
+
add("CMF", "T", -0.5, f"Chaikin para akışı negatif ({cmf_v:+.2f}) — satıcılar baskın")
|
| 629 |
+
|
| 630 |
+
# --- Supertrend (T) ---
|
| 631 |
+
st_d = A["st_dir"][i]
|
| 632 |
+
st_flip = i > 0 and A["st_dir"][i] != A["st_dir"][i - 1]
|
| 633 |
+
if st_d > 0:
|
| 634 |
+
add("Supertrend", "T", 1.5 + (0.5 if st_flip else 0),
|
| 635 |
+
"Supertrend YÜKSELİŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend yükseliş modunda")
|
| 636 |
+
else:
|
| 637 |
+
add("Supertrend", "T", -(1.5 + (0.5 if st_flip else 0)),
|
| 638 |
+
"Supertrend DÜŞÜŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend düşüş modunda")
|
| 639 |
+
|
| 640 |
+
# --- Gann HiLo Activator (T) ---
|
| 641 |
+
hl_d = A["hilo_dir"][i]
|
| 642 |
+
hilo_flip = 0
|
| 643 |
+
if hl_d != 0:
|
| 644 |
+
prev_d = A["hilo_dir"][i - 1] if i > 0 else 0.0
|
| 645 |
+
if prev_d != 0 and prev_d != hl_d:
|
| 646 |
+
hilo_flip = int(hl_d)
|
| 647 |
+
mag = 1.25 + (0.5 if hilo_flip else 0.0)
|
| 648 |
+
if hl_d > 0:
|
| 649 |
+
add("HiLo Activator", "T", mag,
|
| 650 |
+
"HiLo Activator YÜKSELİŞ moduna döndü (taze sinyal)" if hilo_flip else "HiLo Activator yükseliş modunda (fiyat HiLo çizgisinin üzerinde)")
|
| 651 |
+
else:
|
| 652 |
+
add("HiLo Activator", "T", -mag,
|
| 653 |
+
"HiLo Activator DÜŞÜŞ moduna döndü (taze sinyal)" if hilo_flip else "HiLo Activator düşüş modunda (fiyat HiLo çizgisinin altında)")
|
| 654 |
+
|
| 655 |
+
# --- Fibonacci düzeltme seviyeleri (F) ---
|
| 656 |
+
fh, fl, fd, atr_i = A["fib_hi"][i], A["fib_lo"][i], A["fib_dir"][i], A["atr"][i]
|
| 657 |
+
fib_zone, fib_depth = 0, None
|
| 658 |
+
if fh == fh and fl == fl and fd == fd and atr_i == atr_i and atr_i > 0 and (fh - fl) >= FIB_MIN_RANGE_ATR * atr_i:
|
| 659 |
+
rng = fh - fl
|
| 660 |
+
depth = (fh - price) / rng if fd > 0 else (price - fl) / rng
|
| 661 |
+
fib_depth = depth
|
| 662 |
+
d = 1 if fd > 0 else -1
|
| 663 |
+
bounce = (A["c"][i] > A["o"][i]) if d > 0 else (A["c"][i] < A["o"][i])
|
| 664 |
+
near = min(FIB_RATIOS, key=lambda r: abs(r - depth))
|
| 665 |
+
wave = "yükseliş" if d > 0 else "düşüş"
|
| 666 |
+
role = "destek" if d > 0 else "direnç"
|
| 667 |
+
if 0.35 <= depth <= 0.65:
|
| 668 |
+
fib_zone = d
|
| 669 |
+
add("Fibonacci", "F", d * (1.5 if bounce else 0.5),
|
| 670 |
+
f"Fibonacci ALTIN BÖLGE: {wave} dalgasının %{depth * 100:.1f} düzeltmesinde (Fib {near:g} yakını) — {role} bölgesi"
|
| 671 |
+
+ (", mum tepkiyi teyit ediyor" if bounce else ", henüz mum teyidi yok"))
|
| 672 |
+
elif 0.236 <= depth < 0.35:
|
| 673 |
+
add("Fibonacci", "F", d * 0.5,
|
| 674 |
+
f"Sığ düzeltme (%{depth * 100:.1f}, Fib 0.236–0.382) — {wave} dalgası güçlü, trend devamı lehine")
|
| 675 |
+
elif 0.65 < depth <= 0.82:
|
| 676 |
+
add("Fibonacci", "F", d * (0.5 if bounce else 0.0),
|
| 677 |
+
f"Derin düzeltme (%{depth * 100:.1f}, Fib 0.786 yakını) — son {role} bölgesi")
|
| 678 |
+
elif depth > 0.82:
|
| 679 |
+
add("Fibonacci", "F", -d * 1.0,
|
| 680 |
+
f"Fibonacci 0.786 aşıldı (%{depth * 100:.1f}) — {wave} dalgası geçersizleşiyor, ters yön riski")
|
| 681 |
+
|
| 682 |
+
# --- Ichimoku (T) ---
|
| 683 |
+
sa, sb = A["ich_a"][i], A["ich_b"][i]
|
| 684 |
+
if sa == sa and sb == sb:
|
| 685 |
+
top, bot = max(sa, sb), min(sa, sb)
|
| 686 |
+
if price > top:
|
| 687 |
+
add("Ichimoku Bulutu", "T", 1.5, "Fiyat Ichimoku bulutunun üzerinde (boğa bölgesi)")
|
| 688 |
+
elif price < bot:
|
| 689 |
+
add("Ichimoku Bulutu", "T", -1.5, "Fiyat Ichimoku bulutunun altında (ayı bölgesi)")
|
| 690 |
+
else:
|
| 691 |
+
add("Ichimoku Bulutu", "T", 0, "Fiyat Ichimoku bulutunun içinde (kararsız bölge)")
|
| 692 |
+
tk, kj = A["tenkan"][i], A["kijun"][i]
|
| 693 |
+
if tk == tk and kj == kj:
|
| 694 |
+
add("Tenkan/Kijun", "T", 0.5 if tk > kj else -0.5,
|
| 695 |
+
"Tenkan > Kijun (kısa vadeli momentum yukarı)" if tk > kj else "Tenkan < Kijun (kısa vadeli momentum aşağı)")
|
| 696 |
+
|
| 697 |
+
# --- Donchian kırılımı (T) ---
|
| 698 |
+
dhi, dlo, rvol = A["dc_hi20"][i], A["dc_lo20"][i], A["rvol"][i]
|
| 699 |
+
if dhi == dhi and dlo == dlo:
|
| 700 |
+
if price > dhi:
|
| 701 |
+
if rvol >= 1.2:
|
| 702 |
+
add("Donchian Kırılımı", "T", 1.5, f"20 barlık tepe HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)")
|
| 703 |
+
else:
|
| 704 |
+
add("Donchian Kırılımı", "T", 0.75, f"20 barlık tepe kırıldı ama hacim zayıf ({rvol:.1f}x)")
|
| 705 |
+
elif price < dlo:
|
| 706 |
+
if rvol >= 1.2:
|
| 707 |
+
add("Donchian Kırılımı", "T", -1.5, f"20 barlık dip HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)")
|
| 708 |
+
else:
|
| 709 |
+
add("Donchian Kırılımı", "T", -0.75, f"20 barlık dip kırıldı ama hacim zayıf ({rvol:.1f}x)")
|
| 710 |
+
|
| 711 |
+
# --- Piyasa yapısı (T) ---
|
| 712 |
+
st_v = A["struct"][i]
|
| 713 |
+
if st_v > 0:
|
| 714 |
+
add("Piyasa Yapısı", "T", 1, "Piyasa yapısı: Higher-High / Higher-Low (yükseliş yapısı)")
|
| 715 |
+
elif st_v < 0:
|
| 716 |
+
add("Piyasa Yapısı", "T", -1, "Piyasa yapısı: Lower-High / Lower-Low (düşüş yapısı)")
|
| 717 |
+
|
| 718 |
+
# --- Sıkışma (Bollinger ⊂ Keltner) ---
|
| 719 |
+
squeeze_on = A["sq_on"][i] > 0
|
| 720 |
+
mom = A["sq_mom"][i]
|
| 721 |
+
if A["sq_rel3"][i] > 0 and mom == mom and mom != 0:
|
| 722 |
+
d = 1 if mom > 0 else -1
|
| 723 |
+
add("Sıkışma Kırılımı", "T", 1.5 * d,
|
| 724 |
+
"Volatilite sıkışması yukarı yönde çözüldü" if d > 0 else "Volatilite sıkışması aşağı yönde çözüldü")
|
| 725 |
+
elif squeeze_on:
|
| 726 |
+
add("Sıkışma", "T", 0, "Volatilite sıkışması aktif — kırılım bekleniyor, sinyal güveni düşürüldü")
|
| 727 |
+
|
| 728 |
+
# --- REJİM (ADX) — sürekli ağırlıklandırma ---
|
| 729 |
+
adx_v = A["adx"][i]
|
| 730 |
+
w = min(max((adx_v - 15.0) / 15.0, 0.0), 1.0) if adx_v == adx_v else 0.0 # 0 = yatay, 1 = güçlü trend
|
| 731 |
+
trend_w = 0.5 + 0.8 * w
|
| 732 |
+
rev_w = 1.3 - 0.95 * w
|
| 733 |
+
di_dir = 1 if A["plus_di"][i] > A["minus_di"][i] else -1
|
| 734 |
+
add("ADX / DI Yönü", "T", di_dir * w, "") # yönsel bonus; w kadar ölçeklenir
|
| 735 |
+
if w >= 0.67:
|
| 736 |
+
regime = "TREND"
|
| 737 |
+
add("Rejim", "T", 0, f"ADX güçlü trend gösteriyor ({adx_v:.1f}) — trend faktörleri ağırlıklı, karşı-trend sinyaller bastırıldı")
|
| 738 |
+
elif w <= 0.33:
|
| 739 |
+
regime = "YATAY"
|
| 740 |
+
add("Rejim", "T", 0, f"ADX zayıf/yatay piyasa ({adx_v:.1f}) — ortalamaya dönüş faktörleri ağırlıklı")
|
| 741 |
+
else:
|
| 742 |
+
regime = "GEÇİŞ"
|
| 743 |
+
add("Rejim", "T", 0, f"ADX geçiş bölgesinde ({adx_v:.1f}) — faktör ağırlıkları dengeli karışım")
|
| 744 |
+
|
| 745 |
+
gw = {"T": trend_w, "R": rev_w} # "F" (Fibonacci) rejimden bağımsız: ağırlık 1.0
|
| 746 |
+
contribs = [(lab, gw.get(g, 1.0) * raw, raw) for lab, g, raw, _ in items]
|
| 747 |
+
raw_total = sum(c for _, c, _ in contribs)
|
| 748 |
+
if squeeze_on:
|
| 749 |
+
raw_total *= 0.75
|
| 750 |
+
score = 10.0 * math.tanh(raw_total / 10.0) # 2 yeni faktör eklendiği için ölçek 9 -> 10
|
| 751 |
+
bull_w = sum(c for _, c, _ in contribs if c > 0)
|
| 752 |
+
bear_w = -sum(c for _, c, _ in contribs if c < 0)
|
| 753 |
+
|
| 754 |
+
out = {
|
| 755 |
+
"score": score, "price": price, "adx": adx_v, "w": w, "regime": regime,
|
| 756 |
+
"bull_w": bull_w, "bear_w": bear_w, "squeeze_on": bool(squeeze_on),
|
| 757 |
+
"atr_rank": A["atr_rank"][i], "pat_hits": hits,
|
| 758 |
+
"hilo_flip": hilo_flip, "fib_zone": fib_zone, "fib_depth": fib_depth,
|
| 759 |
+
}
|
| 760 |
+
if verbose:
|
| 761 |
+
def tone(raw):
|
| 762 |
+
return "up" if raw > 0 else ("down" if raw < 0 else "flat")
|
| 763 |
+
out["reasons"] = [(tone(raw), text) for _, _, raw, text in items if text]
|
| 764 |
+
out["contribs"] = [(lab, c) for lab, c, raw in contribs if abs(c) > 1e-9]
|
| 765 |
+
return out
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
def compute_score_series(ind, n):
|
| 769 |
+
"""Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...])"""
|
| 770 |
+
start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1)
|
| 771 |
+
return start, [_score_at(ind, i) for i in range(start, n)]
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
# ---------------------------------------------------------------------------
|
| 775 |
+
# BACKTEST — yön doğruluğu + gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon)
|
| 776 |
+
# ---------------------------------------------------------------------------
|
| 777 |
+
def directional_accuracy(df, start, scores, forward=5, threshold=SIGNAL_THRESHOLD):
|
| 778 |
+
"""Klasik test: sinyal 'forward' bar sonra doğru yönü göstermiş mi? (rejim kırılımlı)"""
|
| 779 |
+
c = df["c"].to_numpy()
|
| 780 |
+
n = len(c)
|
| 781 |
+
stats = {"all": [0, 0], "trend": [0, 0], "range": [0, 0]}
|
| 782 |
+
for i in range(start, n - forward):
|
| 783 |
+
s = scores[i - start]
|
| 784 |
+
if abs(s["score"]) < threshold:
|
| 785 |
+
continue
|
| 786 |
+
correct = (s["score"] > 0) == (c[i + forward] > c[i])
|
| 787 |
+
regime = "trend" if s["adx"] >= 25 else "range"
|
| 788 |
+
for key in ("all", regime):
|
| 789 |
+
stats[key][1] += 1
|
| 790 |
+
if correct:
|
| 791 |
+
stats[key][0] += 1
|
| 792 |
+
if stats["all"][1] < 5:
|
| 793 |
+
return None
|
| 794 |
+
|
| 795 |
+
def pack(key):
|
| 796 |
+
ok, t = stats[key]
|
| 797 |
+
return {"accuracy": ok / t, "n_signals": t} if t > 0 else None
|
| 798 |
+
|
| 799 |
+
return {
|
| 800 |
+
"accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0],
|
| 801 |
+
"n_signals": stats["all"][1], "forward_bars": forward,
|
| 802 |
+
"trend_regime": pack("trend"), "range_regime": pack("range"),
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def simulate_trades(df, ind, start, scores, threshold=SIGNAL_THRESHOLD):
|
| 807 |
+
"""Sinyalleri sırayla (üst üste binmeden) işleme çevirir.
|
| 808 |
+
Giriş: sinyal barını izleyen barın AÇILIŞI (gerçekçi, ileri sızıntı yok).
|
| 809 |
+
Çıkış: SL (1.5×ATR) / TP (2.5×ATR) / 24 bar zaman çıkışı. Aynı barda ikisi de değerse SL öncelikli (kötümser)."""
|
| 810 |
+
A = ind["_arr"]
|
| 811 |
+
o, h, l, c, atr_a = A["o"], A["h"], A["l"], A["c"], A["atr"]
|
| 812 |
+
n = len(c)
|
| 813 |
+
trades = []
|
| 814 |
+
i = start
|
| 815 |
+
while i <= n - 2:
|
| 816 |
+
s = scores[i - start]["score"]
|
| 817 |
+
a = atr_a[i]
|
| 818 |
+
if abs(s) < threshold or not (a > 0):
|
| 819 |
+
i += 1
|
| 820 |
+
continue
|
| 821 |
+
d = 1 if s > 0 else -1
|
| 822 |
+
entry = o[i + 1]
|
| 823 |
+
risk = SL_ATR * a
|
| 824 |
+
sl, tp = entry - d * risk, entry + d * TP_ATR * a
|
| 825 |
+
last = min(n - 1, i + 1 + MAX_HOLD_BARS)
|
| 826 |
+
exit_i, exit_px, why = None, None, None
|
| 827 |
+
for j in range(i + 1, last + 1):
|
| 828 |
+
hit_sl = (l[j] <= sl) if d > 0 else (h[j] >= sl)
|
| 829 |
+
hit_tp = (h[j] >= tp) if d > 0 else (l[j] <= tp)
|
| 830 |
+
if hit_sl:
|
| 831 |
+
exit_i, exit_px, why = j, sl, "SL"
|
| 832 |
+
break
|
| 833 |
+
if hit_tp:
|
| 834 |
+
exit_i, exit_px, why = j, tp, "TP"
|
| 835 |
+
break
|
| 836 |
+
if exit_i is None:
|
| 837 |
+
if last == i + 1 + MAX_HOLD_BARS:
|
| 838 |
+
exit_i, exit_px, why = last, c[last], "ZAMAN"
|
| 839 |
+
else:
|
| 840 |
+
break # veri bitti, işlem hâlâ açık — sonuçlandırılamaz
|
| 841 |
+
pnl = d * (exit_px - entry) / entry - 2 * FEE_PER_SIDE
|
| 842 |
+
r_mult = pnl / (risk / entry)
|
| 843 |
+
trades.append({"signal_i": i, "entry_i": i + 1, "exit_i": exit_i, "dir": d, "entry": entry,
|
| 844 |
+
"exit": exit_px, "pnl": pnl, "r": r_mult, "why": why, "score": s})
|
| 845 |
+
i = exit_i
|
| 846 |
+
return trades
|
| 847 |
+
|
| 848 |
+
|
| 849 |
+
def summarize_trades(trades, df, start):
|
| 850 |
+
if len(trades) < 3:
|
| 851 |
+
return None
|
| 852 |
+
n = len(df)
|
| 853 |
+
pnl = np.array([t["pnl"] for t in trades])
|
| 854 |
+
r = np.array([t["r"] for t in trades])
|
| 855 |
+
wins, losses = pnl[pnl > 0], pnl[pnl <= 0]
|
| 856 |
+
gross_loss = abs(losses.sum())
|
| 857 |
+
pf = float(wins.sum() / gross_loss) if gross_loss > 0 else (99.0 if wins.sum() > 0 else 0.0)
|
| 858 |
+
win_rate = len(wins) / len(pnl)
|
| 859 |
+
win_r, loss_r = r[r > 0], r[r <= 0]
|
| 860 |
+
avg_win_r = float(win_r.mean()) if len(win_r) else 0.0
|
| 861 |
+
avg_loss_r = float(abs(loss_r.mean())) if len(loss_r) else 0.0
|
| 862 |
+
kelly = None
|
| 863 |
+
if avg_win_r > 0 and avg_loss_r > 0:
|
| 864 |
+
kelly = win_rate - (1 - win_rate) / (avg_win_r / avg_loss_r)
|
| 865 |
+
sqn = float(np.sqrt(min(len(r), 100)) * r.mean() / r.std()) if r.std() > 0 else 0.0
|
| 866 |
+
|
| 867 |
+
mult = np.ones(n - start)
|
| 868 |
+
for t in trades:
|
| 869 |
+
mult[t["exit_i"] - start] *= (1 + t["pnl"])
|
| 870 |
+
equity = np.cumprod(mult)
|
| 871 |
+
peak = np.maximum.accumulate(equity)
|
| 872 |
+
max_dd = float(((equity - peak) / peak).min())
|
| 873 |
+
c = df["c"].to_numpy()
|
| 874 |
+
bh = c[start:] / c[start]
|
| 875 |
+
|
| 876 |
+
streak = best = 0
|
| 877 |
+
for p in pnl:
|
| 878 |
+
streak = streak + 1 if p <= 0 else 0
|
| 879 |
+
best = max(best, streak)
|
| 880 |
+
|
| 881 |
+
def side(dirn):
|
| 882 |
+
sub = [t for t in trades if t["dir"] == dirn]
|
| 883 |
+
return {"n": len(sub), "win_rate": (sum(1 for t in sub if t["pnl"] > 0) / len(sub)) if sub else None}
|
| 884 |
+
|
| 885 |
+
stats = {
|
| 886 |
+
"n": len(trades), "win_rate": win_rate, "profit_factor": pf, "expectancy": float(pnl.mean()),
|
| 887 |
+
"avg_r": float(r.mean()), "sqn": sqn, "max_dd": max_dd, "total_return": float(equity[-1] - 1),
|
| 888 |
+
"bh_return": float(bh[-1] - 1), "max_consec_loss": best, "avg_win_r": avg_win_r,
|
| 889 |
+
"avg_loss_r": avg_loss_r, "kelly": kelly, "long": side(1), "short": side(-1),
|
| 890 |
+
}
|
| 891 |
+
return stats, equity, bh
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
def run_backtest(df, ind, start, scores):
|
| 895 |
+
n = len(df)
|
| 896 |
+
if n - start < 20:
|
| 897 |
+
return None
|
| 898 |
+
acc = directional_accuracy(df, start, scores)
|
| 899 |
+
trades = simulate_trades(df, ind, start, scores)
|
| 900 |
+
summ = summarize_trades(trades, df, start)
|
| 901 |
+
bt = {"start": start, "acc": acc, "trades": trades, "stats": None, "equity": None, "bh": None}
|
| 902 |
+
if summ:
|
| 903 |
+
bt["stats"], bt["equity"], bt["bh"] = summ
|
| 904 |
+
return bt
|
| 905 |
+
|
| 906 |
+
|
| 907 |
+
# ---------------------------------------------------------------------------
|
| 908 |
+
# CANLI KATMAN — üst zaman dilimleri, emir defteri, funding (backtest EDİLMEZ)
|
| 909 |
+
# ---------------------------------------------------------------------------
|
| 910 |
+
def _mtf_score(symbol, bar, closed_only):
|
| 911 |
+
hdf = fetch_candles(symbol, bar, 300)
|
| 912 |
+
if closed_only:
|
| 913 |
+
hdf = hdf[hdf["confirm"] == 1].reset_index(drop=True)
|
| 914 |
+
hind = compute_indicators(hdf)
|
| 915 |
+
r = _score_at(hind, len(hdf) - 1)
|
| 916 |
+
return {"bar": bar, "score": r["score"]}
|
| 917 |
+
|
| 918 |
+
|
| 919 |
+
def _fetch_live_extras(symbol, bar, mtf, micro, closed_only):
|
| 920 |
+
"""MTF / emir defteri / funding çağrılarını PARALEL yapar; hata veren parça sessizce atlanır."""
|
| 921 |
+
tasks = {}
|
| 922 |
+
with ThreadPoolExecutor(max_workers=4) as ex:
|
| 923 |
+
if mtf and symbol and bar:
|
| 924 |
+
b1 = MTF_MAP.get(bar)
|
| 925 |
+
if b1 and b1 != bar:
|
| 926 |
+
tasks["mtf1"] = ex.submit(_mtf_score, symbol, b1, closed_only)
|
| 927 |
+
b2 = MTF_MAP.get(b1)
|
| 928 |
+
if b2 and b2 not in (b1, bar):
|
| 929 |
+
tasks["mtf2"] = ex.submit(_mtf_score, symbol, b2, closed_only)
|
| 930 |
+
if micro and symbol:
|
| 931 |
+
tasks["ob"] = ex.submit(fetch_orderbook, symbol, 20)
|
| 932 |
+
tasks["fund"] = ex.submit(fetch_funding_rate, symbol)
|
| 933 |
+
out = {}
|
| 934 |
+
for k, f in tasks.items():
|
| 935 |
+
try:
|
| 936 |
+
out[k] = f.result()
|
| 937 |
+
except Exception:
|
| 938 |
+
out[k] = None
|
| 939 |
+
return out
|
| 940 |
+
|
| 941 |
+
|
| 942 |
+
def build_trade_plan(price, direction, atr_v, supports, resists, acct=None, risk_pct=None):
|
| 943 |
+
"""Yapısal (destek/direnç) stop uygun aralıktaysa onu, değilse ATR stop'unu kullanır.
|
| 944 |
+
3 kademeli hedef + hesap büyüklüğüne göre pozisyon boyutu."""
|
| 945 |
+
d = direction
|
| 946 |
+
stop, stop_type = price - d * SL_ATR * atr_v, f"ATR × {SL_ATR}"
|
| 947 |
+
cand = None
|
| 948 |
+
if d > 0 and supports:
|
| 949 |
+
cand = supports[0][0] - 0.25 * atr_v
|
| 950 |
+
elif d < 0 and resists:
|
| 951 |
+
cand = resists[0][0] + 0.25 * atr_v
|
| 952 |
+
if cand is not None:
|
| 953 |
+
dist = abs(price - cand)
|
| 954 |
+
if 0.8 * atr_v <= dist <= 3.0 * atr_v and (cand < price if d > 0 else cand > price):
|
| 955 |
+
stop, stop_type = cand, "Yapısal (S/R arkası)"
|
| 956 |
+
risk = abs(price - stop)
|
| 957 |
+
plan = {
|
| 958 |
+
"direction": "LONG" if d > 0 else "SHORT", "entry": price, "stop_loss": stop, "stop_type": stop_type,
|
| 959 |
+
"risk_dist": risk, "risk_frac": risk / price,
|
| 960 |
+
"targets": [(price + d * risk * m, m) for m in TP_R_MULTIPLES], "sizing": None,
|
| 961 |
+
}
|
| 962 |
+
if acct and acct > 0 and risk_pct and risk_pct > 0 and risk > 0:
|
| 963 |
+
risk_amt = acct * risk_pct / 100.0
|
| 964 |
+
qty = risk_amt / risk
|
| 965 |
+
notional = qty * price
|
| 966 |
+
plan["sizing"] = {"risk_amount": risk_amt, "qty": qty, "notional": notional, "leverage": notional / acct}
|
| 967 |
+
return plan
|
| 968 |
+
|
| 969 |
+
|
| 970 |
+
def compute_confidence(score, agree, mtf_state, hist_q, vol_q):
|
| 971 |
+
mag = min(abs(score) / 8.0, 1.0)
|
| 972 |
+
conf = 100.0 * (0.35 * mag + 0.25 * agree + 0.15 * mtf_state + 0.20 * hist_q + 0.05 * vol_q)
|
| 973 |
+
parts = {"Skor Büyüklüğü": mag, "Faktör Uyumu": agree, "MTF Teyidi": mtf_state,
|
| 974 |
+
"Tarihsel Kenar": hist_q, "Volatilite Rejimi": vol_q}
|
| 975 |
+
return min(conf, 100.0), parts
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
def grade_from_conf(conf):
|
| 979 |
+
for thr, g, col in ((75, "A+", "#3ddc97"), (62, "A", "#7fe6b8"), (50, "B", "#f2b84b"), (38, "C", "#ff9aa8")):
|
| 980 |
+
if conf >= thr:
|
| 981 |
+
return g, col
|
| 982 |
+
return "D", "#ff5c72"
|
| 983 |
+
|
| 984 |
+
|
| 985 |
+
def generate_signal(df, symbol=None, bar=None, mtf=True, micro=True, closed_only=False,
|
| 986 |
+
acct=None, risk_pct=None):
|
| 987 |
+
ind = compute_indicators(df)
|
| 988 |
+
n = len(df)
|
| 989 |
+
start, scores = compute_score_series(ind, n)
|
| 990 |
+
base = _score_at(ind, n - 1, verbose=True)
|
| 991 |
+
score, price, w = base["score"], base["price"], base["w"]
|
| 992 |
+
reasons, contribs = list(base["reasons"]), list(base["contribs"])
|
| 993 |
+
tags = []
|
| 994 |
+
if base["squeeze_on"]:
|
| 995 |
+
tags.append(("SIKIŞMA AKTİF", "#f2b84b"))
|
| 996 |
+
for name, sgn in base["pat_hits"]:
|
| 997 |
+
tags.append((name.upper(), "#3ddc97" if sgn > 0 else "#ff5c72"))
|
| 998 |
+
if base["hilo_flip"]:
|
| 999 |
+
tags.append(("HiLo YUKARI DÖNÜŞ" if base["hilo_flip"] > 0 else "HiLo AŞAĞI DÖNÜŞ",
|
| 1000 |
+
"#3ddc97" if base["hilo_flip"] > 0 else "#ff5c72"))
|
| 1001 |
+
if base["fib_zone"]:
|
| 1002 |
+
tags.append(("FİB ALTIN BÖLGE ▲" if base["fib_zone"] > 0 else "FİB ALTIN BÖLGE ▼",
|
| 1003 |
+
"#3ddc97" if base["fib_zone"] > 0 else "#ff5c72"))
|
| 1004 |
+
|
| 1005 |
+
def overlay(label, delta, text):
|
| 1006 |
+
nonlocal score
|
| 1007 |
+
score += delta
|
| 1008 |
+
reasons.append(("up" if delta > 0 else "down", text))
|
| 1009 |
+
contribs.append((label, delta))
|
| 1010 |
+
|
| 1011 |
+
# --- Uyumsuzluk (RSI divergence) — reversion sinyali, rejime göre ağırlıklı ---
|
| 1012 |
+
div = detect_divergence(df, ind)
|
| 1013 |
+
div_weight = 1.5 - 1.1 * w # güçlü trendde 0.4, yatayda 1.5 (eski davranışla aynı uçlar)
|
| 1014 |
+
if div["bullish"]:
|
| 1015 |
+
overlay("Yükseliş Uyumsuzluğu", div_weight, "Yükseliş uyumsuzluğu tespit edildi (fiyat düşük dip, RSI yüksek dip)")
|
| 1016 |
+
tags.append(("YÜKSELİŞ UYUMSUZLUĞU", "#3ddc97"))
|
| 1017 |
+
if div["bearish"]:
|
| 1018 |
+
overlay("Düşüş Uyumsuzluğu", -div_weight, "Düşüş uyumsuzluğu tespit edildi (fiyat yüksek tepe, RSI düşük tepe)")
|
| 1019 |
+
tags.append(("DÜŞÜŞ UYUMSUZLUĞU", "#ff5c72"))
|
| 1020 |
+
|
| 1021 |
+
# --- Skor ivmesi ---
|
| 1022 |
+
if n - 4 >= start:
|
| 1023 |
+
momentum = base["score"] - scores[n - 4 - start]["score"]
|
| 1024 |
+
if momentum > 1.5:
|
| 1025 |
+
overlay("Skor İvmesi", 0.3, "Kompozit skor son barlarda hızla güçleniyor")
|
| 1026 |
+
elif momentum < -1.5:
|
| 1027 |
+
overlay("Skor İvmesi", -0.3, "Kompozit skor son barlarda hızla zayıflıyor")
|
| 1028 |
+
|
| 1029 |
+
# --- Canlı ekstralar (paralel çekim) ---
|
| 1030 |
+
extras = _fetch_live_extras(symbol, bar, mtf, micro, closed_only)
|
| 1031 |
+
sgn = 1 if score > 0 else (-1 if score < 0 else 0)
|
| 1032 |
+
|
| 1033 |
+
# --- 2 kademeli MTF teyidi ---
|
| 1034 |
+
mtf_info, align_flags = [], []
|
| 1035 |
+
for key, wt in (("mtf1", 1.0), ("mtf2", 0.6)):
|
| 1036 |
+
m = extras.get(key)
|
| 1037 |
+
if not m:
|
| 1038 |
+
continue
|
| 1039 |
+
hs = m["score"]
|
| 1040 |
+
aligned = None
|
| 1041 |
+
if abs(hs) >= 1.0 and sgn != 0:
|
| 1042 |
+
aligned = bool(np.sign(hs) == sgn)
|
| 1043 |
+
align_flags.append(aligned)
|
| 1044 |
+
if aligned:
|
| 1045 |
+
overlay(f"{m['bar']} Teyidi", sgn * wt, f"{m['bar']} zaman diliminde de aynı yönlü teyit var (skor {hs:+.1f})")
|
| 1046 |
+
else:
|
| 1047 |
+
overlay(f"{m['bar']} Çelişkisi", -sgn * wt, f"{m['bar']} zaman dilimi ters yönlü sinyal veriyor (skor {hs:+.1f}) — dikkat")
|
| 1048 |
+
mtf_info.append({"bar": m["bar"], "score": round(hs, 2), "aligned": aligned})
|
| 1049 |
+
mtf_state = (sum(1.0 for a in align_flags if a) / len(align_flags)) if align_flags else 0.5
|
| 1050 |
+
mtf_opposed = any(a is False for a in align_flags)
|
| 1051 |
+
|
| 1052 |
+
# --- Emir defteri dengesizliği (derinlik ağırlıklı, kısa vadede anlamlı) ---
|
| 1053 |
+
imb = None
|
| 1054 |
+
ob = extras.get("ob")
|
| 1055 |
+
if ob and ob.get("bids") and ob.get("asks"):
|
| 1056 |
+
bw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["bids"]))
|
| 1057 |
+
aw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["asks"]))
|
| 1058 |
+
if bw + aw > 0:
|
| 1059 |
+
imb = (bw - aw) / (bw + aw)
|
| 1060 |
+
if abs(imb) >= 0.2:
|
| 1061 |
+
delta = max(-0.8, min(0.8, imb * 2)) * (0.5 if bar in SLOW_BARS else 1.0)
|
| 1062 |
+
overlay("Emir Defteri", delta,
|
| 1063 |
+
f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)")
|
| 1064 |
+
|
| 1065 |
+
# --- Funding rate (kalabalık pozisyon = kontraryen) ---
|
| 1066 |
+
fr = extras.get("fund")
|
| 1067 |
+
if fr is not None:
|
| 1068 |
+
if fr > 0.0005:
|
| 1069 |
+
overlay("Funding", -0.6, f"Funding aşırı pozitif ({fr * 100:.3f}%) — kalabalık long, tasfiye riski")
|
| 1070 |
+
elif fr > 0.0002:
|
| 1071 |
+
overlay("Funding", -0.3, f"Funding yüksek pozitif ({fr * 100:.3f}%) — long tarafı kalabalık")
|
| 1072 |
+
elif fr < -0.0005:
|
| 1073 |
+
overlay("Funding", 0.6, f"Funding aşırı negatif ({fr * 100:.3f}%) — kalabalık short, sıkışma potansiyeli")
|
| 1074 |
+
elif fr < -0.0002:
|
| 1075 |
+
overlay("Funding", 0.3, f"Funding negatif ({fr * 100:.3f}%) — short tarafı kalabalık")
|
| 1076 |
+
|
| 1077 |
+
# --- Destek / direnç yakınlığı ---
|
| 1078 |
+
atr_v = float(ind["atr"].iloc[-1])
|
| 1079 |
+
supports, resists = find_sr_levels(df, atr_v)
|
| 1080 |
+
fib = None
|
| 1081 |
+
_fh, _fl, _fd = (float(ind["fib_hi"].iloc[-1]), float(ind["fib_lo"].iloc[-1]), float(ind["fib_dir"].iloc[-1]))
|
| 1082 |
+
if _fh == _fh and _fl == _fl and _fd == _fd and atr_v > 0 and (_fh - _fl) >= FIB_MIN_RANGE_ATR * atr_v:
|
| 1083 |
+
fib = {"hi": _fh, "lo": _fl, "dir": int(_fd), "levels": fib_levels(_fh, _fl, _fd), "depth": base["fib_depth"]}
|
| 1084 |
+
sgn = 1 if score > 0 else (-1 if score < 0 else 0)
|
| 1085 |
+
if atr_v > 0 and sgn != 0:
|
| 1086 |
+
if sgn > 0:
|
| 1087 |
+
if resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0:
|
| 1088 |
+
lv, tc = resists[0]
|
| 1089 |
+
overlay("Yakın Direnç", -0.8, f"Yakın direnç {fmt_price(lv)} ({tc} temas, {(lv - price) / atr_v:.1f} ATR) — yükselişi sınırlayabilir")
|
| 1090 |
+
elif supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 0.5:
|
| 1091 |
+
lv, tc = supports[0]
|
| 1092 |
+
overlay("Destek Üstünde", 0.4, f"Fiyat güçlü destek {fmt_price(lv)} ({tc} temas) üzerinde")
|
| 1093 |
+
else:
|
| 1094 |
+
if supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0:
|
| 1095 |
+
lv, tc = supports[0]
|
| 1096 |
+
overlay("Yakın Destek", 0.8, f"Yakın destek {fmt_price(lv)} ({tc} temas, {(price - lv) / atr_v:.1f} ATR) — düşüşü sınırlayabilir")
|
| 1097 |
+
elif resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 0.5:
|
| 1098 |
+
lv, tc = resists[0]
|
| 1099 |
+
overlay("Direnç Altında", -0.4, f"Fiyat güçlü direnç {fmt_price(lv)} ({tc} temas) altında")
|
| 1100 |
+
|
| 1101 |
+
score = max(-10.0, min(10.0, score))
|
| 1102 |
+
|
| 1103 |
+
# --- Konsensüs + sınıflandırma ---
|
| 1104 |
+
direction = 1 if score > 0 else -1
|
| 1105 |
+
tot = base["bull_w"] + base["bear_w"]
|
| 1106 |
+
agree = ((base["bull_w"] if direction > 0 else base["bear_w"]) / tot) if tot > 0 else 0.5
|
| 1107 |
+
if abs(score) >= STRONG_THRESHOLD and agree >= 0.65 and not mtf_opposed:
|
| 1108 |
+
signal, color = ("GÜÇLÜ AL", "#3ddc97") if score > 0 else ("GÜÇLÜ SAT", "#ff5c72")
|
| 1109 |
+
elif abs(score) >= SIGNAL_THRESHOLD:
|
| 1110 |
+
signal, color = ("AL", "#7fe6b8") if score > 0 else ("SAT", "#ff9aa8")
|
| 1111 |
+
if abs(score) >= STRONG_THRESHOLD:
|
| 1112 |
+
reasons.append(("flat", f"Skor güçlü bölgede ancak konsensüs ({agree * 100:.0f}%) veya MTF filtresini geçemedi — GÜÇLÜ yerine normal sinyal verildi"))
|
| 1113 |
+
else:
|
| 1114 |
+
signal, color = "BEKLE / NÖTR", "#f2b84b"
|
| 1115 |
+
is_neutral = signal == "BEKLE / NÖTR"
|
| 1116 |
+
|
| 1117 |
+
# --- Tarihsel doğrulama ---
|
| 1118 |
+
backtest = run_backtest(df, ind, start, scores)
|
| 1119 |
+
hist_q = 0.4
|
| 1120 |
+
if backtest:
|
| 1121 |
+
qs = []
|
| 1122 |
+
acc = backtest["acc"]
|
| 1123 |
+
if acc:
|
| 1124 |
+
p_shrunk = (acc["correct"] + 5) / (acc["n_signals"] + 10) # Bayes büzülmesi (önsel %50)
|
| 1125 |
+
qs.append(min(max((p_shrunk - 0.45) / 0.20, 0.0), 1.0))
|
| 1126 |
+
st = backtest["stats"]
|
| 1127 |
+
if st:
|
| 1128 |
+
qs.append(min(max((st["profit_factor"] - 0.8) / 0.8, 0.0), 1.0))
|
| 1129 |
+
if qs:
|
| 1130 |
+
hist_q = float(np.mean(qs))
|
| 1131 |
+
rank = base["atr_rank"]
|
| 1132 |
+
vol_q = 1.0 if 0.10 <= rank <= 0.90 else 0.5
|
| 1133 |
+
if rank > 0.90:
|
| 1134 |
+
reasons.append(("flat", f"Volatilite aşırı yüksek (ATR yüzdeliği %{rank * 100:.0f}) — stoplar geniş tutulmalı, güven düşürüldü"))
|
| 1135 |
+
conf, conf_parts = compute_confidence(score, agree, mtf_state, hist_q, vol_q)
|
| 1136 |
+
grade, grade_color = ("—", "#7c8797") if is_neutral else grade_from_conf(conf)
|
| 1137 |
+
|
| 1138 |
+
plan = None
|
| 1139 |
+
if not is_neutral and atr_v > 0:
|
| 1140 |
+
plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct)
|
| 1141 |
+
|
| 1142 |
+
return {
|
| 1143 |
+
"signal": signal, "score": round(score, 2), "confidence": round(conf, 1), "grade": grade,
|
| 1144 |
+
"grade_color": grade_color, "color": color, "reasons": reasons, "contribs": contribs,
|
| 1145 |
+
"indicators": ind, "price": price, "divergence": div, "mtf": mtf_info, "plan": plan,
|
| 1146 |
+
"backtest": backtest, "regime": base["regime"], "adx": base["adx"], "atr_rank": rank,
|
| 1147 |
+
"squeeze_on": base["squeeze_on"], "agree": agree, "bull_w": base["bull_w"], "bear_w": base["bear_w"],
|
| 1148 |
+
"conf_parts": conf_parts, "tags": tags, "funding": fr, "imbalance": imb,
|
| 1149 |
+
"supports": supports, "resists": resists, "atr": atr_v, "fib": fib,
|
| 1150 |
+
"hilo_dir": int(ind["hilo_dir"].iloc[-1]),
|
| 1151 |
+
}
|
| 1152 |
+
|
| 1153 |
+
|
| 1154 |
+
# ---------------------------------------------------------------------------
|
| 1155 |
+
# RISK ANALYTICS
|
| 1156 |
+
# ---------------------------------------------------------------------------
|
| 1157 |
+
def risk_metrics(df, bar):
|
| 1158 |
+
ret = df["c"].pct_change().dropna()
|
| 1159 |
+
if len(ret) < 5:
|
| 1160 |
+
return None
|
| 1161 |
+
periods = BAR_TO_PERIODS_PER_YEAR.get(bar, 365)
|
| 1162 |
+
vol_annual = ret.std() * np.sqrt(periods)
|
| 1163 |
+
mean_annual = ret.mean() * periods
|
| 1164 |
+
sharpe = mean_annual / vol_annual if vol_annual > 0 else 0.0
|
| 1165 |
+
cum = (1 + ret).cumprod()
|
| 1166 |
+
peak = cum.cummax()
|
| 1167 |
+
dd = (cum - peak) / peak
|
| 1168 |
+
var95 = np.percentile(ret, 5)
|
| 1169 |
+
tail = ret[ret <= var95]
|
| 1170 |
+
cvar95 = tail.mean() if len(tail) else var95
|
| 1171 |
+
return {
|
| 1172 |
+
"volatility_annual": vol_annual, "return_annual": mean_annual, "sharpe": sharpe,
|
| 1173 |
+
"max_drawdown": dd.min(), "current_dd": dd.iloc[-1],
|
| 1174 |
+
"var95_bar": var95, "cvar95_bar": cvar95,
|
| 1175 |
+
}
|
| 1176 |
+
|
| 1177 |
+
|
| 1178 |
+
# ---------------------------------------------------------------------------
|
| 1179 |
+
# FORMAT HELPERS
|
| 1180 |
+
# ---------------------------------------------------------------------------
|
| 1181 |
+
def _isnan(v):
|
| 1182 |
+
return v is None or (isinstance(v, float) and math.isnan(v))
|
| 1183 |
+
|
| 1184 |
+
|
| 1185 |
+
def fmt_price(v):
|
| 1186 |
+
if _isnan(v):
|
| 1187 |
+
return "—"
|
| 1188 |
+
av = abs(v)
|
| 1189 |
+
if av >= 1000:
|
| 1190 |
+
return f"{v:,.2f}"
|
| 1191 |
+
if av >= 1:
|
| 1192 |
+
return f"{v:,.4f}"
|
| 1193 |
+
if av >= 0.01:
|
| 1194 |
+
return f"{v:.6f}"
|
| 1195 |
+
return f"{v:.8f}"
|
| 1196 |
+
|
| 1197 |
+
|
| 1198 |
+
def fmt_pct(v):
|
| 1199 |
+
if _isnan(v):
|
| 1200 |
+
return "—"
|
| 1201 |
+
sign = "+" if v > 0 else ""
|
| 1202 |
+
return f"{sign}{v * 100:.2f}%"
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
def fmt_large(v):
|
| 1206 |
+
if _isnan(v):
|
| 1207 |
+
return "—"
|
| 1208 |
+
av = abs(v)
|
| 1209 |
+
if av >= 1e9:
|
| 1210 |
+
return f"{v / 1e9:.2f}B"
|
| 1211 |
+
if av >= 1e6:
|
| 1212 |
+
return f"{v / 1e6:.2f}M"
|
| 1213 |
+
if av >= 1e3:
|
| 1214 |
+
return f"{v / 1e3:.2f}K"
|
| 1215 |
+
return f"{v:.2f}"
|
| 1216 |
+
|
| 1217 |
+
|
| 1218 |
+
def metric_card(label, value, sub="", tone="neutral"):
|
| 1219 |
+
tone_class = {"up": "mc-up", "down": "mc-down", "neutral": "mc-neutral"}.get(tone, "mc-neutral")
|
| 1220 |
+
sub_html = f'<div class="mc-sub">{sub}</div>' if sub else ""
|
| 1221 |
+
return (f'<div class="metric-card {tone_class}"><div class="mc-label">{label}</div>'
|
| 1222 |
+
f'<div class="mc-value">{value}</div>{sub_html}</div>')
|
| 1223 |
+
|
| 1224 |
+
|
| 1225 |
+
def section(title):
|
| 1226 |
+
return f'<div class="section-title">{title}</div>'
|
| 1227 |
+
|
| 1228 |
+
|
| 1229 |
+
def cards_row(cards):
|
| 1230 |
+
return f'<div class="metric-row">{"".join(cards)}</div>' if cards else ""
|
| 1231 |
+
|
| 1232 |
+
|
| 1233 |
+
def error_box(msg):
|
| 1234 |
+
return f'<div class="error-box">⚠ {msg}</div>'
|
| 1235 |
+
|
| 1236 |
+
|
| 1237 |
+
# ---------------------------------------------------------------------------
|
| 1238 |
+
# CHART BUILDERS
|
| 1239 |
+
# ---------------------------------------------------------------------------
|
| 1240 |
+
def _base_layout(fig, title, height):
|
| 1241 |
+
fig.update_layout(
|
| 1242 |
+
template="plotly_dark", height=height, margin=dict(l=10, r=10, t=40, b=10),
|
| 1243 |
+
paper_bgcolor=CHART_BG, plot_bgcolor=CHART_BG, title=title,
|
| 1244 |
+
legend=dict(orientation="h", y=1.05, font=dict(size=10)),
|
| 1245 |
+
font=dict(family="IBM Plex Mono, monospace", size=11),
|
| 1246 |
+
)
|
| 1247 |
+
return fig
|
| 1248 |
+
|
| 1249 |
+
|
| 1250 |
+
def _empty_fig(msg, title="", height=220):
|
| 1251 |
+
fig = go.Figure()
|
| 1252 |
+
fig.add_annotation(text=msg, x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False,
|
| 1253 |
+
font=dict(size=13, color="#7c8797"))
|
| 1254 |
+
fig.update_xaxes(visible=False)
|
| 1255 |
+
fig.update_yaxes(visible=False)
|
| 1256 |
+
return _base_layout(fig, title, height)
|
| 1257 |
+
|
| 1258 |
+
|
| 1259 |
+
def build_price_chart(df, ind, symbol, bar):
|
| 1260 |
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.75, 0.25], vertical_spacing=0.03)
|
| 1261 |
+
fig.add_trace(go.Candlestick(
|
| 1262 |
+
x=df["ts"], open=df["o"], high=df["h"], low=df["l"], close=df["c"], name=symbol,
|
| 1263 |
+
increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"), row=1, col=1)
|
| 1264 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema20"], name="EMA20",
|
| 1265 |
+
line=dict(color="#5b8cff", width=1)), row=1, col=1)
|
| 1266 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema50"], name="EMA50",
|
| 1267 |
+
line=dict(color="#f2b84b", width=1)), row=1, col=1)
|
| 1268 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_upper"], name="BB Üst",
|
| 1269 |
+
line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot")), row=1, col=1)
|
| 1270 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_lower"], name="BB Alt",
|
| 1271 |
+
line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot"),
|
| 1272 |
+
fill="tonexty", fillcolor="rgba(150,160,255,0.05)"), row=1, col=1)
|
| 1273 |
+
vol_colors = np.where(df["c"] >= df["o"], "#3ddc97", "#ff5c72")
|
| 1274 |
+
fig.add_trace(go.Bar(x=df["ts"], y=df["vol"], name="Hacim", marker_color=vol_colors, opacity=0.55),
|
| 1275 |
+
row=2, col=1)
|
| 1276 |
+
fig.update_layout(xaxis_rangeslider_visible=False)
|
| 1277 |
+
return _base_layout(fig, f"{symbol} · {bar}", 520)
|
| 1278 |
+
|
| 1279 |
+
|
| 1280 |
+
def build_signal_price_chart(df, ind, symbol, bar, res):
|
| 1281 |
+
"""Sinyal sekmesinin ana grafiği: mum + Supertrend + HiLo + Fibonacci + Ichimoku bulutu + S/R + geçmiş sinyal işaretleri + işlem planı."""
|
| 1282 |
+
n = len(df)
|
| 1283 |
+
v0 = max(0, n - 200)
|
| 1284 |
+
sl = slice(v0, n)
|
| 1285 |
+
ts = df["ts"].iloc[sl]
|
| 1286 |
+
fig = go.Figure()
|
| 1287 |
+
fig.add_trace(go.Candlestick(
|
| 1288 |
+
x=ts, open=df["o"].iloc[sl], high=df["h"].iloc[sl], low=df["l"].iloc[sl], close=df["c"].iloc[sl],
|
| 1289 |
+
name=symbol, increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"))
|
| 1290 |
+
# Ichimoku bulutu
|
| 1291 |
+
fig.add_trace(go.Scatter(x=ts, y=ind["ich_a"].iloc[sl], line=dict(width=0), showlegend=False, hoverinfo="skip"))
|
| 1292 |
+
fig.add_trace(go.Scatter(x=ts, y=ind["ich_b"].iloc[sl], line=dict(width=0), name="Ichimoku Bulutu",
|
| 1293 |
+
fill="tonexty", fillcolor="rgba(120,140,255,0.10)", hoverinfo="skip"))
|
| 1294 |
+
# Supertrend (yükseliş/düşüş ayrı renk)
|
| 1295 |
+
st_line, st_dir = ind["st_line"].iloc[sl], ind["st_dir"].iloc[sl]
|
| 1296 |
+
fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir > 0), name="Supertrend ▲",
|
| 1297 |
+
line=dict(color="#3ddc97", width=1.5), connectgaps=False))
|
| 1298 |
+
fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir < 0), name="Supertrend ▼",
|
| 1299 |
+
line=dict(color="#ff5c72", width=1.5), connectgaps=False))
|
| 1300 |
+
# Gann HiLo Activator (yükseliş/düşüş ayrı renk)
|
| 1301 |
+
hl_line, hl_dir = ind["hilo_line"].iloc[sl], ind["hilo_dir"].iloc[sl]
|
| 1302 |
+
fig.add_trace(go.Scatter(x=ts, y=hl_line.where(hl_dir > 0), name="HiLo ▲", mode="lines",
|
| 1303 |
+
line=dict(color="#7fe6b8", width=1.2, dash="dot", shape="hv"), connectgaps=False))
|
| 1304 |
+
fig.add_trace(go.Scatter(x=ts, y=hl_line.where(hl_dir < 0), name="HiLo ▼", mode="lines",
|
| 1305 |
+
line=dict(color="#ff9aa8", width=1.2, dash="dot", shape="hv"), connectgaps=False))
|
| 1306 |
+
# Fibonacci düzeltme seviyeleri
|
| 1307 |
+
fib = res.get("fib")
|
| 1308 |
+
if fib:
|
| 1309 |
+
for r, px in fib["levels"]:
|
| 1310 |
+
key = r in (0.382, 0.5, 0.618)
|
| 1311 |
+
fig.add_hline(y=px, line=dict(color="rgba(242,184,75,%s)" % ("0.65" if key else "0.3"), width=1 if key else 0.7, dash="dashdot"),
|
| 1312 |
+
annotation_text=f"Fib {r:g} · {fmt_price(px)}", annotation_position="right",
|
| 1313 |
+
annotation_font=dict(size=9, color="#f2b84b"))
|
| 1314 |
+
# Destek / direnç
|
| 1315 |
+
for lv, tc in res["supports"][:3]:
|
| 1316 |
+
fig.add_hline(y=lv, line=dict(color="rgba(61,220,151,0.55)", dash="dash", width=1),
|
| 1317 |
+
annotation_text=f"D {fmt_price(lv)} ×{tc}", annotation_position="left",
|
| 1318 |
+
annotation_font=dict(size=9, color="#3ddc97"))
|
| 1319 |
+
for lv, tc in res["resists"][:3]:
|
| 1320 |
+
fig.add_hline(y=lv, line=dict(color="rgba(255,92,114,0.55)", dash="dash", width=1),
|
| 1321 |
+
annotation_text=f"R {fmt_price(lv)} ×{tc}", annotation_position="left",
|
| 1322 |
+
annotation_font=dict(size=9, color="#ff5c72"))
|
| 1323 |
+
# Geçmiş sinyal işlemleri
|
| 1324 |
+
bt = res["backtest"]
|
| 1325 |
+
if bt and bt["trades"]:
|
| 1326 |
+
atr_a = ind["_arr"]["atr"]
|
| 1327 |
+
for d, sym_, col, nm in ((1, "triangle-up", "#3ddc97", "Geçmiş LONG"), (-1, "triangle-down", "#ff5c72", "Geçmiş SHORT")):
|
| 1328 |
+
tr = [t for t in bt["trades"] if t["dir"] == d and t["entry_i"] >= v0]
|
| 1329 |
+
if not tr:
|
| 1330 |
+
continue
|
| 1331 |
+
xs = [df["ts"].iloc[t["entry_i"]] for t in tr]
|
| 1332 |
+
ys = [(df["l"].iloc[t["entry_i"]] - 0.6 * atr_a[t["entry_i"]]) if d > 0
|
| 1333 |
+
else (df["h"].iloc[t["entry_i"]] + 0.6 * atr_a[t["entry_i"]]) for t in tr]
|
| 1334 |
+
hov = [f"{nm} · skor {t['score']:+.1f}<br>Sonuç: {t['why']} ({t['pnl'] * 100:+.2f}% · {t['r']:+.2f}R)" for t in tr]
|
| 1335 |
+
fig.add_trace(go.Scatter(x=xs, y=ys, mode="markers", name=nm, text=hov, hoverinfo="text",
|
| 1336 |
+
marker=dict(symbol=sym_, size=9, color=col, line=dict(width=0.5, color="#0a0e14"))))
|
| 1337 |
+
# Güncel işlem planı
|
| 1338 |
+
plan = res["plan"]
|
| 1339 |
+
if plan:
|
| 1340 |
+
x0, x1 = df["ts"].iloc[max(0, n - 30)], df["ts"].iloc[-1]
|
| 1341 |
+
lines = [("GİRİŞ", plan["entry"], "#e6ebf2"), ("SL", plan["stop_loss"], "#ff5c72")]
|
| 1342 |
+
lines += [(f"TP{k + 1}", px, "#3ddc97") for k, (px, _) in enumerate(plan["targets"])]
|
| 1343 |
+
for lab, y, col in lines:
|
| 1344 |
+
fig.add_shape(type="line", x0=x0, x1=x1, y0=y, y1=y, line=dict(color=col, width=1.4, dash="dot"))
|
| 1345 |
+
fig.add_annotation(x=x1, y=y, text=f"{lab} {fmt_price(y)}", showarrow=False, xanchor="left",
|
| 1346 |
+
font=dict(size=9, color=col))
|
| 1347 |
+
fig.update_layout(xaxis_rangeslider_visible=False)
|
| 1348 |
+
return _base_layout(fig, f"{symbol} · {bar} · Sinyal Haritası", 560)
|
| 1349 |
+
|
| 1350 |
+
|
| 1351 |
+
def build_factor_chart(contribs, symbol):
|
| 1352 |
+
items = [(l, v) for l, v in contribs if abs(v) > 1e-9]
|
| 1353 |
+
if not items:
|
| 1354 |
+
return _empty_fig("Faktör verisi yok", f"{symbol} · Faktör Katkıları")
|
| 1355 |
+
items.sort(key=lambda x: x[1])
|
| 1356 |
+
labels, vals = [l for l, _ in items], [v for _, v in items]
|
| 1357 |
+
fig = go.Figure(go.Bar(
|
| 1358 |
+
x=vals, y=labels, orientation="h", marker_color=["#3ddc97" if v > 0 else "#ff5c72" for v in vals],
|
| 1359 |
+
text=[f"{v:+.2f}" for v in vals], textposition="outside", cliponaxis=False))
|
| 1360 |
+
fig.update_layout(showlegend=False, xaxis_title="Ağırlıklı katkı (rejim ağırlığı uygulanmış)")
|
| 1361 |
+
return _base_layout(fig, f"{symbol} · Faktör Katkıları (skoru ne yönlendiriyor?)", max(300, 25 * len(items) + 100))
|
| 1362 |
+
|
| 1363 |
+
|
| 1364 |
+
def build_backtest_chart(bt, df, symbol):
|
| 1365 |
+
if not bt or not bt.get("stats"):
|
| 1366 |
+
return _empty_fig("Backtest için yeterli işlem oluşmadı (en az 3 işlem gerekir)", f"{symbol} · Sinyal Backtesti")
|
| 1367 |
+
start = bt["start"]
|
| 1368 |
+
ts = df["ts"].iloc[start:]
|
| 1369 |
+
fig = make_subplots(rows=2, cols=1, row_heights=[0.62, 0.38], vertical_spacing=0.14,
|
| 1370 |
+
subplot_titles=("Sinyal Stratejisi vs Al&Tut (komisyon dahil, tam pozisyon)", "İşlem Bazında R Çarpanı"))
|
| 1371 |
+
fig.add_trace(go.Scatter(x=ts, y=bt["equity"], name="Sinyal Stratejisi", line=dict(color="#5b8cff", width=2, shape="hv")),
|
| 1372 |
+
row=1, col=1)
|
| 1373 |
+
fig.add_trace(go.Scatter(x=ts, y=bt["bh"], name="Al ve Tut", line=dict(color="#7c8797", width=1.2)), row=1, col=1)
|
| 1374 |
+
rs = [t["r"] for t in bt["trades"]]
|
| 1375 |
+
fig.add_trace(go.Bar(x=list(range(1, len(rs) + 1)), y=rs, name="R",
|
| 1376 |
+
marker_color=["#3ddc97" if r > 0 else "#ff5c72" for r in rs],
|
| 1377 |
+
text=[t["why"] for t in bt["trades"]], hoverinfo="text+y"), row=2, col=1)
|
| 1378 |
+
fig.update_xaxes(title_text="İşlem #", row=2, col=1)
|
| 1379 |
+
return _base_layout(fig, f"{symbol} · Walk-Forward İşlem Simülasyonu", 470)
|
| 1380 |
+
|
| 1381 |
+
|
| 1382 |
+
def build_signal_chart(df, ind, symbol):
|
| 1383 |
+
fig = make_subplots(rows=4, cols=1, shared_xaxes=True, row_heights=[0.27, 0.25, 0.24, 0.24],
|
| 1384 |
+
vertical_spacing=0.05,
|
| 1385 |
+
subplot_titles=("RSI (14)", "MACD (12,26,9)", "Stokastik (14,3)", "ADX / DI (14)"))
|
| 1386 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["rsi"], name="RSI", line=dict(color="#5b8cff")), row=1, col=1)
|
| 1387 |
+
fig.add_hline(y=70, line=dict(color="#ff5c72", dash="dot", width=1), row=1, col=1)
|
| 1388 |
+
fig.add_hline(y=30, line=dict(color="#3ddc97", dash="dot", width=1), row=1, col=1)
|
| 1389 |
+
|
| 1390 |
+
hist_colors = np.where(ind["macd_hist"] >= 0, "#3ddc97", "#ff5c72")
|
| 1391 |
+
fig.add_trace(go.Bar(x=df["ts"], y=ind["macd_hist"], name="Histogram", marker_color=hist_colors), row=2, col=1)
|
| 1392 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_line"], name="MACD",
|
| 1393 |
+
line=dict(color="#f2b84b", width=1)), row=2, col=1)
|
| 1394 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_signal"], name="Sinyal",
|
| 1395 |
+
line=dict(color="#5b8cff", width=1)), row=2, col=1)
|
| 1396 |
+
|
| 1397 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_k"], name="%K", line=dict(color="#5b8cff")), row=3, col=1)
|
| 1398 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_d"], name="%D", line=dict(color="#f2b84b")), row=3, col=1)
|
| 1399 |
+
fig.add_hline(y=80, line=dict(color="#ff5c72", dash="dot", width=1), row=3, col=1)
|
| 1400 |
+
fig.add_hline(y=20, line=dict(color="#3ddc97", dash="dot", width=1), row=3, col=1)
|
| 1401 |
+
|
| 1402 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["adx"], name="ADX", line=dict(color="#e6ebf2", width=1.5)), row=4, col=1)
|
| 1403 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["plus_di"], name="+DI", line=dict(color="#3ddc97", width=1)), row=4, col=1)
|
| 1404 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=ind["minus_di"], name="-DI", line=dict(color="#ff5c72", width=1)), row=4, col=1)
|
| 1405 |
+
fig.add_hline(y=25, line=dict(color="#f2b84b", dash="dot", width=1), row=4, col=1)
|
| 1406 |
+
|
| 1407 |
+
return _base_layout(fig, f"{symbol} · Momentum Göstergeleri", 720)
|
| 1408 |
+
|
| 1409 |
+
|
| 1410 |
+
def build_equity_chart(df, symbol):
|
| 1411 |
+
ret = df["c"].pct_change().fillna(0)
|
| 1412 |
+
cum = (1 + ret).cumprod()
|
| 1413 |
+
peak = cum.cummax()
|
| 1414 |
+
dd = (cum - peak) / peak
|
| 1415 |
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.65, 0.35], vertical_spacing=0.06,
|
| 1416 |
+
subplot_titles=("Kümülatif Getiri (Al ve Tut)", "Drawdown"))
|
| 1417 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=cum, name="Kümülatif Getiri", line=dict(color="#5b8cff"),
|
| 1418 |
+
fill="tozeroy", fillcolor="rgba(91,140,255,0.08)"), row=1, col=1)
|
| 1419 |
+
fig.add_trace(go.Scatter(x=df["ts"], y=dd, name="Drawdown", line=dict(color="#ff5c72"),
|
| 1420 |
+
fill="tozeroy", fillcolor="rgba(255,92,114,0.12)"), row=2, col=1)
|
| 1421 |
+
fig.update_layout(showlegend=False)
|
| 1422 |
+
return _base_layout(fig, f"{symbol} · Risk Profili", 440)
|
| 1423 |
+
|
| 1424 |
+
|
| 1425 |
+
def build_depth_chart(bids, asks, symbol):
|
| 1426 |
+
bids_sorted = sorted(bids, key=lambda x: -x[0])
|
| 1427 |
+
asks_sorted = sorted(asks, key=lambda x: x[0])
|
| 1428 |
+
bid_prices = [p for p, _ in bids_sorted]
|
| 1429 |
+
bid_cum = np.cumsum([s for _, s in bids_sorted])
|
| 1430 |
+
ask_prices = [p for p, _ in asks_sorted]
|
| 1431 |
+
ask_cum = np.cumsum([s for _, s in asks_sorted])
|
| 1432 |
+
fig = go.Figure()
|
| 1433 |
+
fig.add_trace(go.Scatter(x=bid_prices, y=bid_cum, name="Alış (Bid)", line=dict(color="#3ddc97", width=2),
|
| 1434 |
+
fill="tozeroy", fillcolor="rgba(61,220,151,0.15)"))
|
| 1435 |
+
fig.add_trace(go.Scatter(x=ask_prices, y=ask_cum, name="Satış (Ask)", line=dict(color="#ff5c72", width=2),
|
| 1436 |
+
fill="tozeroy", fillcolor="rgba(255,92,114,0.15)"))
|
| 1437 |
+
fig.update_layout(xaxis_title="Fiyat", yaxis_title="Kümülatif Miktar")
|
| 1438 |
+
return _base_layout(fig, f"{symbol} · Emir Defteri Derinliği", 380)
|
| 1439 |
+
|
| 1440 |
+
|
| 1441 |
+
# ---------------------------------------------------------------------------
|
| 1442 |
+
# TAB CALLBACKS
|
| 1443 |
+
# ---------------------------------------------------------------------------
|
| 1444 |
+
def load_overview(symbol, bar):
|
| 1445 |
+
try:
|
| 1446 |
+
df = fetch_candles(symbol, bar, 300)
|
| 1447 |
+
ticker = fetch_ticker(symbol)
|
| 1448 |
+
ind = compute_indicators(df)
|
| 1449 |
+
last = float(ticker.get("last") or df["c"].iloc[-1])
|
| 1450 |
+
open24h = float(ticker.get("open24h") or df["c"].iloc[0])
|
| 1451 |
+
high24h = float(ticker.get("high24h") or df["h"].max())
|
| 1452 |
+
low24h = float(ticker.get("low24h") or df["l"].min())
|
| 1453 |
+
vol24h = float(ticker.get("volCcy24h") or df["vol"].sum())
|
| 1454 |
+
chg = (last - open24h) / open24h if open24h else 0.0
|
| 1455 |
+
tone = "up" if chg >= 0 else "down"
|
| 1456 |
+
cards = [
|
| 1457 |
+
metric_card("SON FİYAT", f"${fmt_price(last)}", "", tone),
|
| 1458 |
+
metric_card("24S DEĞİŞİM", fmt_pct(chg), "", tone),
|
| 1459 |
+
metric_card("24S YÜKSEK", f"${fmt_price(high24h)}"),
|
| 1460 |
+
metric_card("24S DÜŞÜK", f"${fmt_price(low24h)}"),
|
| 1461 |
+
metric_card("24S HACİM (USDT)", f"${fmt_large(vol24h)}"),
|
| 1462 |
+
]
|
| 1463 |
+
return cards_row(cards), build_price_chart(df, ind, symbol, bar)
|
| 1464 |
+
except Exception as e:
|
| 1465 |
+
return error_box(f"Veri alınamadı: {e}"), go.Figure()
|
| 1466 |
+
|
| 1467 |
+
|
| 1468 |
+
def _reasons_html(reasons):
|
| 1469 |
+
icon = {"up": "▲", "down": "▼", "flat": "•"}
|
| 1470 |
+
return "".join(f'<li class="r-{t}"><span class="ri">{icon[t]}</span>{txt}</li>' for t, txt in reasons)
|
| 1471 |
+
|
| 1472 |
+
|
| 1473 |
+
def load_signal(symbol, bar, closed_only=False, acct=10000, risk_pct=1.0):
|
| 1474 |
+
empty5 = lambda html: (html, go.Figure(), go.Figure(), go.Figure(), go.Figure())
|
| 1475 |
+
try:
|
| 1476 |
+
df = fetch_candles(symbol, bar, 300)
|
| 1477 |
+
if closed_only:
|
| 1478 |
+
df = df[df["confirm"] == 1].reset_index(drop=True)
|
| 1479 |
+
if len(df) < 30:
|
| 1480 |
+
return empty5(error_box("Sinyal için yeterli mum verisi yok (en az 30 bar gerekir)."))
|
| 1481 |
+
acct = float(acct or 0)
|
| 1482 |
+
risk_pct = float(risk_pct or 0)
|
| 1483 |
+
res = generate_signal(df, symbol=symbol, bar=bar, mtf=True, micro=True, closed_only=closed_only,
|
| 1484 |
+
acct=acct, risk_pct=risk_pct)
|
| 1485 |
+
ind, plan, bt = res["indicators"], res["plan"], res["backtest"]
|
| 1486 |
+
|
| 1487 |
+
# ---- Ana panel ----
|
| 1488 |
+
regime_col = {"TREND": "#5b8cff", "YATAY": "#f2b84b", "GEÇİŞ": "#7c8797"}[res["regime"]]
|
| 1489 |
+
panel = (
|
| 1490 |
+
f'<div class="signal-panel">'
|
| 1491 |
+
f'<div class="signal-badge" style="--tone:{res["color"]}">{res["signal"]}</div>'
|
| 1492 |
+
f'<div class="signal-meta"><div class="mc-label">KOMPOZİT SKOR</div>'
|
| 1493 |
+
f'<div class="mc-value">{res["score"]:+.2f} / ±10</div></div>'
|
| 1494 |
+
f'<div class="signal-meta"><div class="mc-label">GÜVEN</div>'
|
| 1495 |
+
f'<div class="mc-value">%{res["confidence"]}</div></div>'
|
| 1496 |
+
f'<div class="signal-meta"><div class="mc-label">KALİTE NOTU</div>'
|
| 1497 |
+
f'<div class="mc-value" style="color:{res["grade_color"]}">{res["grade"]}</div></div>'
|
| 1498 |
+
f'<div class="signal-meta"><div class="mc-label">REJİM</div>'
|
| 1499 |
+
f'<div class="mc-value" style="color:{regime_col}">{res["regime"]}</div></div>'
|
| 1500 |
+
f'</div>'
|
| 1501 |
+
)
|
| 1502 |
+
tot = res["bull_w"] + res["bear_w"]
|
| 1503 |
+
bull_pct = (res["bull_w"] / tot * 100) if tot > 0 else 50.0
|
| 1504 |
+
consensus = (
|
| 1505 |
+
f'<div class="consensus"><div class="mc-label">FAKTÖR KONSENSÜSÜ · BOĞA %{bull_pct:.0f} / AYI %{100 - bull_pct:.0f}</div>'
|
| 1506 |
+
f'<div class="cbar"><div class="cbull" style="width:{bull_pct:.1f}%"></div>'
|
| 1507 |
+
f'<div class="cbear" style="width:{100 - bull_pct:.1f}%"></div></div></div>'
|
| 1508 |
+
)
|
| 1509 |
+
conf_tags = "".join(
|
| 1510 |
+
f'<div class="tag" style="color:var(--text-muted)">{k} %{v * 100:.0f}</div>' for k, v in res["conf_parts"].items())
|
| 1511 |
+
tag_html = ""
|
| 1512 |
+
if res["tags"]:
|
| 1513 |
+
tag_html = '<div class="tags" style="margin-bottom:12px;">' + "".join(
|
| 1514 |
+
f'<div class="tag" style="border-color:{c};color:{c};">{t}</div>' for t, c in res["tags"]) + "</div>"
|
| 1515 |
+
conf_html = f'<div class="tags" style="margin-bottom:12px;">{conf_tags}</div>'
|
| 1516 |
+
|
| 1517 |
+
# ---- İşlem planı + pozisyon boyutu ----
|
| 1518 |
+
plan_cards, size_cards = [], []
|
| 1519 |
+
if plan:
|
| 1520 |
+
up = plan["direction"] == "LONG"
|
| 1521 |
+
plan_cards = [
|
| 1522 |
+
metric_card("ÖNERİLEN YÖN", plan["direction"], "", "up" if up else "down"),
|
| 1523 |
+
metric_card("GİRİŞ", f"${fmt_price(plan['entry'])}"),
|
| 1524 |
+
metric_card("STOP-LOSS", f"${fmt_price(plan['stop_loss'])}",
|
| 1525 |
+
f"{plan['stop_type']} · %{plan['risk_frac'] * 100:.2f} risk", "down"),
|
| 1526 |
+
]
|
| 1527 |
+
for k, (px, m) in enumerate(plan["targets"]):
|
| 1528 |
+
plan_cards.append(metric_card(f"HEDEF {k + 1}", f"${fmt_price(px)}", f"{m:.2f}R", "up"))
|
| 1529 |
+
sz = plan["sizing"]
|
| 1530 |
+
if sz:
|
| 1531 |
+
size_cards = [
|
| 1532 |
+
metric_card("RİSK TUTARI", f"${fmt_large(sz['risk_amount'])}", f"Hesabın %{risk_pct:g}'i"),
|
| 1533 |
+
metric_card("POZİSYON (USDT)", f"${fmt_large(sz['notional'])}", f"Miktar: {sz['qty']:.6g}"),
|
| 1534 |
+
metric_card("KALDIRAÇ ETKİSİ", f"{sz['leverage']:.2f}x",
|
| 1535 |
+
"Spot ile yetmez — kaldıraç gerekir" if sz["leverage"] > 1.0 else "Spot ile uygulanabilir",
|
| 1536 |
+
"down" if sz["leverage"] > 1.0 else "up"),
|
| 1537 |
+
]
|
| 1538 |
+
stats = bt["stats"] if bt else None
|
| 1539 |
+
if stats is not None:
|
| 1540 |
+
k = stats["kelly"]
|
| 1541 |
+
if stats["n"] < 20:
|
| 1542 |
+
kv, ks, kt = "—", "Örnek az (<20 işlem)", "neutral"
|
| 1543 |
+
elif k is None or k <= 0:
|
| 1544 |
+
kv, ks, kt = "Kenar yok", "Kelly ≤ 0", "down"
|
| 1545 |
+
else:
|
| 1546 |
+
half = min(k / 2, 0.03)
|
| 1547 |
+
kv, ks, kt = f"%{half * 100:.2f}", f"Yarım-Kelly (tavan %3) · bilgi amaçlı", "up"
|
| 1548 |
+
size_cards.append(metric_card("ÖNERİLEN RİSK", kv, ks, kt))
|
| 1549 |
+
|
| 1550 |
+
# ---- Backtest kartları ----
|
| 1551 |
+
bt_cards = []
|
| 1552 |
+
if stats:
|
| 1553 |
+
L, S = stats["long"], stats["short"]
|
| 1554 |
+
wr = lambda x: f"%{x['win_rate'] * 100:.0f}" if x["win_rate"] is not None else "—"
|
| 1555 |
+
bt_cards = [
|
| 1556 |
+
metric_card("İŞLEM SAYISI", f"{stats['n']}", f"Long {L['n']} ({wr(L)}) · Short {S['n']} ({wr(S)})"),
|
| 1557 |
+
metric_card("KAZANMA ORANI", f"%{stats['win_rate'] * 100:.1f}", f"Maks. üst üste kayıp: {stats['max_consec_loss']}",
|
| 1558 |
+
"up" if stats["win_rate"] >= 0.45 else "down"),
|
| 1559 |
+
metric_card("PROFIT FACTOR", f"{stats['profit_factor']:.2f}", "Brüt kazanç / brüt kayıp",
|
| 1560 |
+
"up" if stats["profit_factor"] >= 1.2 else "down"),
|
| 1561 |
+
metric_card("BEKLENTİ / İŞLEM", fmt_pct(stats["expectancy"]), f"Ort. {stats['avg_r']:+.2f}R (komisyon dahil)",
|
| 1562 |
+
"up" if stats["expectancy"] > 0 else "down"),
|
| 1563 |
+
metric_card("SQN", f"{stats['sqn']:.2f}", "Sistem kalitesi (>2 iyi, >3 çok iyi)",
|
| 1564 |
+
"up" if stats["sqn"] >= 2 else "neutral"),
|
| 1565 |
+
metric_card("STRATEJİ MAKS. DD", fmt_pct(stats["max_dd"]), "", "down"),
|
| 1566 |
+
metric_card("STRATEJİ vs AL&TUT", f"{fmt_pct(stats['total_return'])} / {fmt_pct(stats['bh_return'])}",
|
| 1567 |
+
"Aynı dönem, komisyon dahil", "up" if stats["total_return"] > stats["bh_return"] else "down"),
|
| 1568 |
+
]
|
| 1569 |
+
acc_cards = []
|
| 1570 |
+
if bt and bt["acc"]:
|
| 1571 |
+
a = bt["acc"]
|
| 1572 |
+
acc_cards.append(metric_card("YÖN DOĞRULUĞU", f"%{a['accuracy'] * 100:.1f}",
|
| 1573 |
+
f"{a['n_signals']} sinyal · {a['forward_bars']} bar ileri",
|
| 1574 |
+
"up" if a["accuracy"] >= 0.5 else "down"))
|
| 1575 |
+
if a.get("trend_regime"):
|
| 1576 |
+
t = a["trend_regime"]
|
| 1577 |
+
acc_cards.append(metric_card("DOĞRULUK · TREND", f"%{t['accuracy'] * 100:.1f}",
|
| 1578 |
+
f"{t['n_signals']} sinyal (ADX ≥ 25)", "up" if t["accuracy"] >= 0.5 else "down"))
|
| 1579 |
+
if a.get("range_regime"):
|
| 1580 |
+
t = a["range_regime"]
|
| 1581 |
+
acc_cards.append(metric_card("DOĞRULUK · YATAY", f"%{t['accuracy'] * 100:.1f}",
|
| 1582 |
+
f"{t['n_signals']} sinyal (ADX < 25)", "up" if t["accuracy"] >= 0.5 else "down"))
|
| 1583 |
+
|
| 1584 |
+
# ---- Piyasa bağlamı ----
|
| 1585 |
+
ctx = []
|
| 1586 |
+
for m in res["mtf"]:
|
| 1587 |
+
if m["aligned"] is None:
|
| 1588 |
+
ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Nötr", f"Üst zaman dilimi skoru: {m['score']}"))
|
| 1589 |
+
else:
|
| 1590 |
+
ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Uyumlu ✓" if m["aligned"] else "Ters Yönlü ⚠",
|
| 1591 |
+
f"Üst zaman dilimi skoru: {m['score']}", "up" if m["aligned"] else "down"))
|
| 1592 |
+
if res["funding"] is not None:
|
| 1593 |
+
f_ = res["funding"]
|
| 1594 |
+
ctx.append(metric_card("FUNDING (8S)", f"{f_ * 100:+.4f}%", "Pozitif = long kalabalık",
|
| 1595 |
+
"down" if f_ > 0.0002 else ("up" if f_ < -0.0002 else "neutral")))
|
| 1596 |
+
if res["imbalance"] is not None:
|
| 1597 |
+
im = res["imbalance"]
|
| 1598 |
+
ctx.append(metric_card("DEFTER DENGESİ", f"{im * 100:+.0f}%", "Derinlik ağırlıklı",
|
| 1599 |
+
"up" if im > 0.2 else ("down" if im < -0.2 else "neutral")))
|
| 1600 |
+
if res["supports"]:
|
| 1601 |
+
lv, tc = res["supports"][0]
|
| 1602 |
+
ctx.append(metric_card("YAKIN DESTEK", f"${fmt_price(lv)}", f"{tc} temas · {(res['price'] - lv) / res['atr']:.1f} ATR", "up"))
|
| 1603 |
+
if res["resists"]:
|
| 1604 |
+
lv, tc = res["resists"][0]
|
| 1605 |
+
ctx.append(metric_card("YAKIN DİRENÇ", f"${fmt_price(lv)}", f"{tc} temas · {(lv - res['price']) / res['atr']:.1f} ATR", "down"))
|
| 1606 |
+
ctx.append(metric_card("VOLATİLİTE YÜZDELİĞİ", f"%{res['atr_rank'] * 100:.0f}", "ATR/fiyat, son 100 bar",
|
| 1607 |
+
"down" if res["atr_rank"] > 0.9 else "neutral"))
|
| 1608 |
+
|
| 1609 |
+
reasons_box = (f'<div class="reasons-box"><div class="mc-label">FAKTÖR ANALİZİ</div>'
|
| 1610 |
+
f'<ul>{_reasons_html(res["reasons"])}</ul></div>')
|
| 1611 |
+
bt_note = ""
|
| 1612 |
+
if stats:
|
| 1613 |
+
bt_note = ('<div class="note">Backtest: son ~220 barda sinyaller üst üste binmeden işleme çevrilir; giriş bir sonraki barın açılışı, '
|
| 1614 |
+
f'çıkış SL {SL_ATR}×ATR / TP {TP_ATR}×ATR / {MAX_HOLD_BARS} bar; komisyon %{FEE_PER_SIDE * 100:.1f}/yön dahildir. '
|
| 1615 |
+
'Emir defteri, funding, S/R ve MTF katmanları canlıdır, geçmişe uygulanamaz.</div>')
|
| 1616 |
+
|
| 1617 |
+
html = (panel + consensus + conf_html + tag_html
|
| 1618 |
+
+ (section("İŞLEM PLANI") + cards_row(plan_cards) if plan_cards else "")
|
| 1619 |
+
+ (section("POZİSYON BOYUTU") + cards_row(size_cards) if size_cards else "")
|
| 1620 |
+
+ (section("GEÇMİŞ PERFORMANS (WALK-FORWARD)") + cards_row(bt_cards) + cards_row(acc_cards) + bt_note
|
| 1621 |
+
if (bt_cards or acc_cards) else "")
|
| 1622 |
+
+ section("PİYASA BAĞLAMI") + cards_row(ctx) + reasons_box)
|
| 1623 |
+
return (html, build_signal_price_chart(df, ind, symbol, bar, res), build_factor_chart(res["contribs"], symbol),
|
| 1624 |
+
build_backtest_chart(bt, df, symbol), build_signal_chart(df, ind, symbol))
|
| 1625 |
+
except Exception as e:
|
| 1626 |
+
return empty5(error_box(f"Veri alınamadı: {e}"))
|
| 1627 |
+
|
| 1628 |
+
|
| 1629 |
+
def load_risk(symbol, bar):
|
| 1630 |
+
try:
|
| 1631 |
+
df = fetch_candles(symbol, bar, 300)
|
| 1632 |
+
rm = risk_metrics(df, bar)
|
| 1633 |
+
if rm is None:
|
| 1634 |
+
return error_box("Risk hesaplaması için yeterli veri yok."), go.Figure()
|
| 1635 |
+
cards = [
|
| 1636 |
+
metric_card("YILLIKLANDIRILMIŞ VOLATİLİTE", fmt_pct(rm["volatility_annual"])),
|
| 1637 |
+
metric_card("YILLIKLANDIRILMIŞ GETİRİ", fmt_pct(rm["return_annual"]), "",
|
| 1638 |
+
"up" if rm["return_annual"] >= 0 else "down"),
|
| 1639 |
+
metric_card("SHARPE ORANI", f"{rm['sharpe']:.2f}", "", "up" if rm["sharpe"] > 0 else "down"),
|
| 1640 |
+
metric_card("MAKS. DRAWDOWN", fmt_pct(rm["max_drawdown"]), "", "down"),
|
| 1641 |
+
metric_card("GÜNCEL DRAWDOWN", fmt_pct(rm["current_dd"]), "",
|
| 1642 |
+
"down" if rm["current_dd"] < 0 else "neutral"),
|
| 1643 |
+
metric_card("VaR %95 (BAR)", fmt_pct(rm["var95_bar"]), "", "down"),
|
| 1644 |
+
metric_card("CVaR %95 (BAR)", fmt_pct(rm["cvar95_bar"]), "", "down"),
|
| 1645 |
+
]
|
| 1646 |
+
return cards_row(cards), build_equity_chart(df, symbol)
|
| 1647 |
+
except Exception as e:
|
| 1648 |
+
return error_box(f"Veri alınamadı: {e}"), go.Figure()
|
| 1649 |
+
|
| 1650 |
+
|
| 1651 |
+
def load_orderbook(symbol, depth):
|
| 1652 |
+
try:
|
| 1653 |
+
ob = fetch_orderbook(symbol, int(depth))
|
| 1654 |
+
bids = [(float(p), float(s)) for p, s, *_ in ob.get("bids", [])]
|
| 1655 |
+
asks = [(float(p), float(s)) for p, s, *_ in ob.get("asks", [])]
|
| 1656 |
+
if not bids or not asks:
|
| 1657 |
+
return error_box("Emir defteri verisi alınamadı."), go.Figure()
|
| 1658 |
+
best_bid, best_ask = bids[0][0], asks[0][0]
|
| 1659 |
+
spread = best_ask - best_bid
|
| 1660 |
+
spread_pct = spread / best_ask if best_ask else 0.0
|
| 1661 |
+
bid_vol = sum(s for _, s in bids)
|
| 1662 |
+
ask_vol = sum(s for _, s in asks)
|
| 1663 |
+
imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol) if (bid_vol + ask_vol) else 0.0
|
| 1664 |
+
tone = "up" if imbalance > 0.05 else ("down" if imbalance < -0.05 else "neutral")
|
| 1665 |
+
cards = [
|
| 1666 |
+
metric_card("EN İYİ ALIŞ", f"${fmt_price(best_bid)}", "", "up"),
|
| 1667 |
+
metric_card("EN İYİ SATIŞ", f"${fmt_price(best_ask)}", "", "down"),
|
| 1668 |
+
metric_card("SPREAD", f"${fmt_price(spread)}", fmt_pct(spread_pct)),
|
| 1669 |
+
metric_card("DEFTER DENGESİZLİĞİ", fmt_pct(imbalance), "Pozitif = alış baskısı", tone),
|
| 1670 |
+
]
|
| 1671 |
+
return cards_row(cards), build_depth_chart(bids, asks, symbol)
|
| 1672 |
+
except Exception as e:
|
| 1673 |
+
return error_box(f"Veri alınamadı: {e}"), go.Figure()
|
| 1674 |
+
|
| 1675 |
+
|
| 1676 |
+
@_gpu
|
| 1677 |
+
def refresh_all(symbol, bar, depth, closed_only, acct, risk_pct):
|
| 1678 |
+
ov_html, ov_fig = load_overview(symbol, bar)
|
| 1679 |
+
sig = load_signal(symbol, bar, closed_only, acct, risk_pct)
|
| 1680 |
+
risk_html, risk_fig = load_risk(symbol, bar)
|
| 1681 |
+
ob_html, ob_fig = load_orderbook(symbol, depth)
|
| 1682 |
+
return (ov_html, ov_fig, *sig, risk_html, risk_fig, ob_html, ob_fig)
|
| 1683 |
+
|
| 1684 |
+
|
| 1685 |
+
def refresh_signal(symbol, bar, closed_only, acct, risk_pct):
|
| 1686 |
+
"""Yalnızca sinyal sekmesini yeniler (risk/kapanmış-mum ayarı değişince hızlı güncelleme)."""
|
| 1687 |
+
return load_signal(symbol, bar, closed_only, acct, risk_pct)
|
| 1688 |
+
|
| 1689 |
+
|
| 1690 |
+
SCAN_MODES = ["Tümü (skora göre)", "Yalnızca AL yönlü", "Yalnızca SAT yönlü", "Yalnızca yüksek kalite (A / A+)"]
|
| 1691 |
+
|
| 1692 |
+
|
| 1693 |
+
def _scan_one(sym, bar, all_tickers, closed_only):
|
| 1694 |
+
df = fetch_candles(sym, bar, 300)
|
| 1695 |
+
if closed_only:
|
| 1696 |
+
df = df[df["confirm"] == 1].reset_index(drop=True)
|
| 1697 |
+
# MTF ve mikro-yapı katmanları taramada kapalı (API yükünü azaltmak için); tekil analizde açık.
|
| 1698 |
+
res = generate_signal(df, symbol=sym, bar=bar, mtf=False, micro=False, closed_only=closed_only)
|
| 1699 |
+
rm = risk_metrics(df, bar)
|
| 1700 |
+
t = all_tickers.get(sym, {})
|
| 1701 |
+
last = float(t.get("last") or df["c"].iloc[-1])
|
| 1702 |
+
open24h = float(t.get("open24h") or df["c"].iloc[0])
|
| 1703 |
+
chg24h = (last - open24h) / open24h if open24h else 0.0
|
| 1704 |
+
div = res["divergence"]
|
| 1705 |
+
div_txt = "Yükseliş" if div["bullish"] else ("Düşüş" if div["bearish"] else "—")
|
| 1706 |
+
bt = res["backtest"]
|
| 1707 |
+
st = bt["stats"] if bt else None
|
| 1708 |
+
acc = bt["acc"] if bt else None
|
| 1709 |
+
return {
|
| 1710 |
+
"Sembol": sym, "Fiyat": last, "24S %": round(chg24h * 100, 2), "Sinyal": res["signal"],
|
| 1711 |
+
"Skor": res["score"], "Kalite": res["grade"], "Güven %": res["confidence"], "Rejim": res["regime"],
|
| 1712 |
+
"RSI": round(res["indicators"]["rsi"].iloc[-1], 1), "ADX": round(res["adx"], 1),
|
| 1713 |
+
"Sıkışma": "●" if res["squeeze_on"] else "—", "Uyumsuzluk": div_txt,
|
| 1714 |
+
"Kazanma %": round(st["win_rate"] * 100, 1) if st else None,
|
| 1715 |
+
"Profit Factor": round(st["profit_factor"], 2) if st else None,
|
| 1716 |
+
"Beklenti %": round(st["expectancy"] * 100, 2) if st else None,
|
| 1717 |
+
"Yön Doğruluğu %": round(acc["accuracy"] * 100, 1) if acc else None,
|
| 1718 |
+
"Volatilite (Yıllık) %": round(rm["volatility_annual"] * 100, 1) if rm else None,
|
| 1719 |
+
"Sharpe": round(rm["sharpe"], 2) if rm else None,
|
| 1720 |
+
}
|
| 1721 |
+
|
| 1722 |
+
|
| 1723 |
+
def run_scanner(symbols_text, bar, closed_only=False, mode=SCAN_MODES[0], progress=gr.Progress()):
|
| 1724 |
+
symbols = [s.strip().upper() for s in symbols_text.split(",") if s.strip()][:25]
|
| 1725 |
+
if not symbols:
|
| 1726 |
+
return pd.DataFrame()
|
| 1727 |
+
try:
|
| 1728 |
+
all_tickers = {t["instId"]: t for t in fetch_all_tickers("SPOT")}
|
| 1729 |
+
except Exception:
|
| 1730 |
+
all_tickers = {}
|
| 1731 |
+
|
| 1732 |
+
rows, done = [], 0
|
| 1733 |
+
with ThreadPoolExecutor(max_workers=4) as ex: # ağ beklemesini paralelle (OKX limiti için 4 ile sınırlı)
|
| 1734 |
+
futs = {ex.submit(_scan_one, s, bar, all_tickers, closed_only): s for s in symbols}
|
| 1735 |
+
for f in as_completed(futs):
|
| 1736 |
+
done += 1
|
| 1737 |
+
progress(done / len(symbols), desc=f"{futs[f]} tarandı ({done}/{len(symbols)})")
|
| 1738 |
+
try:
|
| 1739 |
+
rows.append(f.result())
|
| 1740 |
+
except Exception:
|
| 1741 |
+
continue
|
| 1742 |
+
if not rows:
|
| 1743 |
+
return pd.DataFrame()
|
| 1744 |
+
out = pd.DataFrame(rows)
|
| 1745 |
+
if mode == SCAN_MODES[1]:
|
| 1746 |
+
out = out[out["Skor"] >= SIGNAL_THRESHOLD].sort_values("Skor", ascending=False)
|
| 1747 |
+
elif mode == SCAN_MODES[2]:
|
| 1748 |
+
out = out[out["Skor"] <= -SIGNAL_THRESHOLD].sort_values("Skor", ascending=True)
|
| 1749 |
+
elif mode == SCAN_MODES[3]:
|
| 1750 |
+
out = out[out["Kalite"].isin(["A", "A+"])].sort_values("Güven %", ascending=False)
|
| 1751 |
+
else:
|
| 1752 |
+
out = out.sort_values("Skor", ascending=False)
|
| 1753 |
+
return out.reset_index(drop=True)
|
| 1754 |
+
|
| 1755 |
+
|
| 1756 |
+
# ---------------------------------------------------------------------------
|
| 1757 |
+
# STYLE — dark "trading terminal" identity (deliberate, not default SaaS chrome)
|
| 1758 |
+
# ---------------------------------------------------------------------------
|
| 1759 |
+
HEAD = """
|
| 1760 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 1761 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 1762 |
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
| 1763 |
+
"""
|
| 1764 |
+
|
| 1765 |
+
CSS = """
|
| 1766 |
+
:root {
|
| 1767 |
+
--bg-void: #0a0e14;
|
| 1768 |
+
--bg-panel: #12171f;
|
| 1769 |
+
--line: #232a35;
|
| 1770 |
+
--text-primary: #e6ebf2;
|
| 1771 |
+
--text-muted: #7c8797;
|
| 1772 |
+
--accent-long: #3ddc97;
|
| 1773 |
+
--accent-short: #ff5c72;
|
| 1774 |
+
--accent-signal: #f2b84b;
|
| 1775 |
+
--accent-primary: #5b8cff;
|
| 1776 |
+
}
|
| 1777 |
+
.gradio-container { background: var(--bg-void) !important; color: var(--text-primary) !important;
|
| 1778 |
+
font-family: 'IBM Plex Sans', system-ui, sans-serif !important; }
|
| 1779 |
+
h1, h2, h3 { font-family: 'Space Grotesk', sans-serif !important; letter-spacing: -0.01em; }
|
| 1780 |
+
|
| 1781 |
+
#masthead { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;
|
| 1782 |
+
border-bottom: 1px solid var(--line); padding: 14px 4px 18px 4px; margin-bottom: 6px; }
|
| 1783 |
+
#masthead .wordmark { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.5rem; letter-spacing:-0.02em; }
|
| 1784 |
+
#masthead .wordmark span { color: var(--accent-primary); }
|
| 1785 |
+
#masthead .tags { display:flex; gap:8px; flex-wrap:wrap; }
|
| 1786 |
+
#masthead .tag { font-size:0.7rem; padding:3px 9px; border:1px solid var(--line); border-radius:3px;
|
| 1787 |
+
color:var(--text-muted); font-family:'IBM Plex Mono',monospace; }
|
| 1788 |
+
|
| 1789 |
+
.metric-row { display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 16px 0; }
|
| 1790 |
+
.metric-card { background: var(--bg-panel); border:1px solid var(--line); border-radius:4px;
|
| 1791 |
+
padding:12px 16px; min-width:150px; flex:1; }
|
| 1792 |
+
.mc-label { font-size:0.66rem; color:var(--text-muted); letter-spacing:0.04em; margin-bottom:6px; }
|
| 1793 |
+
.mc-value { font-size:1.24rem; font-weight:600; color:var(--text-primary);
|
| 1794 |
+
font-family:'IBM Plex Mono',monospace; }
|
| 1795 |
+
.mc-sub { font-size:0.72rem; color:var(--text-muted); margin-top:4px; font-family:'IBM Plex Mono',monospace; }
|
| 1796 |
+
.metric-card.mc-up .mc-value { color: var(--accent-long); }
|
| 1797 |
+
.metric-card.mc-down .mc-value { color: var(--accent-short); }
|
| 1798 |
+
|
| 1799 |
+
.section-title { font-family:'Space Grotesk',sans-serif; font-size:0.78rem; font-weight:600; letter-spacing:0.08em;
|
| 1800 |
+
color:var(--text-muted); margin:18px 0 0 0; padding-top:12px; border-top:1px solid var(--line); }
|
| 1801 |
+
|
| 1802 |
+
.signal-panel { display:flex; align-items:center; flex-wrap:wrap; gap:20px; background:var(--bg-panel);
|
| 1803 |
+
border:1px solid var(--line); border-radius:4px; padding:18px 20px; margin-bottom:12px; }
|
| 1804 |
+
.signal-badge { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.6rem; color:var(--tone);
|
| 1805 |
+
border:2px solid var(--tone); border-radius:4px; padding:10px 22px; }
|
| 1806 |
+
.signal-meta .mc-value { font-size:1.35rem; }
|
| 1807 |
+
|
| 1808 |
+
.consensus { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:12px 16px; margin-bottom:12px; }
|
| 1809 |
+
.cbar { display:flex; height:8px; border-radius:2px; overflow:hidden; background:var(--line); }
|
| 1810 |
+
.cbull { background: var(--accent-long); }
|
| 1811 |
+
.cbear { background: var(--accent-short); }
|
| 1812 |
+
|
| 1813 |
+
.tags { display:flex; gap:8px; flex-wrap:wrap; }
|
| 1814 |
+
.tags .tag { font-size:0.72rem; font-weight:600; padding:5px 12px; border:1px solid var(--line);
|
| 1815 |
+
border-radius:3px; font-family:'IBM Plex Mono',monospace; letter-spacing:0.03em; }
|
| 1816 |
+
|
| 1817 |
+
.reasons-box { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:16px 20px; margin-top:8px; }
|
| 1818 |
+
.reasons-box ul { margin:8px 0 0 0; padding-left:0; list-style:none; }
|
| 1819 |
+
.reasons-box li { margin-bottom:6px; color:var(--text-primary); font-size:0.9rem; }
|
| 1820 |
+
.reasons-box .ri { display:inline-block; width:18px; font-size:0.7rem; }
|
| 1821 |
+
.reasons-box .r-up .ri { color: var(--accent-long); }
|
| 1822 |
+
.reasons-box .r-down .ri { color: var(--accent-short); }
|
| 1823 |
+
.reasons-box .r-flat .ri { color: var(--text-muted); }
|
| 1824 |
+
|
| 1825 |
+
.note { color:var(--text-muted); font-size:0.74rem; line-height:1.5; margin:-6px 0 8px 0; }
|
| 1826 |
+
|
| 1827 |
+
.error-box { background: rgba(255,92,114,0.08); border:1px solid var(--accent-short); color:var(--accent-short);
|
| 1828 |
+
padding:14px 18px; border-radius:4px; font-family:'IBM Plex Mono',monospace; font-size:0.85rem; }
|
| 1829 |
+
|
| 1830 |
+
.disclaimer { border-top:1px solid var(--line); margin-top:22px; padding-top:14px; color:var(--text-muted);
|
| 1831 |
+
font-size:0.76rem; line-height:1.55; }
|
| 1832 |
+
"""
|
| 1833 |
+
|
| 1834 |
+
# ---------------------------------------------------------------------------
|
| 1835 |
+
# GRADIO UI
|
| 1836 |
+
# ---------------------------------------------------------------------------
|
| 1837 |
+
with gr.Blocks(title="OKX Quant Terminal") as demo:
|
| 1838 |
+
|
| 1839 |
+
gr.HTML(
|
| 1840 |
+
'<div id="masthead">'
|
| 1841 |
+
'<div class="wordmark">OKX <span>QUANT</span> TERMINAL</div>'
|
| 1842 |
+
'<div class="tags"><div class="tag">ELITE SIGNAL ENGINE v2</div>'
|
| 1843 |
+
'<div class="tag">PUBLIC MARKET DATA</div>'
|
| 1844 |
+
'<div class="tag">READ-ONLY · EMİR GÖNDERİLMEZ</div></div>'
|
| 1845 |
+
'</div>'
|
| 1846 |
+
)
|
| 1847 |
+
|
| 1848 |
+
with gr.Row():
|
| 1849 |
+
symbol_dd = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True,
|
| 1850 |
+
label="Sembol", scale=2)
|
| 1851 |
+
bar_dd = gr.Dropdown(choices=BAR_OPTIONS, value="1H", label="Zaman Dilimi", scale=1)
|
| 1852 |
+
depth_slider = gr.Slider(5, 50, value=20, step=5, label="Defter Derinliği", scale=1)
|
| 1853 |
+
refresh_btn = gr.Button("🔄 Yenile", variant="primary", scale=1)
|
| 1854 |
+
auto_cb = gr.Checkbox(label="Otomatik (15sn)", value=False, scale=1)
|
| 1855 |
+
|
| 1856 |
+
with gr.Row():
|
| 1857 |
+
closed_cb = gr.Checkbox(label="Sadece kapanmış mumlarla sinyal (repaint yok)", value=False, scale=2)
|
| 1858 |
+
acct_num = gr.Number(value=10000, label="Hesap Büyüklüğü (USDT)", minimum=0, scale=1)
|
| 1859 |
+
risk_sl = gr.Slider(0.25, 5, value=1, step=0.25, label="İşlem Başına Risk (%)", scale=1)
|
| 1860 |
+
|
| 1861 |
+
timer = gr.Timer(15, active=False)
|
| 1862 |
+
|
| 1863 |
+
with gr.Tabs():
|
| 1864 |
+
with gr.Tab("📊 Genel Bakış"):
|
| 1865 |
+
ov_html = gr.HTML()
|
| 1866 |
+
ov_plot = gr.Plot()
|
| 1867 |
+
with gr.Tab("🎯 Sinyal Motoru"):
|
| 1868 |
+
sig_html = gr.HTML()
|
| 1869 |
+
sig_map_plot = gr.Plot()
|
| 1870 |
+
sig_factor_plot = gr.Plot()
|
| 1871 |
+
sig_bt_plot = gr.Plot()
|
| 1872 |
+
sig_ind_plot = gr.Plot()
|
| 1873 |
+
with gr.Tab("⚠️ Risk Analitiği"):
|
| 1874 |
+
risk_html = gr.HTML()
|
| 1875 |
+
risk_plot = gr.Plot()
|
| 1876 |
+
with gr.Tab("📖 Emir Defteri"):
|
| 1877 |
+
ob_html = gr.HTML()
|
| 1878 |
+
ob_plot = gr.Plot()
|
| 1879 |
+
with gr.Tab("🔍 Çoklu Varlık Tarayıcı"):
|
| 1880 |
+
gr.Markdown("Popüler pariteleri kompozit sinyal skoruna göre sıralar. Kendi listenizi virgülle ayırıp girebilirsiniz (en fazla 25 sembol).")
|
| 1881 |
+
with gr.Row():
|
| 1882 |
+
scan_symbols = gr.Textbox(value=",".join(POPULAR_SYMBOLS), label="Semboller", scale=3)
|
| 1883 |
+
scan_bar = gr.Dropdown(choices=BAR_OPTIONS, value="4H", label="Zaman Dilimi", scale=1)
|
| 1884 |
+
scan_mode = gr.Dropdown(choices=SCAN_MODES, value=SCAN_MODES[0], label="Filtre", scale=2)
|
| 1885 |
+
scan_btn = gr.Button("🔍 Taramayı Başlat", variant="primary")
|
| 1886 |
+
scan_out = gr.Dataframe(interactive=False, wrap=True)
|
| 1887 |
+
|
| 1888 |
+
gr.HTML(
|
| 1889 |
+
'<div class="disclaimer">Bu panel yalnızca eğitim ve bilgilendirme amaçlıdır; yatırım tavsiyesi '
|
| 1890 |
+
'değildir. Tüm veriler OKX genel (public) piyasa API\'sinden alınır; hiçbir API anahtarı '
|
| 1891 |
+
'kullanılmaz ve hiçbir gerçek emir gönderilmez. Kripto varlıklar yüksek oynaklık içerir; geçmiş '
|
| 1892 |
+
'performans (backtest dahil) gelecekteki sonuçların göstergesi değildir. Backtest sonuçları '
|
| 1893 |
+
'kısa örneklem üzerindedir ve istatistiksel olarak sınırlıdır.</div>'
|
| 1894 |
+
)
|
| 1895 |
+
|
| 1896 |
+
io_inputs = [symbol_dd, bar_dd, depth_slider, closed_cb, acct_num, risk_sl]
|
| 1897 |
+
io_outputs = [ov_html, ov_plot, sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot,
|
| 1898 |
+
risk_html, risk_plot, ob_html, ob_plot]
|
| 1899 |
+
sig_inputs = [symbol_dd, bar_dd, closed_cb, acct_num, risk_sl]
|
| 1900 |
+
sig_outputs = [sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot]
|
| 1901 |
+
|
| 1902 |
+
refresh_btn.click(refresh_all, io_inputs, io_outputs)
|
| 1903 |
+
timer.tick(refresh_all, io_inputs, io_outputs)
|
| 1904 |
+
auto_cb.change(lambda active: gr.Timer(active=active), auto_cb, timer)
|
| 1905 |
+
closed_cb.change(refresh_signal, sig_inputs, sig_outputs)
|
| 1906 |
+
acct_num.submit(refresh_signal, sig_inputs, sig_outputs)
|
| 1907 |
+
risk_sl.release(refresh_signal, sig_inputs, sig_outputs)
|
| 1908 |
+
scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out)
|
| 1909 |
+
demo.load(refresh_all, io_inputs, io_outputs)
|
| 1910 |
+
|
| 1911 |
+
demo.queue().launch(
|
| 1912 |
+
theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"),
|
| 1913 |
+
css=CSS,
|
| 1914 |
+
head=HEAD,
|
| 1915 |
+
ssr_mode=False,
|
| 1916 |
+
)
|