v0.4.2: perf - Sina batch quotes (1 request covers indices/commodities/US stocks), Yahoo circuit breaker (2 fails = skip 10min)

This commit is contained in:
2026-10-28 14:23:27 +08:00
parent 56c73a73d6
commit 5455251673
2 changed files with 91 additions and 5 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dsh.openterm"
android:versionCode="5"
android:versionName="0.4.1">
android:versionCode="6"
android:versionName="0.4.2">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="28" />
<uses-permission android:name="android.permission.INTERNET" />
+89 -3
View File
@@ -59,7 +59,7 @@ public class MainActivity extends Activity implements View.OnClickListener {
double priceNum = Double.NaN, prevNum = Double.NaN;
String highS = "--", lowS = "--", volS = "--", hi52S = "--", lo52S = "--";
String currency = "", exchange = "";
boolean up, loaded;
boolean up, loaded, batchHit;
Quote(String yahoo, String tencent, String name) {
this.yahoo = yahoo; this.tencent = tencent; this.name = name;
this.sym = yahoo;
@@ -706,13 +706,17 @@ public class MainActivity extends Activity implements View.OnClickListener {
void fetchList(List<Quote> list, final boolean mon) {
int ok = 0;
// 国内快路径:Yahoo 已熔断时先走新浪批量(1 次请求覆盖指数/商品/美股)
int batched = 0;
if (System.currentTimeMillis() < yahooDeadUntil) batched = fetchSinaBatch(list);
for (Quote q : list) {
if (q.batchHit) { ok++; q.batchHit = false; continue; }
if (fetchYahoo(q)) ok++;
else if (fetchEastMoney(q)) ok++;
else if (q.tencent != null && fetchTencent(q)) ok++;
else if (fetchSinaHf(q)) ok++;
else if (fetchStooq(q)) ok++;
try { Thread.sleep(120); } catch (InterruptedException e) { return; }
if (batched == 0) try { Thread.sleep(120); } catch (InterruptedException e) { return; }
}
final int n = ok;
ui.post(new Runnable() { @Override public void run() {
@@ -733,6 +737,8 @@ public class MainActivity extends Activity implements View.OnClickListener {
}
boolean fetchYahoo(Quote q) {
long now = System.currentTimeMillis();
if (now < yahooDeadUntil) return false; // 熔断中,直接跳过
try {
JSONObject meta = chartJson(q.yahoo, "1d", "5m").getJSONObject("chart")
.getJSONArray("result").getJSONObject(0).getJSONObject("meta");
@@ -740,13 +746,93 @@ public class MainActivity extends Activity implements View.OnClickListener {
double prev = meta.optDouble("chartPreviousClose",
meta.optDouble("previousClose", Double.NaN));
if (Double.isNaN(price) || price == 0) return false;
yahooFailStreak = 0;
applyQuote(q, price, prev,
meta.optString("currency", ""), meta.optString("exchangeName", ""),
meta.optDouble("regularMarketDayHigh", Double.NaN),
meta.optDouble("regularMarketDayLow", Double.NaN),
meta.optDouble("regularMarketVolume", Double.NaN));
return true;
} catch (Exception e) { return false; }
} catch (Exception e) {
// 连续失败 2 次熔断 10 分钟(国内网络 Yahoo 不可达时省去每标的 ~1s 的等待)
if (++yahooFailStreak >= 2) yahooDeadUntil = System.currentTimeMillis() + 10 * 60 * 1000L;
return false;
}
}
int yahooFailStreak = 0;
long yahooDeadUntil = 0;
/* ── 新浪批量:一次请求覆盖指数/商品/美股 ── */
String sinaCodeFor(Quote q) {
String y = q.yahoo;
if (y.equals("^GSPC")) return "int_sp500";
if (y.equals("^IXIC")) return "int_nasdaq";
if (y.equals("^DJI")) return "int_dji";
if (y.equals("^HSI")) return "int_hangseng";
if (y.equals("^N225")) return "int_nikkei";
if (y.equals("^FTSE")) return "int_ftse";
String hf = sinaHfFor(y);
if (!hf.isEmpty()) return hf;
if (y.matches("[A-Z]{1,5}")) return "gb_" + y.toLowerCase(Locale.US);
return null;
}
int fetchSinaBatch(List<Quote> list) {
StringBuilder codes = new StringBuilder();
List<Quote> mappable = new ArrayList<>();
for (Quote q : list) {
String c = sinaCodeFor(q);
if (c != null) { codes.append(c).append(','); mappable.add(q); }
}
if (mappable.isEmpty()) return 0;
codes.deleteCharAt(codes.length() - 1);
try {
String body = httpGet("https://hq.sinajs.cn/list=" + codes,
"https://finance.sina.com.cn");
int ok = 0;
for (Quote q : mappable) {
String tag = "hq_str_" + sinaCodeFor(q) + "=\"";
int s = body.indexOf(tag);
if (s < 0) continue;
int e = body.indexOf('"', s + tag.length());
if (e < 0) continue;
String[] f = body.substring(s + tag.length(), e).split(",");
boolean done = false;
String code = sinaCodeFor(q);
if (code.startsWith("int_")) { // 指数: 名,价,涨跌,幅度
if (f.length >= 4) {
double price = Double.parseDouble(f[1]);
double chg = Double.parseDouble(f[2]);
applyQuote(q, price, price - chg, "", "SINA",
Double.NaN, Double.NaN, Double.NaN);
done = true;
}
} else if (code.startsWith("hf_")) { // 商品
if (f.length >= 9) {
double price = Double.parseDouble(f[0]);
double prev = Double.parseDouble(f[7]);
if (price != 0) {
applyQuote(q, price, prev, "USD", "SINA",
Double.parseDouble(f[5]), Double.parseDouble(f[6]), Double.NaN);
done = true;
}
}
} else if (code.startsWith("gb_")) { // 美股: 名,价,幅度,时间,涨跌,开,高,低
if (f.length >= 8) {
double price = Double.parseDouble(f[1]);
if (price != 0) {
double chg = Double.parseDouble(f[4]);
applyQuote(q, price, price - chg, "USD", "SINA",
Double.parseDouble(f[6]), Double.parseDouble(f[7]), Double.NaN);
done = true;
}
}
}
if (done) { ok++; q.batchHit = true; }
}
return ok;
} catch (Exception e) { return 0; }
}
JSONObject chartJson(String sym, String range, String interval) throws Exception {