Skip to content

Commit a1fc62b

Browse files
committed
Rewrite to be a serverless function
1 parent b43dd3d commit a1fc62b

8 files changed

+179
-210
lines changed

.editorconfig

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 2
6+
charset = utf-8
7+
trim_trailing_whitespace = false
8+
insert_final_newline = false
9+
10+
[*.md]
11+
trim_trailing_whitespace = false
12+
13+
[*.js]
14+
indent_size = 2
15+
insert_final_newline = true

AlphaVantageClient.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import AlphaVantage from 'alphavantage'
2+
import AutoCache from './AutoCache'
3+
4+
// Initialize AlphaVantage API Client
5+
export const alpha = AlphaVantage({ key: process.env.ALPHAVANTAGE })

AutoCache.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { existsSync, readFileSync, writeFileSync } from 'fs'
2+
import { join } from 'path'
3+
4+
// Constants
5+
const AUTOCACHE_PATH = join(__dirname, 'alphavantage.cache')
6+
7+
// Cache to circumvent AlphaVantage API restrictions
8+
export default class AutoCache {
9+
static tryInit () {
10+
if (
11+
Object.keys(AutoCache.cache).length === 0 &&
12+
existsSync(AUTOCACHE_PATH)
13+
) {
14+
AutoCache.cache = JSON.parse(readFileSync(AUTOCACHE_PATH))
15+
}
16+
}
17+
18+
static verifyArgs (args1, args2) {
19+
let valid = args1.length === args2.length
20+
if (!valid) console.log('[Cache] Parameter count mismatch')
21+
for (const i of args1.keys()) {
22+
if (!valid) break
23+
valid &= args1[i] === args2[i]
24+
}
25+
if (!valid) console.log('[Cache] Detected invalid args')
26+
return valid
27+
}
28+
29+
static retrieve (key, args) {
30+
console.log(`[Cache::FetchDry] ${key}`)
31+
AutoCache.tryInit()
32+
return key in AutoCache.cache &&
33+
AutoCache.verifyArgs(args, AutoCache.cache[key].args)
34+
? AutoCache.cache[key].data
35+
: null
36+
}
37+
38+
static store (key, args, data) {
39+
console.log(`[Cache::Store] ${key}`)
40+
AutoCache.cache[key] = { args, data }
41+
writeFileSync(AUTOCACHE_PATH, JSON.stringify(AutoCache.cache))
42+
return data
43+
}
44+
45+
static async _call (key, fn, ...args) {
46+
console.log(`[Cache::FetchWet] ${key}`)
47+
return fn(...args)
48+
}
49+
50+
static async call (key, fn, ...args) {
51+
const newKey = `${key}<${JSON.stringify(args)}>`
52+
return (
53+
AutoCache.retrieve(newKey, args) ||
54+
AutoCache.store(newKey, args, await AutoCache._call(newKey, fn, ...args))
55+
)
56+
}
57+
}
58+
59+
AutoCache.cache = {}

DividendProjector.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import AutoCache from './AutoCache'
2+
import { alpha } from './AlphaVantageClient'
3+
4+
export async function getDividendStatistics (symbol) {
5+
const data = await getDividendPayments(symbol)
6+
const yearlyAverageDividendYield = DividendProjector.getYearlyAverageDividendYield(
7+
data.history
8+
)
9+
const yearlyAverageDividendYieldPercentage =
10+
100 * (yearlyAverageDividendYield - 1.0)
11+
const totalDividendAmount12m = DividendProjector.getTotalDividendAmount(
12+
data.history,
13+
12
14+
)
15+
const totalDividendAmount24m = DividendProjector.getTotalDividendAmount(
16+
data.history,
17+
24
18+
)
19+
return {
20+
symbol: data.symbol,
21+
updated: data.updated,
22+
zone: data.zone,
23+
yearlyAverageDividendYield,
24+
yearlyAverageDividendYieldPercentage,
25+
totalDividendAmount12m,
26+
totalDividendAmount24m
27+
}
28+
}
29+
30+
export async function getDividendPayments (symbol) {
31+
const res = await AutoCache.call(
32+
'monthly_adjusted',
33+
alpha.data.monthly_adjusted,
34+
symbol
35+
)
36+
return {
37+
...res.meta,
38+
history: Object.entries(res.data).map(([time, { dividend }]) => ({
39+
time,
40+
dividend: parseFloat(dividend)
41+
}))
42+
}
43+
}
44+
45+
export default class DividendProjector {
46+
static getTotalDividendAmount (data, timeframeInMonths) {
47+
return data
48+
.slice(1, timeframeInMonths + 1)
49+
.reduce((acc, { dividend }) => acc + dividend, 0.0)
50+
}
51+
52+
static getYearlyAverageDividendYield (data) {
53+
const validPeriods = data.slice(1).slice(0, 12 * 5) // five year averages
54+
const indexOfFirstPaidPeriod = validPeriods.findIndex(
55+
p => p.dividend > 0
56+
)
57+
const indexOfLastPaidPeriod =
58+
validPeriods.length -
59+
1 -
60+
[...validPeriods].reverse().findIndex(p => p.dividend > 0)
61+
const validPeriodsAfterFirstPaid = validPeriods.slice(
62+
indexOfFirstPaidPeriod,
63+
indexOfLastPaidPeriod
64+
)
65+
const periodRate =
66+
validPeriodsAfterFirstPaid[0].dividend /
67+
validPeriodsAfterFirstPaid[validPeriodsAfterFirstPaid.length - 1]
68+
.dividend
69+
const averageDividendGrowthPerYear =
70+
periodRate ** (1.0 / (validPeriodsAfterFirstPaid.length / 12.0))
71+
return averageDividendGrowthPerYear
72+
}
73+
}

