Feat plugin stock (#196)
This commit is contained in:
BIN
.github/readme/imgs/plugin_stock_token.png
vendored
Normal file
BIN
.github/readme/imgs/plugin_stock_token.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -28,11 +28,13 @@
|
||||
}
|
||||
|
||||
/**Formatter */
|
||||
export function format(n, {sign = false} = {}) {
|
||||
export function format(n, {sign = false, unit = true} = {}) {
|
||||
if (unit) {
|
||||
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}]) {
|
||||
if (n/v >= 1)
|
||||
return `${(sign)&&(n > 0) ? "+" : ""}${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
|
||||
}
|
||||
}
|
||||
return `${(sign)&&(n > 0) ? "+" : ""}${n}`
|
||||
}
|
||||
|
||||
|
||||
75
source/app/mocks/api/axios/get/yahoo.mjs
Normal file
75
source/app/mocks/api/axios/get/yahoo.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Wakatime api
|
||||
if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) {
|
||||
//Get company profile
|
||||
if (/get-profile/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
price:{
|
||||
marketCap:{
|
||||
raw:faker.random.number(1000000000),
|
||||
},
|
||||
symbol:"OCTO",
|
||||
},
|
||||
quoteType:{
|
||||
shortName:faker.company.companyName(),
|
||||
longName:faker.company.companyName(),
|
||||
exchangeTimezoneName:faker.address.timeZone(),
|
||||
symbol:"OCTO",
|
||||
},
|
||||
calendarEvents:{},
|
||||
summaryDetail:{},
|
||||
symbol:"OCTO",
|
||||
assetProfile:{
|
||||
fullTimeEmployees:faker.random.number(10000),
|
||||
city:faker.address.city(),
|
||||
country:faker.address.country(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Get stock chart
|
||||
if (/get-chart/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
chart:{
|
||||
result:[
|
||||
{
|
||||
meta:{
|
||||
currency:"USD",
|
||||
symbol:"OCTO",
|
||||
regularMarketPrice:faker.random.number(10000)/100,
|
||||
chartPreviousClose:faker.random.number(10000)/100,
|
||||
previousClose:faker.random.number(10000)/100,
|
||||
},
|
||||
timestamp:new Array(1000).fill(Date.now()).map((x, i) => x+i*60000),
|
||||
indicators:{
|
||||
quote:[
|
||||
{
|
||||
close:new Array(1000).fill(null).map(_ => faker.random.number(10000)/100),
|
||||
get low() {
|
||||
return this.close
|
||||
},
|
||||
get high() {
|
||||
return this.close
|
||||
},
|
||||
get open() {
|
||||
return this.close
|
||||
},
|
||||
volume:[],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,7 +230,7 @@
|
||||
favorites:distribution(7).map((value, index, array) => ({name:faker.lorem.word(), color:faker.internet.color(), value, size:faker.random.number(1000000), x:array.slice(0, index).reduce((a, b) => a + b, 0)}))
|
||||
}
|
||||
}) : null),
|
||||
//Languages
|
||||
//RSS
|
||||
...(set.plugins.enabled.rss ? ({
|
||||
rss:{
|
||||
source:faker.lorem.words(),
|
||||
@@ -242,6 +242,20 @@
|
||||
})),
|
||||
}
|
||||
}) : null),
|
||||
//Stock price
|
||||
...(set.plugins.enabled.stock ? ({
|
||||
stock:{
|
||||
chart:"(stock chart is not displayed in placeholder)",
|
||||
currency:"USD",
|
||||
price:faker.random.number(10000)/100,
|
||||
previous:faker.random.number(10000)/100,
|
||||
get delta() { return this.price-this.previous },
|
||||
symbol:options["stock.symbol"],
|
||||
company:faker.company.companyName(),
|
||||
interval:options["stock.interval"],
|
||||
duration:options["stock.duration"],
|
||||
}
|
||||
}) : null),
|
||||
//Habits
|
||||
...(set.plugins.enabled.habits ? ({
|
||||
habits:{
|
||||
|
||||
34
source/plugins/stock/README.md
Normal file
34
source/plugins/stock/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
### 💹 Stock prices
|
||||
|
||||
The *stock* plugin lets you display the stock market price of a given company.
|
||||
|
||||
<table>
|
||||
<td align="center">
|
||||
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.stock.svg">
|
||||
<img width="900" height="1" alt="">
|
||||
</td>
|
||||
</table>
|
||||
|
||||
<details>
|
||||
<summary>💬 Obtaining a RapidAPI Yahoo Finance token</summary>
|
||||
|
||||
Create a [RapidAPI account](https://rapidapi.com) and subscribe to [Yahoo Finance API](https://rapidapi.com/apidojo/api/yahoo-finance1) to get a token.
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
#### ℹ️ Examples workflows
|
||||
|
||||
[➡️ Available options for this plugin](metadata.yml)
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
plugin_stock: yes
|
||||
plugin_stock_token: ${{ secrets.STOCK_TOKEN }} # RapidAPI Yahoo Finance token
|
||||
plugin_stock_symbol: TSLA # Display Tesla stock price
|
||||
plugin_stock_duration: 1d # Display last day of market
|
||||
plugin_stock_interval: 5m # Use precision of 5 minutes for each record
|
||||
```
|
||||
60
source/plugins/stock/index.mjs
Normal file
60
source/plugins/stock/index.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stock))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {symbol, interval, duration} = imports.metadata.plugins.stock.inputs({data, account, q})
|
||||
if (!token)
|
||||
throw {error:{message:"A token is required"}}
|
||||
if (!symbol)
|
||||
throw {error:{message:"A company stock symbol is required"}}
|
||||
symbol = symbol.toLocaleUpperCase()
|
||||
|
||||
//Query API for company informations
|
||||
console.debug(`metrics/compute/${login}/plugins > stock > querying api for company`)
|
||||
const {data:{quoteType:{shortName:company}}} = await imports.axios.get("https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-profile", {
|
||||
params:{symbol, region:"US"},
|
||||
headers:{"x-rapidapi-key":token},
|
||||
})
|
||||
|
||||
//Query API for sotck charts
|
||||
console.debug(`metrics/compute/${login}/plugins > stock > querying api for stock`)
|
||||
const {data:{chart:{result:[{meta, timestamp, indicators:{quote:[{close}]}}]}}} = await imports.axios.get("https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-chart", {
|
||||
params:{interval, symbol, range:duration, region:"US"},
|
||||
headers:{"x-rapidapi-key":token},
|
||||
})
|
||||
const {currency, regularMarketPrice:price, previousClose:previous} = meta
|
||||
|
||||
//Generating chart
|
||||
console.debug(`metrics/compute/${login}/plugins > stock > generating chart`)
|
||||
const chart = await imports.chartist("line", {
|
||||
width:480,
|
||||
height:160,
|
||||
showPoint:false,
|
||||
axisX:{showGrid:false, labelInterpolationFnc:(value, index) => index%Math.floor(close.length/4) === 0 ? value : null},
|
||||
axisY:{scaleMinSpace:20},
|
||||
showArea:true,
|
||||
}, {
|
||||
labels:timestamp.map(timestamp => new Intl.DateTimeFormat("en-GB", {month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit"}).format(new Date(timestamp*1000))),
|
||||
series:[close],
|
||||
})
|
||||
|
||||
//Results
|
||||
return {chart, currency, price, previous, delta:price-previous, symbol, company, interval, duration}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
58
source/plugins/stock/metadata.yml
Normal file
58
source/plugins/stock/metadata.yml
Normal file
@@ -0,0 +1,58 @@
|
||||
name: "💹 Stock prices"
|
||||
cost: N/A
|
||||
categorie: other
|
||||
index: 1
|
||||
supports:
|
||||
- user
|
||||
- organization
|
||||
inputs:
|
||||
|
||||
# Enable or disable plugin
|
||||
plugin_stock:
|
||||
description: Display stock prices of a given company
|
||||
type: boolean
|
||||
default: no
|
||||
|
||||
# RapidAPI Yahoo finance token
|
||||
# Case insensitive
|
||||
plugin_stock_token:
|
||||
description: Yahoo Finance token
|
||||
type: token
|
||||
default: ""
|
||||
|
||||
# Company stock symbol (required)
|
||||
plugin_stock_symbol:
|
||||
description: Company stock symbol
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
# Time range to display (relative to current date)
|
||||
plugin_stock_duration:
|
||||
description: Time range to display
|
||||
type: string
|
||||
default: 1d
|
||||
values:
|
||||
- 1d # Today
|
||||
- 5d # 5 days
|
||||
- 1mo # 1 month
|
||||
- 3mo # 3 months
|
||||
- 6mo # 6 months
|
||||
- 1y # 1 year
|
||||
- 2y # 2 years
|
||||
- 5y # 5 years
|
||||
- 10y # 10 years
|
||||
- ytd # Year to date
|
||||
- max # All time
|
||||
|
||||
# Time invervals between each records over the given time range
|
||||
plugin_stock_interval:
|
||||
description: Time intervals between records
|
||||
type: string
|
||||
default: 5m
|
||||
values:
|
||||
- 1m # 1 minute
|
||||
- 2m # 2 minutes
|
||||
- 5m # 5 minutes
|
||||
- 15m # 15 minutes
|
||||
- 60m # 60 minutes
|
||||
- 1d # 1 day
|
||||
17
source/plugins/stock/tests.yml
Normal file
17
source/plugins/stock/tests.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
- name: Stock plugin (default)
|
||||
uses: lowlighter/metrics@latest
|
||||
with:
|
||||
token: NOT_NEEDED
|
||||
plugin_stock: yes
|
||||
plugin_stock_token: MOCKED_TOKEN
|
||||
plugin_stock_symbol: OCTO
|
||||
|
||||
- name: Stock plugin (complete)
|
||||
uses: lowlighter/metrics@latest
|
||||
with:
|
||||
token: NOT_NEEDED
|
||||
plugin_stock: yes
|
||||
plugin_stock_token: MOCKED_TOKEN
|
||||
plugin_stock_symbol: OCTO
|
||||
plugin_stock_duration: 5d
|
||||
plugin_stock_interval: 5m
|
||||
@@ -25,5 +25,6 @@
|
||||
"wakatime",
|
||||
"skyline",
|
||||
"stackoverflow",
|
||||
"stock",
|
||||
"achievements"
|
||||
]
|
||||
48
source/templates/classic/partials/stock.ejs
Normal file
48
source/templates/classic/partials/stock.ejs
Normal file
@@ -0,0 +1,48 @@
|
||||
<% if (plugins.stock) { %>
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M10.75 9a.75.75 0 000 1.5h1.5a.75.75 0 000-1.5h-1.5z"></path><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm14.5 0V5h-13V3.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25zm0 2.75h-13v5.75c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V6.5z"></path></svg>
|
||||
Stock prices <%= plugins.stock.symbol ? `for ${plugins.stock.symbol}` : "" %>
|
||||
</h2>
|
||||
<% if (plugins.stock.error) { %>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<div class="field error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.stock.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="row">
|
||||
<section>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 14.25c0 .138.112.25.25.25H4v-1.25a.75.75 0 01.75-.75h2.5a.75.75 0 01.75.75v1.25h2.25a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v12.5zM1.75 16A1.75 1.75 0 010 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 00.25-.25V8.285a.25.25 0 00-.111-.208l-1.055-.703a.75.75 0 11.832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0114.25 16h-3.5a.75.75 0 01-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 01-.75-.75V14h-1v1.25a.75.75 0 01-.75.75h-3zM3 3.75A.75.75 0 013.75 3h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 3.75zM3.75 6a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM3 9.75A.75.75 0 013.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 9.75zM7.75 9a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM7 6.75A.75.75 0 017.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 017 6.75zM7.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5z"></path></svg>
|
||||
<%= plugins.stock.company %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6 2a.75.75 0 01.696.471L10 10.731l1.304-3.26A.75.75 0 0112 7h3.25a.75.75 0 010 1.5h-2.742l-1.812 4.528a.75.75 0 01-1.392 0L6 4.77 4.696 8.03A.75.75 0 014 8.5H.75a.75.75 0 010-1.5h2.742l1.812-4.529A.75.75 0 016 2z"></path></svg>
|
||||
Valued at <%= plugins.stock.price.toFixed(2) %> <%= plugins.stock.currency %>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z"></path></svg>
|
||||
<%= {"1d":"Today", "5d":"Last five days", "1mo":"Last month", "3mo":"Last trimester", "6mo":"Last semester", "1y":"Last year", "2y":"Last two years", "5y":"Last five years", "10y":"Last ten years", ytd:"Year to date", max:"All-time"}[plugins.stock.duration] %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<% if (plugins.stock.delta > 0) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.47 7.78a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 0l4.25 4.25a.75.75 0 01-1.06 1.06L9 4.81v7.44a.75.75 0 01-1.5 0V4.81L4.53 7.78a.75.75 0 01-1.06 0z"></path></svg>
|
||||
<% } else { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M13.03 8.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.47 9.28a.75.75 0 011.06-1.06l2.97 2.97V3.75a.75.75 0 011.5 0v7.44l2.97-2.97a.75.75 0 011.06 0z"></path></svg>
|
||||
<% } %>
|
||||
<%= f(plugins.stock.delta.toFixed(2), {sign:true}) %> (<%= f((100*plugins.stock.delta/plugins.stock.price).toFixed(2), {sign:true}) %>%)
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="stock-chart">
|
||||
<%- plugins.stock.chart %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
@@ -916,6 +916,22 @@
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.ct-line {
|
||||
stroke-width: 2px !important;
|
||||
stroke: #58A6FF !important;
|
||||
}
|
||||
.ct-area {
|
||||
fill: #58A6FF !important;
|
||||
}
|
||||
.ct-label {
|
||||
fill: rgba(127, 127, 127, 0.8) !important;
|
||||
color: rgba(127, 127, 127, 0.8) !important;
|
||||
}
|
||||
.ct-grid {
|
||||
stroke: rgba(127, 127, 127, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Fade animation */
|
||||
.af {
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user