@cxxgo/fund-valuation-query 1.0.6 → 1.0.7
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.
- package/README.md +7 -7
- package/dist/fund.js +20 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
### 明细
|
|
20
20
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
21
|
+
- `收盘`:每只持仓股票“最近一个已收盘交易日”的日涨跌幅。展示规则:如果当前市场已经收盘,则展示最新收盘日;如果当前仍在交易中,则展示今天之前最近一个已收盘交易日。拿不到历史日线就直接显示 `-`。
|
|
22
|
+
- `盘中`:表示持仓股票当前交易日的实时涨跌幅。只要行情数据日期仍是对应市场的当天就展示;但如果当日收盘涨跌已经可用,为避免和 `收盘` 重复,则这里显示 `-`。
|
|
23
|
+
- `权重估值`:按“当前拿到盘中数据的持仓”做持仓占比加权汇总。
|
|
24
|
+
- `覆盖`:当前拿到有效盘中数据的持仓权重占比。只有覆盖率达到 `60%` 时才展示 `权重估值`,否则显示 `-`。
|
|
25
25
|
- `明细刷新`:只刷新当前基金持仓股的盘中价格,不刷新持仓结构;持仓结构按天缓存。
|
|
26
26
|
|
|
27
27
|
## 安装
|
|
@@ -46,7 +46,7 @@ fund list
|
|
|
46
46
|
fund detail <基金代码>
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
展示该基金最新报告期持仓股票及各自最近收盘、盘中涨跌。
|
|
50
50
|
|
|
51
51
|
### Web 服务
|
|
52
52
|
|
|
@@ -64,9 +64,9 @@ PORT=8080 fund serve
|
|
|
64
64
|
启动后访问 `http://localhost:8888` 即可使用可视化界面,功能包括:
|
|
65
65
|
|
|
66
66
|
- 首次访问需选择配置文件(上传 JSON 文件或粘贴内容),配置保存在浏览器 localStorage
|
|
67
|
-
-
|
|
67
|
+
- 点击表头按基金代码、名称、上个交易日、估值排序
|
|
68
68
|
- 点击基金名称,右侧抽屉展开持仓明细
|
|
69
|
-
-
|
|
69
|
+
- 抽屉内支持按占比、盘中/收盘排序
|
|
70
70
|
- 手动刷新按钮 + 30 秒自动刷新
|
|
71
71
|
- 切换配置按钮可重新选择配置文件
|
|
72
72
|
|
package/dist/fund.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
3
|
-
`)){let u=c.match(/^v_([^=]+)=/);if(!u)continue;let f
|
|
4
|
-
`);for(let g of f){let p=g.match(/^v_([^=]+)="(.+)";?$/);if(!p)continue;let l=p[1],m=p[2].split("~");m.length<33||u.set(l,{name:m[1],changePercent:parseFloat(m[32]),quoteDate:F(m[30])})}return u},{force:t});for(let[d,s]of o.entries())i.has(d)&&a.set(s,i.get(d))}catch{}return a}import{readFileSync as H,writeFileSync as
|
|
5
|
-
${M.gray(t)}`}function
|
|
6
|
-
${t}`:e}function
|
|
7
|
-
${M.bold(t)} ${M.bold(a)}`),console.log(d.toString()),console.log(`${
|
|
8
|
-
`)}function
|
|
2
|
+
import{Command as it}from"commander";import{readFileSync as st}from"fs";import y from"axios";import*as Z from"cheerio";function X(e){let t=Z.load(e),a=[];return t("table").first().find("tbody tr, tr").each((o,r)=>{let i=t(r).find("td");if(i.length<7)return;let d=i.eq(1).find("a"),s=d.text().trim();if(!s)return;let u=(d.attr("href")||"").match(/\/r\/(\d+)\.(\w+)/),f=u?parseInt(u[1]):s.startsWith("6")?1:0,g=f>=100,p=i.eq(2).find("a").text().trim()||i.eq(2).text().trim(),l=i.eq(6).text().trim().replace("%",""),m=parseFloat(l);isNaN(m)||a.push({stockCode:s,stockName:p,holdingRatio:m,market:f,isOverseas:g})}),a}var I={"User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Referer:"https://fund.eastmoney.com/"},N=1e3,Ce=60*N,R=60*Ce,B=24*R,T=new Map;async function v(e,t,a,{force:n=!1}={}){let o=Date.now(),r=T.get(e);if(!n&&r?.value!==void 0&&r.expiresAt>o)return r.value;if(r?.promise)return r.promise;let i=(async()=>{try{let d=await a();return T.set(e,{value:d,expiresAt:o+t,promise:null}),d}catch(d){if(r?.value!==void 0&&!n)return T.set(e,r),r.value;throw T.delete(e),d}})();return T.set(e,{value:r?.value,expiresAt:r?.expiresAt||0,promise:i}),i}async function P(e){return v(`fund-holdings:${e}`,B,async()=>{let t=`https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${e}&topline=50`,{data:a}=await y.get(t,{responseType:"text",headers:I,timeout:8e3}),n=a.match(/content:"([\s\S]*?)",arryear:/);return n?X(n[1]):[]})}async function j(e,{force:t=!1}={}){try{return await v(`fund-estimate:${e}`,30*N,async()=>{let a=`http://fundgz.1234567.com.cn/js/${e}.js`,{data:n}=await y.get(a,{responseType:"text",timeout:5e3}),o=n.match(/jsonpgz\((.+)\)/);if(!o)return null;let r=JSON.parse(o[1]);return r.gszzl==null||!r.gztime?null:{change:parseFloat(r.gszzl),time:r.gztime,navDate:F(r.jzrq)}},{force:t})}catch{return null}}function te(e){let t=new Intl.DateTimeFormat("en-CA",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(new Date(e)),a=n=>t.find(o=>o.type===n)?.value||"";return`${a("year")}-${a("month")}-${a("day")}`}function ee(e,t="Asia/Shanghai"){let a=new Intl.DateTimeFormat("en-CA",{timeZone:t,year:"numeric",month:"2-digit",day:"2-digit"}).formatToParts(e),n=o=>a.find(r=>r.type===o)?.value||"";return`${n("year")}-${n("month")}-${n("day")}`}async function Te(e){return v(`stock-history:nasdaq:${e}`,12*R,async()=>{let t=new Date,a=new Date(t.getTime()-30*B),n=`https://api.nasdaq.com/api/quote/${encodeURIComponent(e)}/historical?assetclass=stocks&fromdate=${ee(a)}&limit=12&todate=${ee(t)}`,o=null;for(let s=0;s<3;s+=1)try{({data:o}=await y.get(n,{timeout:8e3,headers:{"User-Agent":I["User-Agent"],Accept:"application/json, text/plain, */*",Origin:"https://www.nasdaq.com",Referer:"https://www.nasdaq.com/"}}));break}catch(c){if(c?.response?.status!==429||s===2)throw c;await ae(500*(s+1))}let r=o?.data?.tradesTable?.rows;if(!Array.isArray(r)||r.length<2)return{latestDate:null,changesByDate:new Map};let i=r.map(s=>({date:Ne(s?.date),close:Se(s?.close)})).filter(s=>s.date&&Number.isFinite(s.close)).reverse();if(i.length<2)return{latestDate:null,changesByDate:new Map};let d=new Map;for(let s=1;s<i.length;s+=1){let c=i[s-1].close,u=i[s];c!==0&&d.set(u.date,(u.close-c)/c*100)}return{latestDate:i[i.length-1]?.date||null,changesByDate:d}})}function Ne(e){if(!e)return null;let[t,a,n]=String(e).split("/");return!n||!t||!a?null:`${n}-${t.padStart(2,"0")}-${a.padStart(2,"0")}`}function Se(e){return e==null?Number.NaN:Number.parseFloat(String(e).replace(/[$,]/g,""))}function ae(e){return new Promise(t=>setTimeout(t,e))}async function L(e,{force:t=!1}={}){try{return await v(`fund-networth:${e}`,B,async()=>{let a=`https://fund.eastmoney.com/pingzhongdata/${e}.js?v=${Date.now()}`,{data:n}=await y.get(a,{responseType:"text",timeout:8e3,headers:I}),o=n.match(/var Data_netWorthTrend = (\[[\s\S]*?\]);\/\*/);if(!o)return null;let r=JSON.parse(o[1]),i=r[r.length-1];return!i||typeof i.x!="number"||typeof i.equityReturn!="number"?null:{date:te(i.x),change:i.equityReturn}},{force:t})}catch{return null}}function Me({code:e,market:t}){return/^(8|9)\d{5}$/.test(e)?`bj${e}`:t===1?`sh${e}`:t===0?`sz${e}`:t===116?`r_hk${e}`:t>=100?`us${e}`:null}function Le({code:e,market:t}){return/^(8|9)\d{5}$/.test(e)?`bj${e}`:t===1?`sh${e}`:t===0?`sz${e}`:t===116?`hk${e}`:t>=100?`us${e}`:null}function F(e){if(!e)return null;if(/^\d{14}$/.test(e))return`${e.slice(0,4)}-${e.slice(4,6)}-${e.slice(6,8)}`;let t=e.replace(/\//g,"-");return/^\d{4}-\d{2}-\d{2}/.test(t)?t.slice(0,10):null}async function O(){try{return await v("china-market-date",30*N,async()=>{let{data:e}=await y.get("https://qt.gtimg.cn/q=sh000001",{responseType:"arraybuffer",timeout:5e3}),a=new TextDecoder("gbk").decode(e).match(/^v_[^=]+="(.+)";?$/m);if(!a)return null;let n=a[1].split("~");return F(n[30])})}catch{return null}}var Fe=[{code:"sh000001",name:"\u4E0A\u8BC1\u6307\u6570"},{code:"sz399001",name:"\u6DF1\u8BC1\u6210\u6307"},{code:"sz399006",name:"\u521B\u4E1A\u677F\u6307"},{code:"bj899050",name:"\u5317\u8BC150"},{code:"sh000688",name:"\u79D1\u521B50"},{code:"sh000016",name:"\u4E0A\u8BC150"},{code:"sz399330",name:"\u6DF1\u8BC1100"},{code:"sh000300",name:"\u6CAA\u6DF1300"},{code:"sh000905",name:"\u4E2D\u8BC1500"},{code:"sh000852",name:"\u4E2D\u8BC11000"},{code:"sh000012",name:"\u56FD\u503A\u6307\u6570"},{code:"sh000013",name:"\u4F01\u503A\u6307\u6570"}];function Oe(){return Fe.map(e=>({...e}))}function $e(e){let t=e.match(/^v_[^=]+="(.+)";?$/m);return t?t[1].split("~"):null}function ne(){let e=new Intl.DateTimeFormat("en-GB",{timeZone:"Asia/Shanghai",hour:"2-digit",minute:"2-digit",hour12:!1}).formatToParts(new Date),t=a=>e.find(n=>n.type===a)?.value||"";return`${t("hour")}:${t("minute")}`}function Ee(e){let t=te(Date.now());if(!e||e!==t)return"closed";let a=ne(),n=Number.parseInt(a.slice(0,2),10)*60+Number.parseInt(a.slice(3,5),10),o=n>=570&&n<=690,r=n>=780&&n<=900;return n>690&&n<780?"midday_break":o||r?"trading":"closed"}async function re({force:e=!1}={}){let t=Oe();try{return await v("market-overview",30*N,async()=>{let a=`https://qt.gtimg.cn/q=${t.map(c=>c.code).join(",")}`,{data:n}=await y.get(a,{responseType:"arraybuffer",timeout:5e3}),o=new TextDecoder("gbk").decode(n),r=new Map;for(let c of o.trim().split(`
|
|
3
|
+
`)){let u=c.match(/^v_([^=]+)=/);if(!u)continue;let f=$e(c);!f||f.length<33||r.set(u[1],f)}let i=r.get(t[0].code),d=F(i?.[30])||null,s=Ee(d);return{status:s,dateLabel:d?d.slice(5):"",timeLabel:s==="trading"?ne():"",items:t.map(c=>{let u=r.get(c.code),f=Number.parseFloat(u?.[3]),g=Number.parseFloat(u?.[4]),p=Number.parseFloat(u?.[32]);return{code:c.code,name:c.name,value:Number.isFinite(f)?f:null,change:Number.isFinite(f)&&Number.isFinite(g)?f-g:null,changePercent:Number.isFinite(p)?p:null}})}},{force:e})}catch{return{status:"closed",dateLabel:"",timeLabel:"",items:t.map(a=>({...a,value:null,change:null,changePercent:null}))}}}async function z(e){let t=new Map;if(e.length===0)return t;await Promise.all(e.map(async({code:n,market:o})=>{if(o>=100)return;let r=Le({code:n,market:o});if(!r){t.set(n,{latestDate:null,changesByDate:new Map});return}try{let i=await v(`stock-history:${r}`,12*R,async()=>{let d=`https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${r},day,,,30,qfq`,{data:s}=await y.get(d,{timeout:5e3,headers:{Referer:"https://gu.qq.com/"}}),c=s?.data?.[r],u=c?.qfqday||c?.day;if(!Array.isArray(u)||u.length===0)return{latestDate:null,changesByDate:new Map};let f=new Map;for(let g=1;g<u.length;g+=1){let p=u[g-1],l=u[g],m=l?.[0],x=parseFloat(p?.[2]),k=parseFloat(l?.[2]);if(!m||Number.isNaN(x)||Number.isNaN(k)||x===0)continue;let D=(k-x)/x*100;f.set(m,D)}return{latestDate:u[u.length-1]?.[0]||null,changesByDate:f}});t.set(n,i)}catch{t.set(n,{latestDate:null,changesByDate:new Map})}}));let a=0;for(let{code:n,market:o}of e)if(!(o<100)){try{a>0&&await ae(250);let r=await Te(n);t.set(n,r)}catch{t.set(n,{latestDate:null,changesByDate:new Map})}a+=1}return t}async function oe(e,{force:t=!1}={}){let a=new Map;if(e.length===0)return a;let n=e.map(({code:r,market:i})=>({code:r,symbol:Me({code:r,market:i})})).filter(r=>r.symbol);if(n.length===0)return a;let o=new Map(n.map(r=>[r.symbol,r.code]));try{let r=n.map(d=>d.symbol).sort().join(","),i=await v(`stock-quotes:${r}`,20*N,async()=>{let d=`https://qt.gtimg.cn/q=${n.map(g=>g.symbol).join(",")}`,{data:s}=await y.get(d,{responseType:"arraybuffer",timeout:5e3}),c=new TextDecoder("gbk").decode(s),u=new Map,f=c.trim().split(`
|
|
4
|
+
`);for(let g of f){let p=g.match(/^v_([^=]+)="(.+)";?$/);if(!p)continue;let l=p[1],m=p[2].split("~");m.length<33||u.set(l,{name:m[1],changePercent:parseFloat(m[32]),quoteDate:F(m[30])})}return u},{force:t});for(let[d,s]of o.entries())i.has(d)&&a.set(s,i.get(d))}catch{}return a}import{readFileSync as H,writeFileSync as Ie,existsSync as q}from"fs";import{homedir as Re}from"os";import{join as Be,resolve as Pe}from"path";var A=Be(Re(),".fundrc.json");function K(){return q(A)&&JSON.parse(H(A,"utf-8")).configPath||null}function ie(e){let t=Pe(e);q(t)||(console.error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${t}`),process.exit(1));try{let a=JSON.parse(H(t,"utf-8"));(!a||typeof a!="object"||Object.keys(a).length===0)&&(console.error("\u914D\u7F6E\u6587\u4EF6\u4E3A\u7A7A\u6216\u683C\u5F0F\u9519\u8BEF"),process.exit(1))}catch{console.error("\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF\uFF0C\u9700\u8981\u5408\u6CD5\u7684 JSON"),process.exit(1)}Ie(A,JSON.stringify({configPath:t},null,2)),console.log(`\u5DF2\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6: ${t}`)}function U(){let e=K();return e||(console.error("\u672A\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u8FD0\u884C: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>"),process.exit(1)),q(e)||(console.error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${e}\uFF0C\u8BF7\u91CD\u65B0\u8BBE\u7F6E: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>`),process.exit(1)),JSON.parse(H(e,"utf-8"))}function se(e,t){return t.some(n=>n.isOverseas)||/全球|QDII|美元|港|海外|纳斯达克|标普|道琼斯/.test(e)?"\u5168\u7403":/ETF|指数/.test(e)?"\u6307\u6570":/债/.test(e)?"\u504F\u503A":"\u504F\u80A1"}var S="Asia/Shanghai";function je(e=S,t=new Date){return new Date(t.toLocaleString("en-US",{timeZone:e}))}function ze(e){return[e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0")].join("-")}function Ae(){return ze(je())}function h(e,t=new Date){let a=new Intl.DateTimeFormat("en-CA",{timeZone:e,year:"numeric",month:"2-digit",day:"2-digit",weekday:"short",hour:"2-digit",minute:"2-digit",hour12:!1}).formatToParts(t),n=o=>a.find(r=>r.type===o)?.value||"";return{date:`${n("year")}-${n("month")}-${n("day")}`,weekday:n("weekday"),minutes:Number.parseInt(n("hour"),10)*60+Number.parseInt(n("minute"),10)}}function _(e){return["Mon","Tue","Wed","Thu","Fri"].includes(e)}function He(e=new Date){let t=h(S,e);return _(t.weekday)?t.minutes>900||t.minutes<570:!0}function qe(e=new Date){let t=h("Asia/Hong_Kong",e);return _(t.weekday)?t.minutes>960||t.minutes<570:!0}function Ke(e=new Date){let t=h("America/New_York",e);return _(t.weekday)?t.minutes>960||t.minutes<570:!0}function Ue(e,t,a=new Date){return!t?.quoteDate||!w(t.changePercent)?!1:e.market===116?t.quoteDate===h("Asia/Hong_Kong",a).date:e.market>=100?t.quoteDate===h("America/New_York",a).date:t.quoteDate===h(S,a).date}function _e(e,t){return!!(e&&t&&e===t)}function de(e,t=new Date){return e.market===116?h("Asia/Hong_Kong",t).date:e.market>=100?h("America/New_York",t).date:h(S,t).date}function Je(e,t=new Date){return e.market===116?qe(t):e.market>=100?Ke(t):He(t)}function We(e,t,a=new Date){let n=t.get(e.stockCode);if(!n)return null;let o=[...n.changesByDate.keys()].sort();if(o.length===0)return null;let r=de(e,a);if(Je(e,a))return o[o.length-1]||null;let d=o.filter(s=>s<r);return d[d.length-1]||null}function ce(e,t,a=new Date){if(!e?.time||!w(e.change))return!1;let n=e.time.slice(0,10);if(t==="\u5168\u7403"){let r=h("America/New_York",a),i=h("Asia/Hong_Kong",a);return n===r.date||n===i.date}let o=h(S,a).date;return n===o}function $(e){return!e||e.length<10?"":e.slice(5)}function w(e){return typeof e=="number"&&!Number.isNaN(e)}function J(e){return e.map(t=>({code:t.stockCode,market:t.market}))}function W(e,t,a,n=null){let o=Ae(),r=new Date,i=0,d=e.map(l=>{let m=t.get(l.stockCode),x=a.get(l.stockCode),k=We(l,a,r),D=Ue(l,m,r)&&(l.isOverseas||_e(o,n))?m.changePercent:null,G=k,E=k&&x?x.changesByDate.get(k)??null:null,De=G===de(l,r)?null:D,ke=w(D)?l.holdingRatio*D/100:0;return w(D)&&(i+=l.holdingRatio),{stockCode:l.stockCode,stockName:l.stockName,holdingRatio:l.holdingRatio,todayChange:D,intradayDisplayChange:De,previousTradingDayChange:E,previousTradingDayDate:G,contribution:w(E)?l.holdingRatio*E/100:0,weightedTodayContribution:ke,isOverseas:l.isOverseas}}),c=d.length>0&&d.every(l=>w(l.previousTradingDayChange))?d.reduce((l,m)=>l+m.contribution,0):null,u=i>0?i/100:0,f=u>=.6?d.reduce((l,m)=>l+m.weightedTodayContribution,0):null,g=d.map(l=>l.previousTradingDayDate).filter(Boolean).sort(),p=g.length>0?g[g.length-1]:null;return{previousTradingDayChange:c,weightedTodayChange:f,coverageRatio:u,contributions:d,todayDate:o,previousTradingDate:p}}async function Q(e){let t=new Map;return await Promise.all(Object.keys(e).map(async a=>{try{t.set(a,await P(a))}catch{t.set(a,[])}})),t}function Qe(e){let t=new Map;for(let[,a]of e)for(let n of a)t.has(n.stockCode)||t.set(n.stockCode,{code:n.stockCode,market:n.market});return[...t.values()]}async function Y(e,t){return oe(Qe(e),t)}function le(e,t,a,n,o){let r=se(t,a);return{code:e,name:t,todayChange:ce(n,r)?n.change:null,previousTradingDayChange:o?.change??null,previousTradingDayLabel:$(o?.date??null),category:r,canDetail:!0}}async function ue(e,t,a,n,o=null){let[r,i,d]=await Promise.all([z(J(a)),j(e),L(e)]),s=W(a,n,r,o);return{...le(e,t,a,i,d),contributions:s.contributions,weightedTodayChange:s.weightedTodayChange,coverageRatio:s.coverageRatio,todayLabel:$(s.todayDate)}}async function V(e,{forceRefresh:t=!1}={}){let a=await Q(e);return(await Promise.all(Object.entries(e).map(async([o,r])=>{let i=a.get(o)||[],[d,s]=await Promise.all([j(o,{force:t}),L(o,{force:t})]);return{code:o,name:r,holdings:i,estimate:d,latestNetWorth:s}}))).map(({code:o,name:r,holdings:i,estimate:d,latestNetWorth:s})=>le(o,r,i,d,s))}async function fe(e){let[t,a]=await Promise.all([V(e,{forceRefresh:!0}),re({force:!0})]);return{funds:t,marketOverview:a}}async function ge(e,t,{forceRefreshPrices:a=!1}={}){let n=await P(e),o=new Map([[e,n]]),[r,i,d,s]=await Promise.all([Y(o,{force:a}),O(),z(J(n)),L(e)]),c=W(n,r,d,i);return{code:e,name:t,previousTradingDayChange:s?.change??null,previousTradingDayLabel:$(s?.date??null),weightedTodayChange:w(c.weightedTodayChange)?c.weightedTodayChange:null,coverageRatio:c.coverageRatio,contributions:c.contributions}}import M from"chalk";import me from"cli-table3";function C(e){if(e==null||Number.isNaN(e))return"-";let t=parseFloat(e);return t>0?M.red(`+${t.toFixed(2)}%`):t<0?M.green(`${t.toFixed(2)}%`):`${t.toFixed(2)}%`}function pe(e,t,a){return!t||t===a?C(e):`${C(e)}
|
|
5
|
+
${M.gray(t)}`}function Ye(e,t){return t?`${e} (${t})`:e}function he(e,t){let a="";for(let n of e){let o=t(n);o&&o>a&&(a=o)}return a}function be(e,t){return t?`${e}
|
|
6
|
+
${t}`:e}function ye(e){let{code:t,name:a,todayChange:n,previousTradingDayChange:o,contributions:r}=e,i=he(r,s=>s.previousTradingDayDate?.slice(5)||""),d=new me({head:["\u6301\u4ED3","\u5360\u6BD4",be("\u6536\u76D8",i),"\u76D8\u4E2D"],style:{head:["cyan"]}});for(let s of r)d.push([`${s.stockCode} ${s.stockName}`,`${s.holdingRatio.toFixed(2)}%`,pe(s.previousTradingDayChange,s.previousTradingDayDate?.slice(5),i),C(s.intradayDisplayChange)]);console.log(`
|
|
7
|
+
${M.bold(t)} ${M.bold(a)}`),console.log(d.toString()),console.log(`${Ye("\u51C0\u503C",e.previousTradingDayLabel)}: ${C(o)}`),console.log(`\u6DA8\u8DCC: ${C(n)}
|
|
8
|
+
`)}function ve(e){let t=he(e,n=>n.previousTradingDayLabel||""),a=new me({head:["\u57FA\u91D1\u4EE3\u7801","\u57FA\u91D1\u540D\u79F0",be("\u51C0\u503C",t),"\u4F30\u503C"],style:{head:["cyan"]}});for(let n of e)a.push([n.code,n.name,pe(n.previousTradingDayChange,n.previousTradingDayLabel,t),C(n.todayChange)]);console.log(a.toString())}import we from"express";import{spawn as Ve}from"child_process";import{networkInterfaces as Ge}from"os";async function Ze(e){return fe(e)}async function Xe(e,t,a=!1){return ge(e,t,{forceRefreshPrices:a})}function et(e){let t=null,a=[];process.platform==="darwin"?(t="open",a=[e]):process.platform==="win32"?(t="cmd",a=["/c","start","",e]):(t="xdg-open",a=[e]);try{Ve(t,a,{detached:!0,stdio:"ignore"}).unref()}catch{}}function tt(e){let t=Ge(),a=["en0","en1","Ethernet","Wi-Fi","wlan0","eth0"],n=[];for(let[o,r]of Object.entries(t))for(let i of r||[])i.family!=="IPv4"||i.internal||n.push({name:o,address:i.address});return n.length===0?null:(n.sort((o,r)=>{let i=a.indexOf(o.name),d=a.indexOf(r.name),s=i===-1?a.length:i,c=d===-1?a.length:d;return s!==c?s-c:o.name.localeCompare(r.name)}),`http://${n[0].address}:${e}`)}function at(){return`
|
|
9
9
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
10
|
body { background: #f5f0eb; color: #4a4a4a; font-family: -apple-system, "Microsoft YaHei", sans-serif; padding: 24px; }
|
|
11
11
|
h1 { text-align: center; margin-bottom: 8px; font-size: 24px; color: #5a5a5a; }
|
|
@@ -186,7 +186,7 @@ ${M.bold(t)} ${M.bold(a)}`),console.log(d.toString()),console.log(`${Qe("\u51C0\
|
|
|
186
186
|
.market-card { min-height: 72px; padding: 9px 9px; }
|
|
187
187
|
.drawer { width: min(460px, 100vw); }
|
|
188
188
|
}
|
|
189
|
-
`}function
|
|
189
|
+
`}function nt(){return`
|
|
190
190
|
<h1>\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2</h1>
|
|
191
191
|
<div class="update-time" id="updateTime"><span id="timeText"></span><button class="refresh-btn" id="refreshBtn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg></button></div>
|
|
192
192
|
<section class="market-overview hidden" id="marketOverview">
|
|
@@ -228,7 +228,7 @@ ${M.bold(t)} ${M.bold(a)}`),console.log(d.toString()),console.log(`${Qe("\u51C0\
|
|
|
228
228
|
<div class="error-msg" id="errorMsg"></div>
|
|
229
229
|
</div>
|
|
230
230
|
</div>
|
|
231
|
-
`}function
|
|
231
|
+
`}function rt(){return`
|
|
232
232
|
// \u4FDD\u5B58\u5F53\u524D\u9875\u9762\u7684\u6392\u5E8F\u3001\u7B5B\u9009\u3001\u4E3B\u8868\u6570\u636E\u548C\u62BD\u5C49\u72B6\u6001\u3002
|
|
233
233
|
const state = {
|
|
234
234
|
sortKey: 'todayChange',
|
|
@@ -494,7 +494,7 @@ function getSortedDrawerContributions(fund) {
|
|
|
494
494
|
if (state.drawerSortKey) {
|
|
495
495
|
sorted.sort((a, b) => {
|
|
496
496
|
if (state.drawerSortKey === 'holdingRatio') return state.drawerSortDir === 'asc' ? a.holdingRatio - b.holdingRatio : b.holdingRatio - a.holdingRatio;
|
|
497
|
-
if (state.drawerSortKey === 'todayChange') return compareNullableNumbers(a.
|
|
497
|
+
if (state.drawerSortKey === 'todayChange') return compareNullableNumbers(a.intradayDisplayChange, b.intradayDisplayChange, state.drawerSortDir);
|
|
498
498
|
if (state.drawerSortKey === 'previousTradingDayChange') return compareNullableNumbers(a.previousTradingDayChange, b.previousTradingDayChange, state.drawerSortDir);
|
|
499
499
|
return 0;
|
|
500
500
|
});
|
|
@@ -518,14 +518,14 @@ function buildDrawerTable(fund, sorted) {
|
|
|
518
518
|
let html = '<table><thead><tr>' +
|
|
519
519
|
'<th>\u6301\u4ED3</th>' +
|
|
520
520
|
buildSortableTh('\u5360\u6BD4', 'holdingRatio', state.drawerSortKey, state.drawerSortDir, 'data-dkey="holdingRatio"') +
|
|
521
|
-
buildSortableTh(fmtHeaderLabel('\u6536\u76D8
|
|
522
|
-
buildSortableTh('\
|
|
521
|
+
buildSortableTh(fmtHeaderLabel('\u6536\u76D8', headerDate), 'previousTradingDayChange', state.drawerSortKey, state.drawerSortDir, 'data-dkey="previousTradingDayChange"') +
|
|
522
|
+
buildSortableTh('\u76D8\u4E2D', 'todayChange', state.drawerSortKey, state.drawerSortDir, 'data-dkey="todayChange"') +
|
|
523
523
|
'</tr></thead><tbody>';
|
|
524
524
|
for (const c of sorted) {
|
|
525
525
|
html += '<tr><td>' + escapeHtml(c.stockCode + ' ' + c.stockName) + '</td>' +
|
|
526
526
|
'<td>' + c.holdingRatio.toFixed(2) + '%</td>' +
|
|
527
527
|
'<td class="' + colorClass(c.previousTradingDayChange) + '">' + fmtPercentWithDate(c.previousTradingDayChange, c.previousTradingDayDate ? c.previousTradingDayDate.slice(5) : '', headerDate) + '</td>' +
|
|
528
|
-
'<td class="' + colorClass(c.
|
|
528
|
+
'<td class="' + colorClass(c.intradayDisplayChange) + '"><span class="cell-value">' + fmtNullablePercent(c.intradayDisplayChange) + '</span></td></tr>';
|
|
529
529
|
}
|
|
530
530
|
return html + '</tbody></table>';
|
|
531
531
|
}
|
|
@@ -544,8 +544,8 @@ function renderDrawer() {
|
|
|
544
544
|
'<div class="drawer-title">' + escapeHtml(fund.name) + '</div>' +
|
|
545
545
|
'<div class="drawer-subtitle">' + escapeHtml(fund.code) + ' \u6301\u4ED3\u660E\u7EC6</div>' +
|
|
546
546
|
'</div><div class="drawer-actions">' +
|
|
547
|
-
'<div><div class="drawer-estimate"><span class="drawer-info-label" title="\u6309\u5F53\u524D\u6709\u76D8\u4E2D\
|
|
548
|
-
'<div class="drawer-coverage"><span class="drawer-info-label" title="\u5F53\u524D\u62FF\u5230\u76D8\u4E2D\
|
|
547
|
+
'<div><div class="drawer-estimate"><span class="drawer-info-label" title="\u6309\u5F53\u524D\u6709\u76D8\u4E2D\u6570\u636E\u7684\u6301\u4ED3\uFF0C\u6309\u6301\u4ED3\u5360\u6BD4\u52A0\u6743\u6C47\u603B\u5F97\u5230\u7684\u4F30\u503C\u3002\u8986\u76D6\u7387\u4E0D\u8DB3\u65F6\u4E0D\u5C55\u793A\u6570\u503C\u3002">\u6743\u91CD\u4F30\u503C</span><strong class="' + colorClass(fund.weightedTodayChange) + '">' + fmtNullablePercent(fund.weightedTodayChange) + '</strong></div>' +
|
|
548
|
+
'<div class="drawer-coverage"><span class="drawer-info-label" title="\u5F53\u524D\u62FF\u5230\u6709\u6548\u76D8\u4E2D\u6570\u636E\u7684\u6301\u4ED3\u6743\u91CD\u5360\u6BD4\u3002\u8986\u76D6\u7387\u8FBE\u5230 60% \u624D\u5C55\u793A\u6743\u91CD\u4F30\u503C\u3002">\u8986\u76D6</span> ' + fmtCoverage(fund.coverageRatio) + '</div></div>' +
|
|
549
549
|
buildDrawerRefreshButton() +
|
|
550
550
|
'</div></div>';
|
|
551
551
|
|
|
@@ -806,17 +806,17 @@ function bindEvents() {
|
|
|
806
806
|
|
|
807
807
|
// \u6BCF 30 \u79D2\u81EA\u52A8\u5237\u65B0\u4E00\u6B21\u4E3B\u8868\u548C\u5927\u76D8\u5361\u7247\u3002
|
|
808
808
|
setInterval(doRefresh, 30000);
|
|
809
|
-
`}function
|
|
809
|
+
`}function ot(){return`<!DOCTYPE html>
|
|
810
810
|
<html lang="zh-CN">
|
|
811
811
|
<head>
|
|
812
812
|
<meta charset="UTF-8">
|
|
813
813
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
814
814
|
<title>\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2</title>
|
|
815
|
-
<style>${
|
|
815
|
+
<style>${at()}</style>
|
|
816
816
|
</head>
|
|
817
817
|
<body>
|
|
818
|
-
${
|
|
819
|
-
<script>${
|
|
818
|
+
${nt()}
|
|
819
|
+
<script>${rt()}</script>
|
|
820
820
|
</body>
|
|
821
|
-
</html>`}async function
|
|
821
|
+
</html>`}async function xe(e){let t=we();t.use(we.json()),t.get("/",(o,r)=>{r.type("html").send(ot())}),t.post("/api/funds",async(o,r)=>{try{let i=o.body?.funds;if(!i||typeof i!="object"||Object.keys(i).length===0)return r.status(400).json({error:"funds \u914D\u7F6E\u65E0\u6548"});let d=await Ze(i);r.json(d)}catch(i){r.status(500).json({error:i.message})}}),t.get("/api/funds/:code/detail",async(o,r)=>{try{let i=o.params.code,d=typeof o.query?.name=="string"?o.query.name:i,s=o.query?.refreshPrices==="1",c=await Xe(i,d,s);r.json(c)}catch(i){r.status(500).json({error:i.message})}});let a=`http://localhost:${e}`,n=tt(e);return new Promise(o=>{let r=t.listen(e,()=>{console.log("\u670D\u52A1\u5DF2\u542F\u52A8:"),console.log(`- ${a}`),n&&console.log(`- ${n}`),et(a),o(r)});r.on("error",i=>{if(i?.code==="EADDRINUSE"){console.log(`\u7AEF\u53E3 ${e} \u5DF2\u88AB\u5360\u7528\uFF0C\u53EF\u80FD\u670D\u52A1\u5DF2\u7ECF\u542F\u52A8\u3002\u53EF\u5C1D\u8BD5\u8BBF\u95EE:`),console.log(`- ${a}`),n&&console.log(`- ${n}`),o(null);return}console.error(`\u542F\u52A8\u670D\u52A1\u5931\u8D25: ${i.message}`),o(null)})})}var{version:dt}=JSON.parse(st(new URL("../package.json",import.meta.url),"utf8"));function ct(e,t){let a=e.todayChange===null||e.todayChange===void 0||Number.isNaN(e.todayChange),n=t.todayChange===null||t.todayChange===void 0||Number.isNaN(t.todayChange);return a&&n?0:a?1:n?-1:t.todayChange-e.todayChange}var b=new it;b.name("fund").description("\u57FA\u91D1\u4F30\u503C\u67E5\u8BE2\u5DE5\u5177").version(dt,"-v, -V, --version");b.action(()=>{b.help()});b.command("config [path]").description("\u8BBE\u7F6E\u6216\u67E5\u770B\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84").action(e=>{if(e)ie(e);else{let t=K();t?console.log(t):(console.error("\u672A\u8BBE\u7F6E\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u8FD0\u884C: fund config <\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84>"),process.exit(1))}});b.command("list").description("\u5217\u51FA\u6240\u6709\u914D\u7F6E\u7684\u57FA\u91D1").action(async()=>{let e=U(),t=await V(e);t.sort(ct),ve(t)});b.command("detail <code>").description("\u67E5\u770B\u5355\u53EA\u57FA\u91D1\u6301\u4ED3\u8BE6\u60C5").action(async e=>{let a=U()[e];a||(console.error(`\u57FA\u91D1 ${e} \u672A\u5728\u914D\u7F6E\u6587\u4EF6\u4E2D\u914D\u7F6E`),process.exit(1));let n=await Q({[e]:a}),[o,r]=await Promise.all([Y(n),O()]),i=await ue(e,a,n.get(e)||[],o,r);ye(i)});b.command("serve").description("\u542F\u52A8 Web \u670D\u52A1\uFF08\u7528\u4E8E\u90E8\u7F72\u5230\u670D\u52A1\u5668\uFF09").option("-p, --port <port>","\u7AEF\u53E3\u53F7",process.env.PORT||"8888").action(async e=>{let t=parseInt(e.port,10);await xe(t)});b.command("*",null,{noHelp:!0}).action(()=>{console.error(`\u672A\u77E5\u547D\u4EE4
|
|
822
822
|
`),b.help()});b.parse();
|