api/stockinfo.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import StockInfo from 'stock-info'
2+
3+
class StockInfoClient {
4+
static async getStockInformation (symbol) {
5+
return new Promise((resolve, reject) => {
6+
StockInfo.getSingleStockInfo(symbol).then(resolve).catch(reject)
7+
})
8+
}
9+
}
10+
11+
module.exports = (req, res) => {
12+
const { symbol } = JSON.parse(req.body)
13+
StockInfoClient.getStockInformation(symbol).then(data => {
14+
res.json(data)
15+
})
16+
}

main.js

+1-209
Original file line numberDiff line numberDiff line change
@@ -3,209 +3,12 @@
33
'use strict'
44

55
// Import libraries
6-
import { existsSync, readFileSync, writeFileSync } from 'fs'
7-
import { join } from 'path'
8-
import AlphaVantage from 'alphavantage'
9-
import jayson from 'jayson'
10-
import cors from 'cors'
11-
import bodyParser from 'body-parser'
12-
import connect from 'connect'
136
import StockInfo from 'stock-info'
147

15-
// Load environment
16-
require('dotenv').config()
17-
18-
// Constants
19-
const AUTOCACHE_PATH = join(__dirname, 'alphavantage.cache')
20-
21-
// Initialize AlphaVantage API Client
22-
const alpha = AlphaVantage({ key: process.env.ALPHAVANTAGE })
23-
24-
// Cache to circumvent AlphaVantage API restrictions
25-
class AutoCache {
26-
static tryInit () {
27-
if (
28-
Object.keys(AutoCache.cache).length === 0 &&
29-
existsSync(AUTOCACHE_PATH)
30-
) {
31-
AutoCache.cache = JSON.parse(readFileSync(AUTOCACHE_PATH))
32-
}
33-
}
34-
35-
static verifyArgs (args1, args2) {
36-
let valid = args1.length === args2.length
37-
if (!valid) console.log('[Cache] Parameter count mismatch')
38-
for (const i of args1.keys()) {
39-
if (!valid) break
40-
valid &= args1[i] === args2[i]
41-
}
42-
if (!valid) console.log('[Cache] Detected invalid args')
43-
return valid
44-
}
45-
46-
static retrieve (key, args) {
47-
console.log(`[Cache::FetchDry] ${key}`)
48-
AutoCache.tryInit()
49-
return key in AutoCache.cache &&
50-
AutoCache.verifyArgs(args, AutoCache.cache[key].args)
51-
? AutoCache.cache[key].data
52-
: null
53-
}
54-
55-
static store (key, args, data) {
56-
console.log(`[Cache::Store] ${key}`)
57-
AutoCache.cache[key] = { args, data }
58-
writeFileSync(AUTOCACHE_PATH, JSON.stringify(AutoCache.cache))
59-
return data
60-
}
61-
62-
static async _call (key, fn, ...args) {
63-
console.log(`[Cache::FetchWet] ${key}`)
64-
const rawData = await fn(...args)
65-
return alpha.util.polish(rawData)
66-
}
67-
68-
static async call (key, fn, ...args) {
69-
const newKey = `${key}<${JSON.stringify(args)}>`
70-
return (
71-
AutoCache.retrieve(newKey, args) ||
72-
AutoCache.store(newKey, args, await AutoCache._call(newKey, fn, ...args))
73-
)
74-
}
75-
}
76-
AutoCache.cache = {}
77-
78-
class DividendProjector {
79-
static getTotalDividendAmount (data, timeframeInMonths) {
80-
return data
81-
.slice(1, timeframeInMonths + 1)
82-
.reduce((acc, { dividend }) => acc + dividend, 0.0)
83-
}
84-
85-
static getYearlyAverageDividendYield (data) {
86-
const validPeriods = data.slice(1).slice(0, 12 * 5) // five year averages
87-
const indexOfFirstPaidPeriod = validPeriods.findIndex(
88-
p => p.dividend > 0
89-
)
90-
const indexOfLastPaidPeriod =
91-
validPeriods.length -
92-
1 -
93-
[...validPeriods].reverse().findIndex(p => p.dividend > 0)
94-
const validPeriodsAfterFirstPaid = validPeriods.slice(
95-
indexOfFirstPaidPeriod,
96-
indexOfLastPaidPeriod
97-
)
98-
const periodRate =
99-
validPeriodsAfterFirstPaid[0].dividend /
100-
validPeriodsAfterFirstPaid[validPeriodsAfterFirstPaid.length - 1]
101-
.dividend
102-
const averageDividendGrowthPerYear =
103-
periodRate ** (1.0 / (validPeriodsAfterFirstPaid.length / 12.0))
104-
return averageDividendGrowthPerYear
105-
}
106-
}
107-
108-
async function getDividendStatistics (symbol) {
109-
const data = await getDividendPayments(symbol)
110-
const yearlyAverageDividendYield = DividendProjector.getYearlyAverageDividendYield(
111-
data.history
112-
)
113-
const yearlyAverageDividendYieldPercentage =
114-
100 * (yearlyAverageDividendYield - 1.0)
115-
const totalDividendAmount12m = DividendProjector.getTotalDividendAmount(
116-
data.history,
117-
12
118-
)
119-
const totalDividendAmount24m = DividendProjector.getTotalDividendAmount(
120-
data.history,
121-
24
122-
)
123-
return {
124-
symbol: data.symbol,
125-
updated: data.updated,
126-
zone: data.zone,
127-
yearlyAverageDividendYield,
128-
yearlyAverageDividendYieldPercentage,
129-
totalDividendAmount12m,
130-
totalDividendAmount24m
131-
}
132-
}
133-
134-
async function getDividendPayments (symbol) {
135-
const res = await AutoCache.call(
136-
'monthly_adjusted',
137-
alpha.data.monthly_adjusted,
138-
symbol
139-
)
140-
return {
141-
...res.meta,
142-
history: Object.entries(res.data).map(([time, { dividend }]) => ({
143-
time,
144-
dividend: parseFloat(dividend)
145-
}))
146-
}
147-
}
148-
149-
// Easy chaining of complex async operations
150-
class ComplexOperation {
151-
constructor (context) {
152-
this.chain = []
153-
this.context = context
154-
}
155-
156-
static init (context = {}) {
157-
return new ComplexOperation(context)
158-
}
159-
160-
next ({ fn, errorCode }) {
161-
this.chain.push({ fn, errorCode })
162-
return this
163-
}
164-
165-
middleware (fn) {
166-
return this.next(fn())
167-
}
168-
169-
assert ({ fn, errorCode }) {
170-
const condFn = context => {
171-
return new Promise((resolve, reject) => {
172-
if (fn(context)) {
173-
resolve({})
174-
} else {
175-
reject(new Error('Assertion failed.'))
176-
}
177-
})
178-
}
179-
return this.next({ fn: condFn, errorCode })
180-
}
181-
182-
async evaluate ({ success, error }) {
183-
for (const { fn, errorCode } of this.chain) {
184-
try {
185-
this.context = {
186-
...this.context,
187-
...(await fn(this.context))
188-
}
189-
} catch (e) {
190-
console.log('ERROR')
191-
console.log({ code: errorCode, data: e })
192-
error({
193-
code: errorCode,
194-
message: 'Error',
195-
data: e
196-
})
197-
return
198-
}
199-
}
200-
console.log(this.context)
201-
success(this.context)
202-
}
203-
}
204-
2058
class StockInfoClient {
2069
static async getStockInformation (symbol) {
20710
return new Promise((resolve, reject) => {
208-
AutoCache.call('stockinfo__getsinglestockinfo', StockInfo.getSingleStockInfo, symbol).then(resolve).catch(reject)
11+
StockInfo.getSingleStockInfo(symbol).then(resolve).catch(reject)
20912
})
21013
}
21114
}
@@ -235,14 +38,3 @@ const server = jayson.Server({
23538
StockInfoClient.getStockInformation(args.symbol).then(res => cb(null, res)).catch(() => cb(error))
23639
}
23740
})
238-
239-
// Configure server
240-
const app = connect()
241-
app.use(cors({ methods: ['GET', 'POST'] }))
242-
app.use(bodyParser.json())
243-
app.use(server.middleware())
244-
245-
// Listen
246-
app.listen(process.env.PORT, process.env.HOST, () => {
247-
console.log(`Listening on port ${process.env.PORT}.`)
248-
})

0 commit comments

Comments
 (0)