Files
alpha_tools/alpha_submit/main.go
T

262 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
)
const (
totalRetryCount = 100000
nacosURL = "http://192.168.31.41:30848/nacos/v1/cs/configs?dataId=wq_account&group=quantify"
)
// NacosConfig 对应 nacos 返回的账号配置
type NacosConfig struct {
UserName string `json:"user_name"`
Password string `json:"password"`
}
// basicAuthTransport 为每个请求自动添加 Basic Auth 头
type basicAuthTransport struct {
username string
password string
base http.RoundTripper
}
func (t *basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.SetBasicAuth(t.username, t.password)
return t.base.RoundTrip(req)
}
// Login 登录 WorldQuant Brain API,返回带 BasicAuth 的 HTTP Client
func Login() (*http.Client, error) {
// 1. 从 nacos 获取账号密码
resp, err := http.Get(nacosURL)
if err != nil {
log.Printf("获取账号配置失败: %v", err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("nacos 返回非 200 状态码: %d", resp.StatusCode)
}
var config NacosConfig
if err := json.NewDecoder(resp.Body).Decode(&config); err != nil {
return nil, fmt.Errorf("解析 nacos 配置失败: %w", err)
}
log.Printf("正在登录账户: %s", config.UserName)
// 2. 创建 HTTP Client,后续所有请求都会使用 BasicAuth
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &basicAuthTransport{
username: config.UserName,
password: config.Password,
base: http.DefaultTransport,
},
}
// 3. 发送登录请求
loginReq, _ := http.NewRequest("POST", "https://api.worldquantbrain.com/authentication", nil)
loginResp, err := client.Do(loginReq)
if err != nil {
return nil, fmt.Errorf("登录请求失败: %w", err)
}
defer loginResp.Body.Close()
log.Printf("登录状态: %d", loginResp.StatusCode)
if loginResp.StatusCode == http.StatusCreated {
log.Println("登录成功!")
return client, nil
}
body, _ := io.ReadAll(loginResp.Body)
return nil, fmt.Errorf("登录失败: %s", string(body))
}
// SubmitAlpha 提交 alpha,自动重试直到成功或达到最大重试次数
// 返回 nil 表示提交成功,否则返回错误
func SubmitAlpha(alphaID string) error {
retryCount := 0
var client *http.Client
for retryCount < totalRetryCount {
// 如果没有有效 client,则重新登录
if client == nil {
var err error
client, err = Login()
if err != nil {
log.Printf("登录失败: %v, 10秒后重试 (重试次数: %d)", err, retryCount)
time.Sleep(10 * time.Second)
retryCount++
continue
}
}
url := fmt.Sprintf("https://api.worldquantbrain.com/alphas/%s/submit", alphaID)
log.Printf("请求 URL: %s", url)
// 发送提交请求
resp, err := client.Post(url, "application/json", nil)
if err != nil {
log.Printf("网络请求异常 (alpha=%s, retry=%d): %v", alphaID, retryCount, err)
retryCount++
time.Sleep(10 * time.Second)
continue
}
// 处理 400 特殊情形:已提交,进入轮询
if resp.StatusCode == http.StatusBadRequest {
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
bodyStr := string(bodyBytes)
if strings.Contains(bodyStr, "The plain HTTP request was sent to HTTPS port") {
log.Println("Alpha 已提交,正在轮询状态...")
pollInterval := 1.0 // 秒
for {
time.Sleep(time.Duration(pollInterval) * time.Second)
fmt.Print(".")
// 重新 GET 查询状态
pollResp, err := client.Get(url)
if err != nil {
log.Printf("轮询请求失败: %v", err)
break
}
// 检查 Retry-After 头
if retryAfter := pollResp.Header.Get("Retry-After"); retryAfter != "" {
if f, err := strconv.ParseFloat(retryAfter, 64); err == nil && f > 0 {
pollInterval = max(f, 3.0)
} else {
pollInterval = 3.0
}
} else {
pollInterval = 3.0
}
// 当状态不再是 400 且不包含该特定消息时,退出轮询
if pollResp.StatusCode != http.StatusBadRequest {
resp = pollResp
break
}
bodyBytes2, _ := io.ReadAll(pollResp.Body)
pollResp.Body.Close()
if !strings.Contains(string(bodyBytes2), "The plain HTTP request was sent to HTTPS port") {
resp = pollResp
break
}
pollResp.Body.Close()
}
log.Printf("轮询结束,最终状态码: %d", resp.StatusCode)
} else {
// 非特殊 400,按普通错误处理
resp.Body.Close()
}
}
// 确保 resp 不为 nil(如果上面分支没有设置 resp,则跳过本次循环)
if resp == nil {
retryCount++
continue
}
// 根据状态码处理
switch resp.StatusCode {
case http.StatusTooManyRequests: // 429
log.Println("触发限流 (429),休眠 60 秒后重试")
resp.Body.Close()
time.Sleep(60 * time.Second)
retryCount++
continue
case http.StatusUnauthorized: // 401
log.Println("认证失效,重新登录")
resp.Body.Close()
client = nil
retryCount++
continue
case http.StatusNotFound: // 404
log.Printf("Alpha %s 不存在或超时,重试 (%d/%d)", alphaID, retryCount+1, totalRetryCount)
resp.Body.Close()
retryCount++
continue
case http.StatusForbidden: // 403
log.Printf("%s 提交失败 (403)", alphaID)
var failChecks []map[string]interface{}
var bodyMap map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&bodyMap); err == nil {
if isObj, ok := bodyMap["is"].(map[string]interface{}); ok {
if checks, ok := isObj["checks"].([]interface{}); ok {
for _, c := range checks {
if ch, ok := c.(map[string]interface{}); ok {
if result, ok := ch["result"]; ok && result == "FAIL" {
failChecks = append(failChecks, ch)
}
}
}
}
}
}
resp.Body.Close()
log.Printf("失败的检查项: %v", failChecks)
// 如果因为提交次数超限而失败,放弃重试
for _, ch := range failChecks {
if name, ok := ch["name"]; ok {
if name == "REGULAR_SUBMISSION" || name == "SUPER_SUBMISSION" {
return fmt.Errorf("提交次数超过限制: %v", failChecks)
}
}
}
return fmt.Errorf("提交失败,HTTP 403")
case http.StatusOK: // 200
log.Printf("%s 提交成功", alphaID)
resp.Body.Close()
return nil
default:
// 处理 5xx 错误
if resp.StatusCode >= 500 && resp.StatusCode < 600 {
log.Printf("服务器错误 %d,5 秒后重试", resp.StatusCode)
resp.Body.Close()
time.Sleep(5 * time.Second)
retryCount++
continue
}
// 其他非 2xx 状态码视为失败
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return fmt.Errorf("未处理的响应状态码 %d: %s", resp.StatusCode, string(bodyBytes))
}
}
return fmt.Errorf("达到最大重试次数 %d,提交失败", totalRetryCount)
}
// max 返回两个 float64 中的较大值
func max(a, b float64) float64 {
if a > b {
return a
}
return b
}
func main() {
// 使用示例
alphaID := "your_alpha_id_here"
if err := SubmitAlpha(alphaID); err != nil {
log.Fatalf("提交失败: %v", err)
}
log.Println("提交完成")
}