You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
480 lines
11 KiB
480 lines
11 KiB
package main
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
const (
|
|
baseURL = "https://api.worldquantbrain.com"
|
|
zeroStreakThreshold = 5 * 252
|
|
requiredDays = 2920
|
|
)
|
|
|
|
type Client struct {
|
|
client *fasthttp.Client
|
|
username string
|
|
password string
|
|
}
|
|
|
|
type AlphaRecord struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
DateCreated string `json:"dateCreated"`
|
|
Sharpe float64 `json:"sharpe"`
|
|
Fitness float64 `json:"fitness"`
|
|
Turnover float64 `json:"turnover"`
|
|
Margin float64 `json:"margin"`
|
|
LongCount float64 `json:"longCount"`
|
|
ShortCount float64 `json:"shortCount"`
|
|
Decay int `json:"decay"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type AlphaResponse struct {
|
|
Results []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
DateCreated string `json:"dateCreated"`
|
|
Is map[string]interface{} `json:"is"`
|
|
Settings struct {
|
|
Decay int `json:"decay"`
|
|
} `json:"settings"`
|
|
Regular struct {
|
|
Code string `json:"code"`
|
|
} `json:"regular"`
|
|
} `json:"results"`
|
|
}
|
|
|
|
type PnlResponse struct {
|
|
Records [][]interface{} `json:"records"`
|
|
}
|
|
|
|
func NewClient(username, password string) *Client {
|
|
return &Client{
|
|
client: &fasthttp.Client{
|
|
ReadTimeout: 60 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
MaxIdleConnDuration: 120 * time.Second,
|
|
},
|
|
username: username,
|
|
password: password,
|
|
}
|
|
}
|
|
|
|
func (c *Client) getAuthHeader() string {
|
|
auth := base64.StdEncoding.EncodeToString([]byte(c.username + ":" + c.password))
|
|
return "Basic " + auth
|
|
}
|
|
|
|
func (c *Client) Login() error {
|
|
req := fasthttp.AcquireRequest()
|
|
resp := fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
|
|
req.SetRequestURI(baseURL + "/authentication")
|
|
req.Header.SetMethod("POST")
|
|
req.Header.Set("Authorization", c.getAuthHeader())
|
|
|
|
err := c.client.Do(req, resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(string(resp.Body()))
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) WaitGet(url string, maxRetries int) (*fasthttp.Response, error) {
|
|
retries := 0
|
|
for retries < maxRetries {
|
|
for {
|
|
req := fasthttp.AcquireRequest()
|
|
resp := fasthttp.AcquireResponse()
|
|
req.SetRequestURI(url)
|
|
req.Header.SetMethod("GET")
|
|
req.Header.Set("Authorization", c.getAuthHeader())
|
|
|
|
err := c.client.Do(req, resp)
|
|
if err != nil {
|
|
fasthttp.ReleaseRequest(req)
|
|
fasthttp.ReleaseResponse(resp)
|
|
return nil, err
|
|
}
|
|
|
|
retryAfter := resp.Header.Peek("Retry-After")
|
|
if len(retryAfter) == 0 {
|
|
fasthttp.ReleaseRequest(req)
|
|
if resp.StatusCode() < 400 {
|
|
return resp, nil
|
|
}
|
|
fasthttp.ReleaseResponse(resp)
|
|
break
|
|
}
|
|
|
|
sleepSec, _ := strconv.ParseFloat(string(retryAfter), 64)
|
|
time.Sleep(time.Duration(sleepSec) * time.Second)
|
|
fasthttp.ReleaseRequest(req)
|
|
fasthttp.ReleaseResponse(resp)
|
|
}
|
|
|
|
time.Sleep(time.Duration(math.Pow(2, float64(retries))) * time.Second)
|
|
retries++
|
|
}
|
|
|
|
return nil, fmt.Errorf("max retries exceeded")
|
|
}
|
|
|
|
func (c *Client) Get(url string) (*fasthttp.Response, error) {
|
|
req := fasthttp.AcquireRequest()
|
|
resp := fasthttp.AcquireResponse()
|
|
req.SetRequestURI(url)
|
|
req.Header.SetMethod("GET")
|
|
req.Header.Set("Authorization", c.getAuthHeader())
|
|
|
|
err := c.client.Do(req, resp)
|
|
if err != nil {
|
|
fasthttp.ReleaseRequest(req)
|
|
fasthttp.ReleaseResponse(resp)
|
|
return nil, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (c *Client) Patch(url string, data map[string]interface{}) (*fasthttp.Response, error) {
|
|
req := fasthttp.AcquireRequest()
|
|
resp := fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
|
|
req.SetRequestURI(url)
|
|
req.Header.SetMethod("PATCH")
|
|
req.Header.Set("Authorization", c.getAuthHeader())
|
|
req.Header.SetContentType("application/json")
|
|
|
|
jsonData, _ := json.Marshal(data)
|
|
req.SetBody(jsonData)
|
|
|
|
err := c.client.Do(req, resp)
|
|
if err != nil {
|
|
fasthttp.ReleaseResponse(resp)
|
|
return nil, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func GetAlphas(c *Client, startDate, endDate string, sharpeTh, fitnessTh float64, region string, alphaNum int, usage string) ([][]interface{}, *Client, error) {
|
|
output := make([][]interface{}, 0)
|
|
count := 0
|
|
|
|
for i := 0; i < alphaNum; i += 100 {
|
|
fmt.Println(i)
|
|
|
|
urlE := fmt.Sprintf("%s/users/self/alphas?limit=100&offset=%d&status=UNSUBMITTED%%1FIS_FAIL&dateCreated%%3E=2025-%sT00:00:00-04:00&dateCreated%%3C2025-%sT00:00:00-04:00&is.fitness%%3E%f&is.sharpe%%3E%f&settings.region=%s&order=-is.sharpe&hidden=false&type!=SUPER",
|
|
baseURL, i, startDate, endDate, fitnessTh, sharpeTh, region)
|
|
|
|
urlC := fmt.Sprintf("%s/users/self/alphas?limit=100&offset=%d&status=UNSUBMITTED%%1FIS_FAIL&dateCreated%%3E=2025-%sT00:00:00-04:00&dateCreated%%3C2025-%sT00:00:00-04:00&is.fitness%%3C-%f&is.sharpe%%3C-%f&settings.region=%s&order=is.sharpe&hidden=false&type!=SUPER",
|
|
baseURL, i, startDate, endDate, fitnessTh, sharpeTh, region)
|
|
|
|
urls := []string{urlE}
|
|
if usage != "submit" {
|
|
urls = append(urls, urlC)
|
|
}
|
|
|
|
for _, url := range urls {
|
|
resp, err := c.Get(url)
|
|
if err != nil {
|
|
fmt.Printf("%d finished re-login\n", i)
|
|
c.Login()
|
|
continue
|
|
}
|
|
|
|
var alphaResp AlphaResponse
|
|
if err := json.Unmarshal(resp.Body(), &alphaResp); err != nil {
|
|
fasthttp.ReleaseResponse(resp)
|
|
fmt.Printf("%d finished re-login\n", i)
|
|
c.Login()
|
|
continue
|
|
}
|
|
fasthttp.ReleaseResponse(resp)
|
|
|
|
for _, item := range alphaResp.Results {
|
|
alphaID := item.ID
|
|
_ = item.Name // avoid unused variable
|
|
dateCreated := item.DateCreated
|
|
sharpe := getFloat(item.Is, "sharpe")
|
|
fitness := getFloat(item.Is, "fitness")
|
|
turnover := getFloat(item.Is, "turnover")
|
|
margin := getFloat(item.Is, "margin")
|
|
longCount := getFloat(item.Is, "longCount")
|
|
shortCount := getFloat(item.Is, "shortCount")
|
|
decay := item.Settings.Decay
|
|
exp := item.Regular.Code
|
|
|
|
count++
|
|
|
|
if (longCount + shortCount) > 100 {
|
|
if sharpe < -sharpeTh {
|
|
exp = "-" + exp
|
|
}
|
|
|
|
rec := []interface{}{alphaID, exp, sharpe, turnover, fitness, margin, dateCreated, decay}
|
|
fmt.Println(rec)
|
|
|
|
if turnover > 0.7 {
|
|
rec = append(rec, float64(decay)*4)
|
|
} else if turnover > 0.6 {
|
|
rec = append(rec, float64(decay)*3+3)
|
|
} else if turnover > 0.5 {
|
|
rec = append(rec, float64(decay)*3)
|
|
} else if turnover > 0.4 {
|
|
rec = append(rec, float64(decay)*2)
|
|
} else if turnover > 0.35 {
|
|
rec = append(rec, float64(decay)+4)
|
|
} else if turnover > 0.3 {
|
|
rec = append(rec, float64(decay)+2)
|
|
}
|
|
|
|
output = append(output, rec)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf("count: %d\n", count)
|
|
return output, c, nil
|
|
}
|
|
|
|
func getFloat(m map[string]interface{}, key string) float64 {
|
|
if v, ok := m[key]; ok {
|
|
switch val := v.(type) {
|
|
case float64:
|
|
return val
|
|
case int:
|
|
return float64(val)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func CheckConsecutiveNonZeroValues(alphaID string, data [][]interface{}, requiredStreak int) bool {
|
|
if len(data) < requiredStreak {
|
|
return true
|
|
}
|
|
|
|
checkColumn := func(columnData []float64) bool {
|
|
if len(columnData) < requiredStreak {
|
|
return true
|
|
}
|
|
|
|
currentStreakCount := 0
|
|
var currentStreakValue interface{}
|
|
|
|
for _, value := range columnData {
|
|
if value != 0 {
|
|
if currentStreakValue != nil && value == currentStreakValue {
|
|
currentStreakCount++
|
|
} else {
|
|
currentStreakValue = value
|
|
currentStreakCount = 1
|
|
}
|
|
} else {
|
|
currentStreakValue = nil
|
|
currentStreakCount = 0
|
|
}
|
|
|
|
if currentStreakCount >= requiredStreak {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
var column1Values, column2Values []float64
|
|
for _, row := range data {
|
|
if len(row) >= 3 {
|
|
if v, ok := row[1].(float64); ok {
|
|
column1Values = append(column1Values, v)
|
|
}
|
|
if v, ok := row[2].(float64); ok {
|
|
column2Values = append(column2Values, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(column1Values) > 0 && len(column2Values) > 0 {
|
|
isCol1AllZeros := allZeros(column1Values)
|
|
isCol2AllZeros := allZeros(column2Values)
|
|
if isCol1AllZeros || isCol2AllZeros {
|
|
fmt.Println(alphaID, "不合法")
|
|
return false
|
|
}
|
|
}
|
|
|
|
if !checkColumn(column1Values) {
|
|
fmt.Println(alphaID, "不合法")
|
|
return false
|
|
}
|
|
|
|
if !checkColumn(column2Values) {
|
|
fmt.Println(alphaID, "不合法")
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func allZeros(arr []float64) bool {
|
|
for _, v := range arr {
|
|
if v != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func GetAlphaPnlLegal(c *Client, alphaID string) bool {
|
|
notLegalID := make([]string, 0)
|
|
|
|
url := baseURL + "/alphas/" + alphaID + "/recordsets/pnl"
|
|
resp, err := c.WaitGet(url, 10)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
|
|
var pnlResp PnlResponse
|
|
if err := json.Unmarshal(resp.Body(), &pnlResp); err != nil {
|
|
return false
|
|
}
|
|
|
|
records := pnlResp.Records
|
|
if len(records) == 0 {
|
|
return false
|
|
}
|
|
|
|
var dateList []time.Time
|
|
for _, record := range records {
|
|
if len(record) == 0 {
|
|
continue
|
|
}
|
|
dateStr, ok := record[0].(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
dateObj, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
dateList = append(dateList, dateObj)
|
|
}
|
|
|
|
if len(dateList) == 0 {
|
|
return false
|
|
}
|
|
|
|
minDate := dateList[0]
|
|
maxDate := dateList[0]
|
|
for _, d := range dateList {
|
|
if d.Before(minDate) {
|
|
minDate = d
|
|
}
|
|
if d.After(maxDate) {
|
|
maxDate = d
|
|
}
|
|
}
|
|
|
|
totalDays := int(maxDate.Sub(minDate).Hours() / 24)
|
|
if totalDays < requiredDays {
|
|
return false
|
|
}
|
|
|
|
col1Zeros := make([]bool, 0)
|
|
for _, record := range records {
|
|
if len(record) >= 2 {
|
|
if v, ok := record[1].(float64); ok {
|
|
col1Zeros = append(col1Zeros, v == 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
col1MaxZeroStreak := maxConsecutiveZeros(col1Zeros)
|
|
|
|
if col1MaxZeroStreak >= zeroStreakThreshold {
|
|
fmt.Printf("%s 不合法:存在连续%d年零值\n", alphaID, zeroStreakThreshold/252)
|
|
notLegalID = append(notLegalID, alphaID)
|
|
return false
|
|
}
|
|
|
|
if !CheckConsecutiveNonZeroValues(alphaID, records, 200) {
|
|
return false
|
|
}
|
|
|
|
_ = notLegalID
|
|
return true
|
|
}
|
|
|
|
func maxConsecutiveZeros(arr []bool) int {
|
|
maxStreak := 0
|
|
currentStreak := 0
|
|
for _, val := range arr {
|
|
if val {
|
|
currentStreak++
|
|
if currentStreak > maxStreak {
|
|
maxStreak = currentStreak
|
|
}
|
|
} else {
|
|
currentStreak = 0
|
|
}
|
|
}
|
|
return maxStreak
|
|
}
|
|
|
|
func Mute(c *Client, alphaID string) {
|
|
url := baseURL + "/alphas/" + alphaID
|
|
data := map[string]interface{}{
|
|
"hidden": true,
|
|
}
|
|
c.Patch(url, data)
|
|
}
|
|
|
|
func main() {
|
|
client := NewClient("", "")
|
|
client.Login()
|
|
|
|
foTracker, c, err := GetAlphas(client, "12-01", "12-31", 1, 0.5, "USA", 1000, "submit")
|
|
if err != nil {
|
|
fmt.Println("Error:", err)
|
|
return
|
|
}
|
|
|
|
fNum := len(foTracker)
|
|
fmt.Printf("%d 个alpha 进行pnl合法检测,请耐心等待\n", fNum)
|
|
fmt.Println(len(foTracker))
|
|
|
|
count := 0
|
|
for i := len(foTracker) - 1; i >= 0; i-- {
|
|
if count%25 == 0 {
|
|
fmt.Printf("=========== %d ===========\n", count)
|
|
}
|
|
count++
|
|
|
|
alphaID, ok := foTracker[i][0].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if !GetAlphaPnlLegal(c, alphaID) {
|
|
fmt.Println(alphaID, "已经隐藏")
|
|
Mute(c, alphaID)
|
|
}
|
|
}
|
|
}
|
|
|