brain-login-and-sim 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brain_login_and_sim-0.1.0/LICENSE +21 -0
- brain_login_and_sim-0.1.0/PKG-INFO +101 -0
- brain_login_and_sim-0.1.0/README.md +75 -0
- brain_login_and_sim-0.1.0/pyproject.toml +35 -0
- brain_login_and_sim-0.1.0/setup.cfg +4 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim/__init__.py +33 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim/auth.py +257 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim/simulation.py +350 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim.egg-info/PKG-INFO +101 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim.egg-info/SOURCES.txt +12 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim.egg-info/dependency_links.txt +1 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim.egg-info/requires.txt +7 -0
- brain_login_and_sim-0.1.0/src/brain_login_and_sim.egg-info/top_level.txt +1 -0
- brain_login_and_sim-0.1.0/tests/test_full_loop.py +205 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanakrit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brain-login-and-sim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WorldQuant BRAIN client — login (biometric Scan + auto re-login), simulation, batch run, and auto-keeping promising alphas
|
|
5
|
+
Author-email: Tanakrit <9tanakrit.work@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Tanakrit/brain-login-and-sim
|
|
8
|
+
Project-URL: Issues, https://github.com/Tanakrit/brain-login-and-sim/issues
|
|
9
|
+
Keywords: worldquant,brain,alpha,quant,simulation,trading
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: requests>=2.28
|
|
20
|
+
Requires-Dist: python-dotenv>=0.19
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build; extra == "dev"
|
|
23
|
+
Requires-Dist: twine; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# brain-login-and-sim
|
|
28
|
+
|
|
29
|
+
ไคลเอนต์ Python สำหรับ **WorldQuant BRAIN** — login (จัดการ biometric "Scan" + auto re-login),
|
|
30
|
+
ส่ง simulation, รัน batch (สูงสุด 3 พร้อมกันตามลิมิต), และคัดเก็บ alpha ที่ผ่านเกณฑ์อัตโนมัติ
|
|
31
|
+
|
|
32
|
+
## ติดตั้ง
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install brain-login-and-sim
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## ตั้งค่า credentials
|
|
39
|
+
|
|
40
|
+
สร้างไฟล์ `.env` (ดู `.env.example`):
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
WQ_EMAIL=your-email@example.com
|
|
44
|
+
WQ_PASSWORD=your-password
|
|
45
|
+
# ส่งลิงก์ Scan เข้า Telegram (ทางเลือก แต่แนะนำ)
|
|
46
|
+
TELEGRAM_BOT_TOKEN=123456789:AAH...
|
|
47
|
+
TELEGRAM_CHAT_ID=123456789
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## ใช้งาน
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from brain_login_and_sim import BrainClient, Simulator
|
|
54
|
+
|
|
55
|
+
# login — ถ้าระบบขอ biometric Scan จะส่งลิงก์เข้า Telegram แล้วรอจนสแกนเสร็จ
|
|
56
|
+
client = BrainClient(notify_telegram=True).authenticate()
|
|
57
|
+
print(client.whoami()["id"])
|
|
58
|
+
|
|
59
|
+
sim = Simulator(client)
|
|
60
|
+
|
|
61
|
+
# ยิง 1 ตัว
|
|
62
|
+
r = sim.simulate("ts_rank(close, 5)")
|
|
63
|
+
print(r["simulation"]["status"], r["alpha"]["is"]["sharpe"])
|
|
64
|
+
|
|
65
|
+
# รันหลายตัวจากไฟล์ JSONL คัดเก็บตัวที่ผ่านเกณฑ์อัตโนมัติ
|
|
66
|
+
sim.simulate_batch("alphas.jsonl", save_promising=True)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## ฟีเจอร์หลัก
|
|
70
|
+
|
|
71
|
+
- **Login + biometric Scan** — ส่งลิงก์สแกนเข้า Telegram/อีเมล แล้ว poll รอจนเสร็จ (ไม่ต้องเฝ้าจอ)
|
|
72
|
+
- **Auto re-login** — เมื่อ session หมดอายุ (WorldQuant ให้ ~4 ชม.) เจอ 401 จะ login ใหม่แล้วลองซ้ำให้เอง
|
|
73
|
+
- **คัดเก็บ alpha ดี** — เกณฑ์ `|Sharpe| ≥ 1` และ `|Fitness| ≥ 0.9` (ค่าสัมบูรณ์ จับ flip candidate ที่ negate แล้วดี)
|
|
74
|
+
- **Batch** — รันสูงสุด 3 ตัวพร้อมกัน (ลิมิต WorldQuant), error รายตัวไม่ล้มทั้ง batch
|
|
75
|
+
- **Resume** — จด checkpoint ด้วย hash รันไฟล์เดิมซ้ำจะข้ามตัวที่รันแล้ว รันต่อจากที่ค้าง
|
|
76
|
+
- **Input ยืดหยุ่น** — list ของสูตร, ไฟล์ `.txt` (1 สูตร/บรรทัด), หรือ `.jsonl` ที่มี field `expression` (+`hash`)
|
|
77
|
+
|
|
78
|
+
## รูปแบบ JSONL
|
|
79
|
+
|
|
80
|
+
```jsonl
|
|
81
|
+
{"hash": "f7b3...", "expression": "rank(reverse(ts_delta(anl4_cfo_median, 10)))", "status": "generated"}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
ระบบดึง `expression` ไปรัน และถ้าผ่านเกณฑ์จะเก็บ `source_hash` ไว้อ้างกลับไฟล์ต้นฉบับใน `promising_alphas.jsonl`
|
|
85
|
+
|
|
86
|
+
## API ย่อ
|
|
87
|
+
|
|
88
|
+
`BrainClient`: `authenticate(on_biometric=None)`, `whoami()`, `get(path)`, `post(path)`, `auto_relogin`
|
|
89
|
+
|
|
90
|
+
`Simulator(client)`: `simulate(...)`, `simulate_batch(...)`, `is_promising(...)`, `save_if_promising(...)`,
|
|
91
|
+
`load_promising(path)`, `get_operators()`, `get_data_fields(...)`, `get_alpha(id)`
|
|
92
|
+
|
|
93
|
+
## ทดสอบ
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python tests/test_full_loop.py # mock API ไม่ต้องต่อเน็ต/รหัสผ่าน
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# brain-login-and-sim
|
|
2
|
+
|
|
3
|
+
ไคลเอนต์ Python สำหรับ **WorldQuant BRAIN** — login (จัดการ biometric "Scan" + auto re-login),
|
|
4
|
+
ส่ง simulation, รัน batch (สูงสุด 3 พร้อมกันตามลิมิต), และคัดเก็บ alpha ที่ผ่านเกณฑ์อัตโนมัติ
|
|
5
|
+
|
|
6
|
+
## ติดตั้ง
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install brain-login-and-sim
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## ตั้งค่า credentials
|
|
13
|
+
|
|
14
|
+
สร้างไฟล์ `.env` (ดู `.env.example`):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
WQ_EMAIL=your-email@example.com
|
|
18
|
+
WQ_PASSWORD=your-password
|
|
19
|
+
# ส่งลิงก์ Scan เข้า Telegram (ทางเลือก แต่แนะนำ)
|
|
20
|
+
TELEGRAM_BOT_TOKEN=123456789:AAH...
|
|
21
|
+
TELEGRAM_CHAT_ID=123456789
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## ใช้งาน
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from brain_login_and_sim import BrainClient, Simulator
|
|
28
|
+
|
|
29
|
+
# login — ถ้าระบบขอ biometric Scan จะส่งลิงก์เข้า Telegram แล้วรอจนสแกนเสร็จ
|
|
30
|
+
client = BrainClient(notify_telegram=True).authenticate()
|
|
31
|
+
print(client.whoami()["id"])
|
|
32
|
+
|
|
33
|
+
sim = Simulator(client)
|
|
34
|
+
|
|
35
|
+
# ยิง 1 ตัว
|
|
36
|
+
r = sim.simulate("ts_rank(close, 5)")
|
|
37
|
+
print(r["simulation"]["status"], r["alpha"]["is"]["sharpe"])
|
|
38
|
+
|
|
39
|
+
# รันหลายตัวจากไฟล์ JSONL คัดเก็บตัวที่ผ่านเกณฑ์อัตโนมัติ
|
|
40
|
+
sim.simulate_batch("alphas.jsonl", save_promising=True)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## ฟีเจอร์หลัก
|
|
44
|
+
|
|
45
|
+
- **Login + biometric Scan** — ส่งลิงก์สแกนเข้า Telegram/อีเมล แล้ว poll รอจนเสร็จ (ไม่ต้องเฝ้าจอ)
|
|
46
|
+
- **Auto re-login** — เมื่อ session หมดอายุ (WorldQuant ให้ ~4 ชม.) เจอ 401 จะ login ใหม่แล้วลองซ้ำให้เอง
|
|
47
|
+
- **คัดเก็บ alpha ดี** — เกณฑ์ `|Sharpe| ≥ 1` และ `|Fitness| ≥ 0.9` (ค่าสัมบูรณ์ จับ flip candidate ที่ negate แล้วดี)
|
|
48
|
+
- **Batch** — รันสูงสุด 3 ตัวพร้อมกัน (ลิมิต WorldQuant), error รายตัวไม่ล้มทั้ง batch
|
|
49
|
+
- **Resume** — จด checkpoint ด้วย hash รันไฟล์เดิมซ้ำจะข้ามตัวที่รันแล้ว รันต่อจากที่ค้าง
|
|
50
|
+
- **Input ยืดหยุ่น** — list ของสูตร, ไฟล์ `.txt` (1 สูตร/บรรทัด), หรือ `.jsonl` ที่มี field `expression` (+`hash`)
|
|
51
|
+
|
|
52
|
+
## รูปแบบ JSONL
|
|
53
|
+
|
|
54
|
+
```jsonl
|
|
55
|
+
{"hash": "f7b3...", "expression": "rank(reverse(ts_delta(anl4_cfo_median, 10)))", "status": "generated"}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
ระบบดึง `expression` ไปรัน และถ้าผ่านเกณฑ์จะเก็บ `source_hash` ไว้อ้างกลับไฟล์ต้นฉบับใน `promising_alphas.jsonl`
|
|
59
|
+
|
|
60
|
+
## API ย่อ
|
|
61
|
+
|
|
62
|
+
`BrainClient`: `authenticate(on_biometric=None)`, `whoami()`, `get(path)`, `post(path)`, `auto_relogin`
|
|
63
|
+
|
|
64
|
+
`Simulator(client)`: `simulate(...)`, `simulate_batch(...)`, `is_promising(...)`, `save_if_promising(...)`,
|
|
65
|
+
`load_promising(path)`, `get_operators()`, `get_data_fields(...)`, `get_alpha(id)`
|
|
66
|
+
|
|
67
|
+
## ทดสอบ
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python tests/test_full_loop.py # mock API ไม่ต้องต่อเน็ต/รหัสผ่าน
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "brain-login-and-sim"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "WorldQuant BRAIN client — login (biometric Scan + auto re-login), simulation, batch run, and auto-keeping promising alphas"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Tanakrit", email = "9tanakrit.work@gmail.com" }]
|
|
13
|
+
keywords = ["worldquant", "brain", "alpha", "quant", "simulation", "trading"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.28",
|
|
16
|
+
"python-dotenv>=0.19",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
24
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["build", "twine", "pytest"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Tanakrit/brain-login-and-sim"
|
|
32
|
+
Issues = "https://github.com/Tanakrit/brain-login-and-sim/issues"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brain_login_and_sim
|
|
3
|
+
====================
|
|
4
|
+
ไคลเอนต์ WorldQuant BRAIN — login (จัดการ biometric Scan + auto re-login),
|
|
5
|
+
ส่ง simulation, รัน batch (สูงสุด 3 พร้อมกัน), คัดเก็บ alpha ที่ผ่านเกณฑ์อัตโนมัติ
|
|
6
|
+
และ resume ได้
|
|
7
|
+
|
|
8
|
+
ใช้งานสั้น ๆ:
|
|
9
|
+
from brain_login_and_sim import BrainClient, Simulator
|
|
10
|
+
|
|
11
|
+
client = BrainClient(notify_telegram=True).authenticate()
|
|
12
|
+
sim = Simulator(client)
|
|
13
|
+
sim.simulate_batch("alphas.jsonl", save_promising=True)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .auth import (
|
|
17
|
+
BrainClient,
|
|
18
|
+
BrainAuthError,
|
|
19
|
+
send_biometric_email,
|
|
20
|
+
send_biometric_telegram,
|
|
21
|
+
)
|
|
22
|
+
from .simulation import Simulator
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BrainClient",
|
|
28
|
+
"BrainAuthError",
|
|
29
|
+
"Simulator",
|
|
30
|
+
"send_biometric_email",
|
|
31
|
+
"send_biometric_telegram",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
auth.py — ระบบ login เข้า WorldQuant BRAIN
|
|
3
|
+
|
|
4
|
+
- login ด้วย HTTP Basic Auth ที่ POST /authentication
|
|
5
|
+
- จัดการขั้นตอน biometric "Scan" (Persona): ส่งลิงก์เข้า Telegram/อีเมล แล้ว poll รอ
|
|
6
|
+
- auto re-login เมื่อ session หมดอายุ (WorldQuant ให้ ~4 ชม.): เจอ 401 -> login ใหม่ -> retry
|
|
7
|
+
- อ่าน credential จาก .env (WQ_EMAIL/WQ_PASSWORD), จาก argument, หรือไฟล์ JSON
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import smtplib
|
|
14
|
+
import ssl
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from email.message import EmailMessage
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from urllib.parse import urljoin
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
from requests.auth import HTTPBasicAuth
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from dotenv import load_dotenv
|
|
26
|
+
# อ่าน .env ที่อยู่ข้างไฟล์นี้ก่อน แล้วค่อยลอง cwd (ไม่ทับค่าที่โหลดแล้ว)
|
|
27
|
+
load_dotenv(Path(__file__).with_name(".env"))
|
|
28
|
+
load_dotenv()
|
|
29
|
+
except ImportError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
API_BASE = "https://api.worldquantbrain.com"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BrainAuthError(Exception):
|
|
36
|
+
"""ยกขึ้นเมื่อ authenticate ไม่สำเร็จ"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def send_biometric_email(scan_url, to_addr=None, smtp_host=None, smtp_port=None,
|
|
40
|
+
smtp_user=None, smtp_password=None):
|
|
41
|
+
"""ส่งลิงก์ Scan ไปอีเมล (อ่านค่า SMTP_* / WQ_NOTIFY_EMAIL จาก env ได้)"""
|
|
42
|
+
host = smtp_host or os.environ.get("SMTP_HOST", "smtp.gmail.com")
|
|
43
|
+
port = int(smtp_port or os.environ.get("SMTP_PORT", 587))
|
|
44
|
+
user = smtp_user or os.environ.get("SMTP_USER")
|
|
45
|
+
pw = smtp_password or os.environ.get("SMTP_PASSWORD")
|
|
46
|
+
to_addr = to_addr or os.environ.get("WQ_NOTIFY_EMAIL") or user
|
|
47
|
+
if not (user and pw and to_addr):
|
|
48
|
+
raise BrainAuthError(
|
|
49
|
+
"ส่งอีเมลไม่ได้ — ต้องตั้ง SMTP_USER, SMTP_PASSWORD (App Password), WQ_NOTIFY_EMAIL")
|
|
50
|
+
msg = EmailMessage()
|
|
51
|
+
msg["Subject"] = "WorldQuant BRAIN — ลิงก์ยืนยันตัวตน (Scan)"
|
|
52
|
+
msg["From"] = user
|
|
53
|
+
msg["To"] = to_addr
|
|
54
|
+
msg.set_content(
|
|
55
|
+
"เปิดลิงก์นี้บนมือถือแล้วสแกนให้เสร็จ:\n\n"
|
|
56
|
+
f"{scan_url}\n\nเมื่อสแกนเสร็จ โปรแกรมจะตรวจพบและทำงานต่อให้อัตโนมัติ")
|
|
57
|
+
ctx = ssl.create_default_context()
|
|
58
|
+
if port == 465:
|
|
59
|
+
with smtplib.SMTP_SSL(host, port, context=ctx) as s:
|
|
60
|
+
s.login(user, pw)
|
|
61
|
+
s.send_message(msg)
|
|
62
|
+
else:
|
|
63
|
+
with smtplib.SMTP(host, port) as s:
|
|
64
|
+
s.starttls(context=ctx)
|
|
65
|
+
s.login(user, pw)
|
|
66
|
+
s.send_message(msg)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def send_biometric_telegram(scan_url, bot_token=None, chat_id=None):
|
|
70
|
+
"""ส่งลิงก์ Scan ผ่าน Telegram bot (อ่าน TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID จาก env)"""
|
|
71
|
+
token = bot_token or os.environ.get("TELEGRAM_BOT_TOKEN")
|
|
72
|
+
chat = chat_id or os.environ.get("TELEGRAM_CHAT_ID")
|
|
73
|
+
if not (token and chat):
|
|
74
|
+
raise BrainAuthError(
|
|
75
|
+
"ส่ง Telegram ไม่ได้ — ต้องตั้ง TELEGRAM_BOT_TOKEN และ TELEGRAM_CHAT_ID")
|
|
76
|
+
text = ("WorldQuant BRAIN ขอยืนยันตัวตน (ขั้นตอน Scan)\n\n"
|
|
77
|
+
"เปิดลิงก์นี้บนมือถือแล้วสแกนให้เสร็จ:\n"
|
|
78
|
+
f"{scan_url}\n\nเมื่อสแกนเสร็จ โปรแกรมจะตรวจพบและทำงานต่อให้อัตโนมัติ")
|
|
79
|
+
r = requests.post(
|
|
80
|
+
f"https://api.telegram.org/bot{token}/sendMessage",
|
|
81
|
+
json={"chat_id": chat, "text": text, "disable_web_page_preview": False},
|
|
82
|
+
timeout=15)
|
|
83
|
+
if not r.ok:
|
|
84
|
+
raise BrainAuthError(f"ส่ง Telegram ไม่สำเร็จ: HTTP {r.status_code} — {r.text[:200]}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BrainClient:
|
|
88
|
+
def __init__(self, email=None, password=None, credentials_file=None,
|
|
89
|
+
api_base=API_BASE, notify_email=False, notify_telegram=False,
|
|
90
|
+
poll_interval=5.0, biometric_timeout=600.0):
|
|
91
|
+
"""
|
|
92
|
+
หา credential ตามลำดับ:
|
|
93
|
+
1. argument email/password
|
|
94
|
+
2. env WQ_EMAIL / WQ_PASSWORD (มาจาก .env ผ่าน python-dotenv)
|
|
95
|
+
3. ไฟล์ JSON ที่ระบุใน credentials_file หรือ env WQ_CREDENTIALS_FILE
|
|
96
|
+
"""
|
|
97
|
+
self.api_base = api_base.rstrip("/")
|
|
98
|
+
self.session = requests.Session()
|
|
99
|
+
self.session.headers.update({"User-Agent": "brain-login-and-sim/0.1"})
|
|
100
|
+
self._email, self._password = self._resolve_credentials(
|
|
101
|
+
email, password, credentials_file)
|
|
102
|
+
self.user_id = None
|
|
103
|
+
self.notify_email = notify_email
|
|
104
|
+
self.notify_telegram = notify_telegram
|
|
105
|
+
self.poll_interval = poll_interval
|
|
106
|
+
self.biometric_timeout = biometric_timeout
|
|
107
|
+
# auto re-login เมื่อ session หมดอายุ (~4 ชม.)
|
|
108
|
+
self.auto_relogin = True
|
|
109
|
+
self._auth_lock = threading.Lock()
|
|
110
|
+
self._last_auth = 0.0
|
|
111
|
+
self._on_biometric = None
|
|
112
|
+
|
|
113
|
+
# -- credentials --------------------------------------------------- #
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _resolve_credentials(email, password, credentials_file):
|
|
116
|
+
if email and password:
|
|
117
|
+
return email, password
|
|
118
|
+
env_email = os.environ.get("WQ_EMAIL")
|
|
119
|
+
env_pw = os.environ.get("WQ_PASSWORD")
|
|
120
|
+
if env_email and env_pw:
|
|
121
|
+
return env_email, env_pw
|
|
122
|
+
path = credentials_file or os.environ.get("WQ_CREDENTIALS_FILE")
|
|
123
|
+
if path and Path(path).exists():
|
|
124
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
125
|
+
if data.get("email") and data.get("password"):
|
|
126
|
+
return data["email"], data["password"]
|
|
127
|
+
raise BrainAuthError(
|
|
128
|
+
"ไม่พบ credentials — แนะนำตั้ง WQ_EMAIL/WQ_PASSWORD ในไฟล์ .env (ดู .env.example) "
|
|
129
|
+
"| หรือส่ง email/password เข้า BrainClient ตรง ๆ | หรือใช้ไฟล์ JSON ผ่าน "
|
|
130
|
+
"WQ_CREDENTIALS_FILE. ถ้าตั้งใน .env แล้วยังไม่เจอ เช็กว่าติดตั้ง python-dotenv แล้ว")
|
|
131
|
+
|
|
132
|
+
# -- authentication ------------------------------------------------ #
|
|
133
|
+
def authenticate(self, on_biometric=None):
|
|
134
|
+
"""login เข้า BRAIN; on_biometric=callback(url) เพื่อจัดการ Scan เอง"""
|
|
135
|
+
self._on_biometric = on_biometric
|
|
136
|
+
url = f"{self.api_base}/authentication"
|
|
137
|
+
r = self.session.post(url, auth=HTTPBasicAuth(self._email, self._password))
|
|
138
|
+
if r.status_code in (200, 201):
|
|
139
|
+
self._capture_user(r)
|
|
140
|
+
return self
|
|
141
|
+
if r.status_code == requests.codes.unauthorized:
|
|
142
|
+
www = r.headers.get("WWW-Authenticate", "").lower()
|
|
143
|
+
location = r.headers.get("Location")
|
|
144
|
+
if "persona" in www or "biometric" in www or location:
|
|
145
|
+
return self._handle_biometric(r, on_biometric)
|
|
146
|
+
raise BrainAuthError(
|
|
147
|
+
f"login ไม่สำเร็จ (401) — ตรวจ email/password. response: {r.text[:200]}")
|
|
148
|
+
raise BrainAuthError(f"login ไม่สำเร็จ: HTTP {r.status_code} — {r.text[:200]}")
|
|
149
|
+
|
|
150
|
+
def _handle_biometric(self, resp, on_biometric):
|
|
151
|
+
location = resp.headers.get("Location")
|
|
152
|
+
if not location:
|
|
153
|
+
try:
|
|
154
|
+
inquiry = resp.json().get("inquiry")
|
|
155
|
+
except Exception:
|
|
156
|
+
inquiry = None
|
|
157
|
+
location = (f"/authentication/persona?inquiry-id={inquiry}"
|
|
158
|
+
if inquiry else "/authentication/persona")
|
|
159
|
+
bio_url = urljoin(resp.url, location)
|
|
160
|
+
|
|
161
|
+
if on_biometric is not None:
|
|
162
|
+
on_biometric(bio_url)
|
|
163
|
+
return self._wait_for_biometric(bio_url)
|
|
164
|
+
|
|
165
|
+
notified = False
|
|
166
|
+
if self.notify_telegram:
|
|
167
|
+
try:
|
|
168
|
+
send_biometric_telegram(bio_url)
|
|
169
|
+
dest = os.environ.get("TELEGRAM_CHAT_ID")
|
|
170
|
+
print(f"\nส่งลิงก์ Scan ทาง Telegram แล้ว (chat {dest}) — สแกนผ่านมือถือได้เลย")
|
|
171
|
+
notified = True
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"ส่ง Telegram ไม่สำเร็จ ({e}) — ใช้ลิงก์ด้านล่างแทน")
|
|
174
|
+
if self.notify_email:
|
|
175
|
+
try:
|
|
176
|
+
send_biometric_email(bio_url)
|
|
177
|
+
print("ส่งลิงก์ Scan ไปอีเมลแล้ว — สแกนผ่านมือถือได้เลย")
|
|
178
|
+
notified = True
|
|
179
|
+
except Exception as e:
|
|
180
|
+
print(f"ส่งอีเมลไม่สำเร็จ ({e}) — ใช้ลิงก์ด้านล่างแทน")
|
|
181
|
+
|
|
182
|
+
print("\nต้องยืนยันตัวตนด้วย biometric (ขั้นตอน Scan) เปิดลิงก์นี้แล้วสแกน:")
|
|
183
|
+
print(f" {bio_url}\n")
|
|
184
|
+
if notified:
|
|
185
|
+
print(f"กำลังรอการสแกน... (เช็กทุก {self.poll_interval:.0f} วิ)")
|
|
186
|
+
return self._wait_for_biometric(bio_url, poll=True)
|
|
187
|
+
input("ทำ Scan เสร็จแล้วกด Enter เพื่อทำต่อ...")
|
|
188
|
+
return self._wait_for_biometric(bio_url)
|
|
189
|
+
|
|
190
|
+
def _wait_for_biometric(self, bio_url, poll=False):
|
|
191
|
+
deadline = time.time() + self.biometric_timeout
|
|
192
|
+
while True:
|
|
193
|
+
done = self.session.post(bio_url)
|
|
194
|
+
if done.status_code in (200, 201):
|
|
195
|
+
self._capture_user(done)
|
|
196
|
+
return self
|
|
197
|
+
recheck = self.session.post(
|
|
198
|
+
f"{self.api_base}/authentication",
|
|
199
|
+
auth=HTTPBasicAuth(self._email, self._password))
|
|
200
|
+
if recheck.status_code in (200, 201):
|
|
201
|
+
self._capture_user(recheck)
|
|
202
|
+
return self
|
|
203
|
+
if not poll or time.time() >= deadline:
|
|
204
|
+
raise BrainAuthError(
|
|
205
|
+
f"ยืนยัน biometric ไม่สำเร็จ (persona HTTP {done.status_code})")
|
|
206
|
+
time.sleep(self.poll_interval)
|
|
207
|
+
|
|
208
|
+
def _capture_user(self, resp):
|
|
209
|
+
self._last_auth = time.time()
|
|
210
|
+
try:
|
|
211
|
+
data = resp.json()
|
|
212
|
+
self.user_id = (data.get("user") or {}).get("id") or data.get("id")
|
|
213
|
+
except Exception:
|
|
214
|
+
self.user_id = None
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def is_authenticated(self):
|
|
218
|
+
return len(self.session.cookies) > 0
|
|
219
|
+
|
|
220
|
+
# -- request helpers (auto re-login) ------------------------------- #
|
|
221
|
+
def get(self, path, **kwargs):
|
|
222
|
+
u = self._url(path)
|
|
223
|
+
return self._with_relogin(lambda: self.session.get(u, **kwargs), u)
|
|
224
|
+
|
|
225
|
+
def post(self, path, **kwargs):
|
|
226
|
+
u = self._url(path)
|
|
227
|
+
return self._with_relogin(lambda: self.session.post(u, **kwargs), u)
|
|
228
|
+
|
|
229
|
+
def _url(self, path):
|
|
230
|
+
if path.startswith("http"):
|
|
231
|
+
return path
|
|
232
|
+
return f"{self.api_base}/{path.lstrip('/')}"
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _is_auth_url(url):
|
|
236
|
+
u = url.split("?")[0].rstrip("/")
|
|
237
|
+
return u.endswith("/authentication") or "/persona" in url
|
|
238
|
+
|
|
239
|
+
def _relogin_if_stale(self):
|
|
240
|
+
with self._auth_lock:
|
|
241
|
+
if time.time() - self._last_auth < 15:
|
|
242
|
+
return # thread อื่นเพิ่ง re-login ไปแล้ว
|
|
243
|
+
print("session หมดอายุ — กำลัง login ใหม่อัตโนมัติ ...")
|
|
244
|
+
self.authenticate(on_biometric=self._on_biometric)
|
|
245
|
+
|
|
246
|
+
def _with_relogin(self, call, url):
|
|
247
|
+
"""เรียก call(); ถ้าได้ 401 และไม่ใช่ endpoint login -> re-login แล้วลองซ้ำ 1 ครั้ง"""
|
|
248
|
+
resp = call()
|
|
249
|
+
if (getattr(resp, "status_code", None) == 401
|
|
250
|
+
and self.auto_relogin and not self._is_auth_url(url)):
|
|
251
|
+
self._relogin_if_stale()
|
|
252
|
+
resp = call()
|
|
253
|
+
return resp
|
|
254
|
+
|
|
255
|
+
def whoami(self):
|
|
256
|
+
"""ข้อมูลผู้ใช้ปัจจุบัน (ยืนยันว่า session ใช้งานได้)"""
|
|
257
|
+
return self.get("/users/self").json()
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
simulation.py — ส่ง simulation + คัดเก็บ alpha ที่ผ่านเกณฑ์
|
|
3
|
+
|
|
4
|
+
Simulator รับ BrainClient ที่ login แล้ว (จาก auth.py) มาใช้ session ร่วมกัน
|
|
5
|
+
- simulate() : ยิง 1 ตัว, poll จนเสร็จ, ดึง metric จาก /alphas
|
|
6
|
+
- simulate_batch() : รันหลายตัว (สูงสุด 3 พร้อมกันตามลิมิต WorldQuant),
|
|
7
|
+
คัดเก็บอัตโนมัติ, resume (ข้ามตัวที่รันแล้ว), error รายตัวไม่ล้ม batch
|
|
8
|
+
- is_promising() : เกณฑ์ |Sharpe|>=1 และ |Fitness|>=0.9 (จับ flip candidate ด้วย)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# WorldQuant จำกัด simulation ที่รันพร้อมกันได้สูงสุด 3 ตัว/บัญชี
|
|
20
|
+
MAX_CONCURRENT = 3
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Simulator:
|
|
24
|
+
"""ห่อ BrainClient ที่ login แล้ว เพื่อเรียก endpoint ฝั่ง simulation/data"""
|
|
25
|
+
|
|
26
|
+
MIN_SHARPE = 1.0
|
|
27
|
+
MIN_FITNESS = 0.9
|
|
28
|
+
|
|
29
|
+
DEFAULT_SETTINGS = {
|
|
30
|
+
"instrumentType": "EQUITY", "region": "USA", "universe": "TOP3000",
|
|
31
|
+
"delay": 1, "decay": 0, "neutralization": "INDUSTRY", "truncation": 0.08,
|
|
32
|
+
"pasteurization": "ON", "testPeriod": "P0Y0M", "unitHandling": "VERIFY",
|
|
33
|
+
"nanHandling": "OFF", "language": "FASTEXPR", "visualization": False,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def __init__(self, client):
|
|
37
|
+
self.client = client
|
|
38
|
+
self._save_lock = threading.Lock()
|
|
39
|
+
self._print_lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
# -- ข้อมูลประกอบ -------------------------------------------------- #
|
|
42
|
+
def get_operators(self):
|
|
43
|
+
return self.client.get("/operators").json()
|
|
44
|
+
|
|
45
|
+
def get_data_fields(self, region="USA", delay=1, universe="TOP3000",
|
|
46
|
+
instrument_type="EQUITY"):
|
|
47
|
+
return self.client.get("/data-fields", params={
|
|
48
|
+
"instrumentType": instrument_type, "region": region,
|
|
49
|
+
"delay": delay, "universe": universe}).json()
|
|
50
|
+
|
|
51
|
+
def get_alpha(self, alpha_id):
|
|
52
|
+
"""รายละเอียด alpha รวม metric in-sample (`is`) / out-of-sample (`os`)"""
|
|
53
|
+
return self.client.get(f"/alphas/{alpha_id}").json()
|
|
54
|
+
|
|
55
|
+
# -- คัดกรอง + เก็บ ------------------------------------------------ #
|
|
56
|
+
@staticmethod
|
|
57
|
+
def is_promising(alpha_detail, min_sharpe=MIN_SHARPE, min_fitness=MIN_FITNESS,
|
|
58
|
+
include_inverse=True):
|
|
59
|
+
"""
|
|
60
|
+
ผ่านเกณฑ์เมื่อ (in-sample):
|
|
61
|
+
include_inverse=True (ดีฟอลต์): |sharpe|>=min_sharpe และ |fitness|>=min_fitness
|
|
62
|
+
-> จับทั้งตัวบวกแรง และตัวลบแรง (พลิกเครื่องหมายสูตรแล้วดี)
|
|
63
|
+
include_inverse=False: เฉพาะตัวบวก
|
|
64
|
+
"""
|
|
65
|
+
stats = (alpha_detail or {}).get("is") or {}
|
|
66
|
+
sharpe = stats.get("sharpe")
|
|
67
|
+
fitness = stats.get("fitness")
|
|
68
|
+
if sharpe is None or fitness is None:
|
|
69
|
+
return False
|
|
70
|
+
if include_inverse:
|
|
71
|
+
return abs(sharpe) >= min_sharpe and abs(fitness) >= min_fitness
|
|
72
|
+
return sharpe >= min_sharpe and fitness >= min_fitness
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _flatten(alpha_detail, source=None):
|
|
76
|
+
a = alpha_detail or {}
|
|
77
|
+
is_ = a.get("is") or {}
|
|
78
|
+
settings = a.get("settings") or {}
|
|
79
|
+
code = ((a.get("regular") or {}).get("code")
|
|
80
|
+
if isinstance(a.get("regular"), dict) else a.get("regular"))
|
|
81
|
+
sharpe = is_.get("sharpe")
|
|
82
|
+
inverse = sharpe is not None and sharpe < 0 # flip candidate
|
|
83
|
+
return {
|
|
84
|
+
"savedAt": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
85
|
+
"source_hash": (source or {}).get("hash"),
|
|
86
|
+
"alpha_id": a.get("id"), "regular": code, "inverse": inverse,
|
|
87
|
+
"regular_inverse": f"-({code})" if (inverse and code) else None,
|
|
88
|
+
"sharpe": sharpe, "fitness": is_.get("fitness"),
|
|
89
|
+
"turnover": is_.get("turnover"), "returns": is_.get("returns"),
|
|
90
|
+
"drawdown": is_.get("drawdown"), "margin": is_.get("margin"),
|
|
91
|
+
"region": settings.get("region"), "universe": settings.get("universe"),
|
|
92
|
+
"delay": settings.get("delay"),
|
|
93
|
+
"neutralization": settings.get("neutralization"),
|
|
94
|
+
"dateCreated": a.get("dateCreated"),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def save_if_promising(self, alpha_detail, path="promising_alphas.jsonl",
|
|
98
|
+
min_sharpe=MIN_SHARPE, min_fitness=MIN_FITNESS,
|
|
99
|
+
include_inverse=True, source=None):
|
|
100
|
+
"""เก็บ alpha ลงไฟล์ JSONL ถ้าผ่านเกณฑ์ (กันบันทึกซ้ำ alpha id เดิม)"""
|
|
101
|
+
if not self.is_promising(alpha_detail, min_sharpe, min_fitness, include_inverse):
|
|
102
|
+
return False
|
|
103
|
+
record = self._flatten(alpha_detail, source)
|
|
104
|
+
p = Path(path)
|
|
105
|
+
with self._save_lock:
|
|
106
|
+
if p.exists() and record.get("alpha_id"):
|
|
107
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
108
|
+
try:
|
|
109
|
+
if json.loads(line).get("alpha_id") == record["alpha_id"]:
|
|
110
|
+
return False
|
|
111
|
+
except Exception:
|
|
112
|
+
continue
|
|
113
|
+
with p.open("a", encoding="utf-8") as f:
|
|
114
|
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def load_promising(path="promising_alphas.jsonl"):
|
|
119
|
+
p = Path(path)
|
|
120
|
+
if not p.exists():
|
|
121
|
+
return []
|
|
122
|
+
out = []
|
|
123
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
124
|
+
line = line.strip()
|
|
125
|
+
if line:
|
|
126
|
+
try:
|
|
127
|
+
out.append(json.loads(line))
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
# -- simulate ------------------------------------------------------ #
|
|
133
|
+
def simulate(self, regular, settings=None, timeout=600.0, with_metrics=True,
|
|
134
|
+
save_promising=False, save_path="promising_alphas.jsonl",
|
|
135
|
+
min_sharpe=MIN_SHARPE, min_fitness=MIN_FITNESS,
|
|
136
|
+
include_inverse=True, source=None):
|
|
137
|
+
"""
|
|
138
|
+
ยิง simulation 1 ตัวแล้วรอจนเสร็จ
|
|
139
|
+
flow: POST /simulations -> poll GET /simulations/{id} (Retry-After ระหว่างรัน)
|
|
140
|
+
-> GET /alphas/{id} เอา metric
|
|
141
|
+
คืน: {simulation, alpha_id, alpha, promising, saved}
|
|
142
|
+
"""
|
|
143
|
+
payload = {"type": "REGULAR",
|
|
144
|
+
"settings": settings or dict(self.DEFAULT_SETTINGS),
|
|
145
|
+
"regular": regular}
|
|
146
|
+
r = self.client.post("/simulations", json=payload)
|
|
147
|
+
r.raise_for_status()
|
|
148
|
+
progress_url = r.headers.get("Location")
|
|
149
|
+
if not progress_url:
|
|
150
|
+
return {"simulation": r.json(), "alpha_id": None, "alpha": None,
|
|
151
|
+
"promising": False, "saved": False}
|
|
152
|
+
|
|
153
|
+
deadline = time.time() + timeout
|
|
154
|
+
sim = {}
|
|
155
|
+
while time.time() < deadline:
|
|
156
|
+
pr = self.client.get(progress_url) # ผ่าน auto-relogin ถ้า session หมด
|
|
157
|
+
pr.raise_for_status()
|
|
158
|
+
retry_after = pr.headers.get("Retry-After")
|
|
159
|
+
if retry_after: # ยังรันอยู่
|
|
160
|
+
time.sleep(float(retry_after))
|
|
161
|
+
continue
|
|
162
|
+
sim = pr.json()
|
|
163
|
+
break
|
|
164
|
+
else:
|
|
165
|
+
raise TimeoutError("simulation ใช้เวลานานเกินกำหนด")
|
|
166
|
+
|
|
167
|
+
result = {"simulation": sim, "alpha_id": sim.get("alpha"),
|
|
168
|
+
"alpha": None, "promising": False, "saved": False}
|
|
169
|
+
if sim.get("status") and sim["status"] != "COMPLETE":
|
|
170
|
+
return result
|
|
171
|
+
if (with_metrics or save_promising) and result["alpha_id"]:
|
|
172
|
+
result["alpha"] = self.get_alpha(result["alpha_id"])
|
|
173
|
+
result["promising"] = self.is_promising(
|
|
174
|
+
result["alpha"], min_sharpe, min_fitness, include_inverse)
|
|
175
|
+
if save_promising and result["promising"]:
|
|
176
|
+
result["saved"] = self.save_if_promising(
|
|
177
|
+
result["alpha"], save_path, min_sharpe, min_fitness,
|
|
178
|
+
include_inverse, source=source)
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
# -- batch + resume ------------------------------------------------ #
|
|
182
|
+
def simulate_batch(self, expressions, settings=None, save_promising=True,
|
|
183
|
+
save_path="promising_alphas.jsonl", min_sharpe=MIN_SHARPE,
|
|
184
|
+
min_fitness=MIN_FITNESS, include_inverse=True, timeout=600.0,
|
|
185
|
+
max_concurrent=MAX_CONCURRENT, stop_on_error=False,
|
|
186
|
+
skip_processed=True, processed_path="processed.jsonl",
|
|
187
|
+
on_progress=None):
|
|
188
|
+
"""
|
|
189
|
+
รันหลาย alpha (สูงสุด max_concurrent พร้อมกัน, บีบไม่เกิน MAX_CONCURRENT=3)
|
|
190
|
+
expressions: path ไฟล์ (.txt/.jsonl) | list ของ string | list ของ dict {expression, hash}
|
|
191
|
+
skip_processed: resume — ข้ามตัวที่เคยรันแล้ว (key=hash) จด checkpoint ลง processed_path
|
|
192
|
+
คืน: list ของ record ต่อ alpha (เรียงตามลำดับ input)
|
|
193
|
+
"""
|
|
194
|
+
if isinstance(expressions, str):
|
|
195
|
+
items = self.load_alpha_items(expressions)
|
|
196
|
+
else:
|
|
197
|
+
items = []
|
|
198
|
+
for x in expressions:
|
|
199
|
+
if isinstance(x, dict):
|
|
200
|
+
expr = x.get("expression") or x.get("regular")
|
|
201
|
+
if expr:
|
|
202
|
+
items.append({"expression": expr, "source": x})
|
|
203
|
+
else:
|
|
204
|
+
items.append({"expression": x, "source": None})
|
|
205
|
+
total = len(items)
|
|
206
|
+
workers = max(1, min(max_concurrent, MAX_CONCURRENT, total or 1))
|
|
207
|
+
|
|
208
|
+
def item_key(item):
|
|
209
|
+
return (item.get("source") or {}).get("hash") or item["expression"]
|
|
210
|
+
|
|
211
|
+
processed = self._load_processed_keys(processed_path) if skip_processed else set()
|
|
212
|
+
|
|
213
|
+
def run_one(item):
|
|
214
|
+
expr = item["expression"]
|
|
215
|
+
source = item.get("source")
|
|
216
|
+
rec = {"regular": expr, "source_hash": (source or {}).get("hash"),
|
|
217
|
+
"ok": False, "status": None, "alpha_id": None, "sharpe": None,
|
|
218
|
+
"fitness": None, "promising": False, "saved": False,
|
|
219
|
+
"skipped": False, "error": None}
|
|
220
|
+
try:
|
|
221
|
+
r = self.simulate(expr, settings=settings, timeout=timeout,
|
|
222
|
+
with_metrics=True, save_promising=save_promising,
|
|
223
|
+
save_path=save_path, min_sharpe=min_sharpe,
|
|
224
|
+
min_fitness=min_fitness,
|
|
225
|
+
include_inverse=include_inverse, source=source)
|
|
226
|
+
is_ = (r["alpha"] or {}).get("is", {})
|
|
227
|
+
rec.update(ok=True, status=r["simulation"].get("status"),
|
|
228
|
+
alpha_id=r["alpha_id"], sharpe=is_.get("sharpe"),
|
|
229
|
+
fitness=is_.get("fitness"), promising=r["promising"],
|
|
230
|
+
saved=r["saved"])
|
|
231
|
+
except Exception as e:
|
|
232
|
+
rec["error"] = str(e)
|
|
233
|
+
if skip_processed:
|
|
234
|
+
self._append_processed(processed_path, item_key(item), rec)
|
|
235
|
+
return rec
|
|
236
|
+
|
|
237
|
+
def report(done, rec):
|
|
238
|
+
if on_progress:
|
|
239
|
+
on_progress(rec)
|
|
240
|
+
return
|
|
241
|
+
with self._print_lock:
|
|
242
|
+
if rec["skipped"]:
|
|
243
|
+
print(f"[{done}/{total}] {rec['regular'][:45]:45} skip (เคยรันแล้ว)")
|
|
244
|
+
elif rec["ok"]:
|
|
245
|
+
flip = " (flip ต้อง negate)" if (
|
|
246
|
+
rec["sharpe"] is not None and rec["sharpe"] < 0) else ""
|
|
247
|
+
tag = ("เก็บแล้ว" + flip if rec["saved"]
|
|
248
|
+
else "ผ่านเกณฑ์" + flip if rec["promising"] else "ไม่ผ่าน")
|
|
249
|
+
print(f"[{done}/{total}] {rec['regular'][:45]:45} "
|
|
250
|
+
f"sharpe={rec['sharpe']} fitness={rec['fitness']} {tag}")
|
|
251
|
+
else:
|
|
252
|
+
print(f"[{done}/{total}] {rec['regular'][:45]:45} ERROR: {rec['error']}")
|
|
253
|
+
|
|
254
|
+
def skipped_rec(item):
|
|
255
|
+
src = item.get("source") or {}
|
|
256
|
+
return {"regular": item["expression"], "source_hash": src.get("hash"),
|
|
257
|
+
"ok": False, "status": None, "alpha_id": None, "sharpe": None,
|
|
258
|
+
"fitness": None, "promising": False, "saved": False,
|
|
259
|
+
"skipped": True, "error": None}
|
|
260
|
+
|
|
261
|
+
results = [None] * total
|
|
262
|
+
to_run = []
|
|
263
|
+
for i, item in enumerate(items):
|
|
264
|
+
if skip_processed and item_key(item) in processed:
|
|
265
|
+
results[i] = skipped_rec(item)
|
|
266
|
+
else:
|
|
267
|
+
to_run.append((i, item))
|
|
268
|
+
n_skip = total - len(to_run)
|
|
269
|
+
|
|
270
|
+
if workers == 1:
|
|
271
|
+
done = n_skip
|
|
272
|
+
for i, item in to_run:
|
|
273
|
+
rec = run_one(item)
|
|
274
|
+
results[i] = rec
|
|
275
|
+
done += 1
|
|
276
|
+
report(done, rec)
|
|
277
|
+
if rec["error"] and stop_on_error:
|
|
278
|
+
break
|
|
279
|
+
else:
|
|
280
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
281
|
+
fut_to_idx = {ex.submit(run_one, item): i for i, item in to_run}
|
|
282
|
+
done = n_skip
|
|
283
|
+
for fut in as_completed(fut_to_idx):
|
|
284
|
+
idx = fut_to_idx[fut]
|
|
285
|
+
rec = fut.result()
|
|
286
|
+
results[idx] = rec
|
|
287
|
+
done += 1
|
|
288
|
+
report(done, rec)
|
|
289
|
+
|
|
290
|
+
results = [r for r in results if r is not None]
|
|
291
|
+
ok = sum(r["ok"] for r in results)
|
|
292
|
+
promising = sum(r["promising"] for r in results)
|
|
293
|
+
saved = sum(r["saved"] for r in results)
|
|
294
|
+
print(f"\nสรุป: รันใหม่สำเร็จ {ok} | ข้าม {n_skip} | ผ่านเกณฑ์ {promising} | "
|
|
295
|
+
f"เก็บใหม่ {saved} -> {save_path}")
|
|
296
|
+
return results
|
|
297
|
+
|
|
298
|
+
# -- checkpoint (resume) ------------------------------------------- #
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _load_processed_keys(path):
|
|
301
|
+
p = Path(path)
|
|
302
|
+
if not p.exists():
|
|
303
|
+
return set()
|
|
304
|
+
keys = set()
|
|
305
|
+
for line in p.read_text(encoding="utf-8").splitlines():
|
|
306
|
+
line = line.strip()
|
|
307
|
+
if not line:
|
|
308
|
+
continue
|
|
309
|
+
try:
|
|
310
|
+
k = json.loads(line).get("key")
|
|
311
|
+
if k is not None:
|
|
312
|
+
keys.add(k)
|
|
313
|
+
except Exception:
|
|
314
|
+
continue
|
|
315
|
+
return keys
|
|
316
|
+
|
|
317
|
+
def _append_processed(self, path, key, rec):
|
|
318
|
+
entry = {"key": key, "source_hash": rec.get("source_hash"),
|
|
319
|
+
"alpha_id": rec.get("alpha_id"), "regular": rec.get("regular"),
|
|
320
|
+
"ok": rec.get("ok"), "status": rec.get("status"),
|
|
321
|
+
"promising": rec.get("promising"), "error": rec.get("error"),
|
|
322
|
+
"at": datetime.now(timezone.utc).isoformat(timespec="seconds")}
|
|
323
|
+
with self._save_lock:
|
|
324
|
+
with Path(path).open("a", encoding="utf-8") as f:
|
|
325
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
326
|
+
|
|
327
|
+
# -- โหลดรายการ alpha จากไฟล์ -------------------------------------- #
|
|
328
|
+
@staticmethod
|
|
329
|
+
def load_alpha_items(path):
|
|
330
|
+
"""อ่าน list ของ {expression, source} จากไฟล์ — รองรับ JSONL และข้อความล้วน"""
|
|
331
|
+
items = []
|
|
332
|
+
for raw in Path(path).read_text(encoding="utf-8").splitlines():
|
|
333
|
+
line = raw.strip()
|
|
334
|
+
if not line or line.startswith("#"):
|
|
335
|
+
continue
|
|
336
|
+
if line.startswith("{"):
|
|
337
|
+
try:
|
|
338
|
+
obj = json.loads(line)
|
|
339
|
+
except Exception:
|
|
340
|
+
continue
|
|
341
|
+
expr = obj.get("expression") or obj.get("regular")
|
|
342
|
+
if expr:
|
|
343
|
+
items.append({"expression": expr, "source": obj})
|
|
344
|
+
else:
|
|
345
|
+
items.append({"expression": line, "source": None})
|
|
346
|
+
return items
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def load_expressions(cls, path):
|
|
350
|
+
return [it["expression"] for it in cls.load_alpha_items(path)]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brain-login-and-sim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WorldQuant BRAIN client — login (biometric Scan + auto re-login), simulation, batch run, and auto-keeping promising alphas
|
|
5
|
+
Author-email: Tanakrit <9tanakrit.work@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Tanakrit/brain-login-and-sim
|
|
8
|
+
Project-URL: Issues, https://github.com/Tanakrit/brain-login-and-sim/issues
|
|
9
|
+
Keywords: worldquant,brain,alpha,quant,simulation,trading
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: requests>=2.28
|
|
20
|
+
Requires-Dist: python-dotenv>=0.19
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build; extra == "dev"
|
|
23
|
+
Requires-Dist: twine; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# brain-login-and-sim
|
|
28
|
+
|
|
29
|
+
ไคลเอนต์ Python สำหรับ **WorldQuant BRAIN** — login (จัดการ biometric "Scan" + auto re-login),
|
|
30
|
+
ส่ง simulation, รัน batch (สูงสุด 3 พร้อมกันตามลิมิต), และคัดเก็บ alpha ที่ผ่านเกณฑ์อัตโนมัติ
|
|
31
|
+
|
|
32
|
+
## ติดตั้ง
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install brain-login-and-sim
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## ตั้งค่า credentials
|
|
39
|
+
|
|
40
|
+
สร้างไฟล์ `.env` (ดู `.env.example`):
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
WQ_EMAIL=your-email@example.com
|
|
44
|
+
WQ_PASSWORD=your-password
|
|
45
|
+
# ส่งลิงก์ Scan เข้า Telegram (ทางเลือก แต่แนะนำ)
|
|
46
|
+
TELEGRAM_BOT_TOKEN=123456789:AAH...
|
|
47
|
+
TELEGRAM_CHAT_ID=123456789
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## ใช้งาน
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from brain_login_and_sim import BrainClient, Simulator
|
|
54
|
+
|
|
55
|
+
# login — ถ้าระบบขอ biometric Scan จะส่งลิงก์เข้า Telegram แล้วรอจนสแกนเสร็จ
|
|
56
|
+
client = BrainClient(notify_telegram=True).authenticate()
|
|
57
|
+
print(client.whoami()["id"])
|
|
58
|
+
|
|
59
|
+
sim = Simulator(client)
|
|
60
|
+
|
|
61
|
+
# ยิง 1 ตัว
|
|
62
|
+
r = sim.simulate("ts_rank(close, 5)")
|
|
63
|
+
print(r["simulation"]["status"], r["alpha"]["is"]["sharpe"])
|
|
64
|
+
|
|
65
|
+
# รันหลายตัวจากไฟล์ JSONL คัดเก็บตัวที่ผ่านเกณฑ์อัตโนมัติ
|
|
66
|
+
sim.simulate_batch("alphas.jsonl", save_promising=True)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## ฟีเจอร์หลัก
|
|
70
|
+
|
|
71
|
+
- **Login + biometric Scan** — ส่งลิงก์สแกนเข้า Telegram/อีเมล แล้ว poll รอจนเสร็จ (ไม่ต้องเฝ้าจอ)
|
|
72
|
+
- **Auto re-login** — เมื่อ session หมดอายุ (WorldQuant ให้ ~4 ชม.) เจอ 401 จะ login ใหม่แล้วลองซ้ำให้เอง
|
|
73
|
+
- **คัดเก็บ alpha ดี** — เกณฑ์ `|Sharpe| ≥ 1` และ `|Fitness| ≥ 0.9` (ค่าสัมบูรณ์ จับ flip candidate ที่ negate แล้วดี)
|
|
74
|
+
- **Batch** — รันสูงสุด 3 ตัวพร้อมกัน (ลิมิต WorldQuant), error รายตัวไม่ล้มทั้ง batch
|
|
75
|
+
- **Resume** — จด checkpoint ด้วย hash รันไฟล์เดิมซ้ำจะข้ามตัวที่รันแล้ว รันต่อจากที่ค้าง
|
|
76
|
+
- **Input ยืดหยุ่น** — list ของสูตร, ไฟล์ `.txt` (1 สูตร/บรรทัด), หรือ `.jsonl` ที่มี field `expression` (+`hash`)
|
|
77
|
+
|
|
78
|
+
## รูปแบบ JSONL
|
|
79
|
+
|
|
80
|
+
```jsonl
|
|
81
|
+
{"hash": "f7b3...", "expression": "rank(reverse(ts_delta(anl4_cfo_median, 10)))", "status": "generated"}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
ระบบดึง `expression` ไปรัน และถ้าผ่านเกณฑ์จะเก็บ `source_hash` ไว้อ้างกลับไฟล์ต้นฉบับใน `promising_alphas.jsonl`
|
|
85
|
+
|
|
86
|
+
## API ย่อ
|
|
87
|
+
|
|
88
|
+
`BrainClient`: `authenticate(on_biometric=None)`, `whoami()`, `get(path)`, `post(path)`, `auto_relogin`
|
|
89
|
+
|
|
90
|
+
`Simulator(client)`: `simulate(...)`, `simulate_batch(...)`, `is_promising(...)`, `save_if_promising(...)`,
|
|
91
|
+
`load_promising(path)`, `get_operators()`, `get_data_fields(...)`, `get_alpha(id)`
|
|
92
|
+
|
|
93
|
+
## ทดสอบ
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python tests/test_full_loop.py # mock API ไม่ต้องต่อเน็ต/รหัสผ่าน
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/brain_login_and_sim/__init__.py
|
|
5
|
+
src/brain_login_and_sim/auth.py
|
|
6
|
+
src/brain_login_and_sim/simulation.py
|
|
7
|
+
src/brain_login_and_sim.egg-info/PKG-INFO
|
|
8
|
+
src/brain_login_and_sim.egg-info/SOURCES.txt
|
|
9
|
+
src/brain_login_and_sim.egg-info/dependency_links.txt
|
|
10
|
+
src/brain_login_and_sim.egg-info/requires.txt
|
|
11
|
+
src/brain_login_and_sim.egg-info/top_level.txt
|
|
12
|
+
tests/test_full_loop.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brain_login_and_sim
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Full-loop test (mock API, no network/credentials). Run: pytest -s หรือ python tests/test_full_loop.py"""
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from brain_login_and_sim import BrainClient, Simulator
|
|
10
|
+
|
|
11
|
+
_p = 0
|
|
12
|
+
_f = 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check(name, cond):
|
|
16
|
+
global _p, _f
|
|
17
|
+
if cond:
|
|
18
|
+
_p += 1
|
|
19
|
+
print(" PASS", name)
|
|
20
|
+
else:
|
|
21
|
+
_f += 1
|
|
22
|
+
print(" FAIL", name, "<--")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
API = "https://api.worldquantbrain.com"
|
|
26
|
+
METRICS = {
|
|
27
|
+
"rank(close - open)": (1.40, 0.95),
|
|
28
|
+
"-rank(returns)": (-1.80, -0.95),
|
|
29
|
+
"ts_rank(close, 5)": (0.50, 0.40),
|
|
30
|
+
"broken_expr(": None,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FR:
|
|
35
|
+
def __init__(self, code=200, data=None, headers=None, url=API + "/authentication"):
|
|
36
|
+
self.status_code = code
|
|
37
|
+
self._d = data or {}
|
|
38
|
+
self.headers = headers or {}
|
|
39
|
+
self.text = json.dumps(self._d)
|
|
40
|
+
self.url = url
|
|
41
|
+
|
|
42
|
+
def json(self):
|
|
43
|
+
return self._d
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def ok(self):
|
|
47
|
+
return self.status_code < 400
|
|
48
|
+
|
|
49
|
+
def raise_for_status(self):
|
|
50
|
+
if self.status_code >= 400:
|
|
51
|
+
raise requests.HTTPError(f"HTTP {self.status_code}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FS:
|
|
55
|
+
def __init__(self, biometric=False):
|
|
56
|
+
self.headers = {}
|
|
57
|
+
self.cookies = []
|
|
58
|
+
self.biometric = biometric
|
|
59
|
+
self._bio = False
|
|
60
|
+
self._n = 0
|
|
61
|
+
self._se = {}
|
|
62
|
+
self._poll = {}
|
|
63
|
+
self.expired = False
|
|
64
|
+
|
|
65
|
+
def post(self, url, auth=None, json=None, **kw):
|
|
66
|
+
if url.endswith("/authentication"):
|
|
67
|
+
if self.biometric and not self._bio:
|
|
68
|
+
return FR(401, {}, {"WWW-Authenticate": "persona",
|
|
69
|
+
"Location": "/authentication/persona?inquiry-id=X"},
|
|
70
|
+
url=API + "/authentication")
|
|
71
|
+
self.expired = False
|
|
72
|
+
return FR(201, {"user": {"id": "U1"}})
|
|
73
|
+
if "/persona" in url:
|
|
74
|
+
self._bio = True
|
|
75
|
+
self.expired = False
|
|
76
|
+
return FR(201, {"user": {"id": "U1"}})
|
|
77
|
+
if url.endswith("/simulations"):
|
|
78
|
+
expr = (json or {}).get("regular")
|
|
79
|
+
if METRICS.get(expr, "x") is None:
|
|
80
|
+
return FR(400, {"error": "bad"})
|
|
81
|
+
self._n += 1
|
|
82
|
+
sid = f"SIM{self._n}"
|
|
83
|
+
self._se[sid] = expr
|
|
84
|
+
self._poll[sid] = 0
|
|
85
|
+
return FR(201, {}, {"Location": f"{API}/simulations/{sid}"})
|
|
86
|
+
return FR(404, {})
|
|
87
|
+
|
|
88
|
+
def get(self, url, params=None, **kw):
|
|
89
|
+
if self.expired:
|
|
90
|
+
return FR(401, {"error": "expired"})
|
|
91
|
+
if "/simulations/" in url:
|
|
92
|
+
sid = url.rsplit("/", 1)[-1]
|
|
93
|
+
self._poll[sid] += 1
|
|
94
|
+
if self._poll[sid] == 1:
|
|
95
|
+
return FR(200, {"progress": 0.5}, {"Retry-After": "0"})
|
|
96
|
+
expr = self._se[sid]
|
|
97
|
+
return FR(200, {"id": sid, "status": "COMPLETE", "alpha": f"ALPHA_{sid}",
|
|
98
|
+
"regular": expr, "settings": {"region": "USA",
|
|
99
|
+
"universe": "TOP3000", "delay": 1, "neutralization": "INDUSTRY"}})
|
|
100
|
+
if "/alphas/" in url:
|
|
101
|
+
aid = url.rsplit("/", 1)[-1]
|
|
102
|
+
sid = aid.replace("ALPHA_", "")
|
|
103
|
+
expr = self._se.get(sid)
|
|
104
|
+
sh, fi = METRICS[expr]
|
|
105
|
+
return FR(200, {"id": aid, "regular": expr,
|
|
106
|
+
"settings": {"region": "USA", "universe": "TOP3000",
|
|
107
|
+
"delay": 1, "neutralization": "INDUSTRY"},
|
|
108
|
+
"is": {"sharpe": sh, "fitness": fi, "turnover": 0.3,
|
|
109
|
+
"returns": 0.1, "drawdown": 0.2, "margin": 0.001}, "os": {}})
|
|
110
|
+
if url.endswith("/users/self"):
|
|
111
|
+
return FR(200, {"id": "U1"})
|
|
112
|
+
if url.endswith("/operators"):
|
|
113
|
+
return FR(200, [{"name": "rank"}, {"name": "ts_rank"}])
|
|
114
|
+
if "/data-fields" in url:
|
|
115
|
+
return FR(200, {"count": 2, "results": []})
|
|
116
|
+
return FR(404, {})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def mk(biometric=False):
|
|
120
|
+
c = BrainClient(email="t@e.com", password="x")
|
|
121
|
+
c.session = FS(biometric=biometric)
|
|
122
|
+
return c
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run():
|
|
126
|
+
print("=" * 50)
|
|
127
|
+
print("FULL-LOOP TEST")
|
|
128
|
+
print("=" * 50)
|
|
129
|
+
|
|
130
|
+
print("\n[1] login direct")
|
|
131
|
+
c = mk().authenticate()
|
|
132
|
+
check("returns client", isinstance(c, BrainClient))
|
|
133
|
+
check("uid U1", c.user_id == "U1")
|
|
134
|
+
check("whoami", c.whoami().get("id") == "U1")
|
|
135
|
+
|
|
136
|
+
print("\n[2] login biometric")
|
|
137
|
+
seen = {}
|
|
138
|
+
c = mk(biometric=True)
|
|
139
|
+
c.authenticate(on_biometric=lambda u: seen.update(u=u))
|
|
140
|
+
check("persona url", "/persona" in seen.get("u", ""))
|
|
141
|
+
check("login ok", c.user_id == "U1")
|
|
142
|
+
|
|
143
|
+
print("\n[3] single simulate")
|
|
144
|
+
sim = Simulator(mk().authenticate())
|
|
145
|
+
r = sim.simulate("rank(close - open)")
|
|
146
|
+
check("COMPLETE", r["simulation"]["status"] == "COMPLETE")
|
|
147
|
+
check("alpha_id", bool(r["alpha_id"]))
|
|
148
|
+
check("sharpe 1.4", r["alpha"]["is"]["sharpe"] == 1.40)
|
|
149
|
+
check("promising", r["promising"] is True)
|
|
150
|
+
|
|
151
|
+
print("\n[4] is_promising")
|
|
152
|
+
check("pos", Simulator.is_promising({"is": {"sharpe": 1.4, "fitness": 0.95}}))
|
|
153
|
+
check("neg abs", Simulator.is_promising({"is": {"sharpe": -1.8, "fitness": -0.95}}))
|
|
154
|
+
check("weak", Simulator.is_promising({"is": {"sharpe": 0.5, "fitness": 0.4}}) is False)
|
|
155
|
+
check("neg no-inv", Simulator.is_promising(
|
|
156
|
+
{"is": {"sharpe": -1.8, "fitness": -0.95}}, include_inverse=False) is False)
|
|
157
|
+
|
|
158
|
+
print("\n[5-7] batch + resume + inverse + error")
|
|
159
|
+
with tempfile.TemporaryDirectory() as d:
|
|
160
|
+
tmp = Path(d)
|
|
161
|
+
jl = tmp / "a.jsonl"
|
|
162
|
+
jl.write_text("\n".join(json.dumps(o) for o in [
|
|
163
|
+
{"hash": "H_pos", "expression": "rank(close - open)"},
|
|
164
|
+
{"hash": "H_neg", "expression": "-rank(returns)"},
|
|
165
|
+
{"hash": "H_weak", "expression": "ts_rank(close, 5)"},
|
|
166
|
+
{"hash": "H_err", "expression": "broken_expr("}]), encoding="utf-8")
|
|
167
|
+
sv = str(tmp / "p.jsonl")
|
|
168
|
+
pr = str(tmp / "proc.jsonl")
|
|
169
|
+
sim = Simulator(mk().authenticate())
|
|
170
|
+
r1 = sim.simulate_batch(str(jl), save_promising=True, save_path=sv,
|
|
171
|
+
processed_path=pr, max_concurrent=3)
|
|
172
|
+
check("ran 4", len([x for x in r1 if not x["skipped"]]) == 4)
|
|
173
|
+
check("promising 2", sum(x["promising"] for x in r1) == 2)
|
|
174
|
+
check("saved 2", sum(x["saved"] for x in r1) == 2)
|
|
175
|
+
check("1 error", sum(1 for x in r1 if x["error"]) == 1)
|
|
176
|
+
saved = Simulator.load_promising(sv)
|
|
177
|
+
neg = next((s for s in saved if s["source_hash"] == "H_neg"), None)
|
|
178
|
+
check("inverse flag", neg and neg["inverse"] is True)
|
|
179
|
+
check("neg expr", "-(-rank(returns))" == (neg and neg["regular_inverse"]))
|
|
180
|
+
check("source_hash", any(s["source_hash"] == "H_pos" for s in saved))
|
|
181
|
+
r2 = sim.simulate_batch(str(jl), save_promising=True, save_path=sv,
|
|
182
|
+
processed_path=pr, max_concurrent=3)
|
|
183
|
+
check("resume skip all", all(x["skipped"] for x in r2) and len(r2) == 4)
|
|
184
|
+
check("no dup save", len(Simulator.load_promising(sv)) == len(saved))
|
|
185
|
+
|
|
186
|
+
print("\n[8] auto re-login")
|
|
187
|
+
c = mk().authenticate()
|
|
188
|
+
c.session.expired = True
|
|
189
|
+
c._last_auth = 0
|
|
190
|
+
rr = c.get("/operators")
|
|
191
|
+
check("401 -> relogin -> 200", rr.status_code == 200)
|
|
192
|
+
check("usable again", c.session.expired is False)
|
|
193
|
+
|
|
194
|
+
print("\n" + "=" * 50)
|
|
195
|
+
print(f"RESULT: passed {_p} | failed {_f}")
|
|
196
|
+
print("=" * 50)
|
|
197
|
+
return _f
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_full_loop():
|
|
201
|
+
assert run() == 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(1 if run() else 0)
|