程序从Alpha Vantage获取股票数据.
https://www.alphavantage.co/documentation/#
以日线数据(function=TIME_SERIES_DAILY)为例实现API的调用.
程序的目的是把请求返回的数据解析后保存到数据库中.
实现过程中,从可实现到尝试找到一种更简单的实现方式.
TIME_SERIES_DAILY返回的数据示例如下:
{
"Meta Data": {
"1. Information": "Daily Prices (open, high, low, close) and Volumes",
"2. Symbol": "002340.SZ",
"3. Last Refreshed": "2019-12-05",
"4. Output Size": "Compact",
"5. Time Zone": "US/Eastern"
},
"Time Series (Daily)": {
"2019-12-05": {
"1. open": "4.1300",
"2. high": "4.1700",
"3. low": "4.1100",
"4. close": "4.1600",
"5. volume": "13717150"
},
2019-12-04": {
"1. open": "4.0200",
"2. high": "4.1300",
"3. low": "4.0200",
"4. close": "4.1100",
"5. volume": "45928009"
}
}
}
响应消息包括Meta Data和日线数据部分.
请求到处理响应的代码结构:
public void getDaily() throws Exception {
class RequestParam {
String function;
String symbol;
String apikey = config.getToken();
}
client.open();
RequestParam param = new RequestParam();
param.function = "TIME_SERIES_DAILY";
param.symbol = "002340.SZ";
String s = config.getUrl() + "/query?" + HttpUtil.toUrlParam(param);
/// 请求,返回结果(JSON串)
String result = client.get(s);
/// 响应消息解析
DailyResponse v = objectMapper.readValue(result, new TypeReference<DailyResponse>() {
});
/// 元数据对象化
MetaData metaData = convert(v.getMetaData(), MetaData.class);
/// 解析统一股票代码
String stockCode = new GlobalStockID(metaData.getSymbol()).getId();
///< TODO 转换日线数据,生成Entity对象,输出到数据库
/// ...
client.close();
}
本文的重点是上面代码中TODO的部分.
@Data
public class DailyResponse {
@JsonProperty("Meta Data")
Map<String,String> metaData;
@JsonProperty("Time Series (Daily)")
Map<String, Map<String, String>> items;
}
增加AlphaVantageField注解.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE})
public @interface AlphaVantageField {
int order() default 0;
String name() default "";
String format() default "";
}
order: 数据项序号.如"1. open"对应1
name: 数据项名称.如"1. open"
format:保留用于处理日期格式
AlphaVantageField可用于class,也可用于属性上.优先使用属性上的注解
注解用于class时,对各个属性采用默认规则映射.
数据项名称到属性名称的映射: 去掉引导序号,点号(.);去掉单词空格.单词首字母大写.属性名小写字母开头.
如数据项名称:"3. Last Refreshed"映射到属性名称:lastRefreshed
///< 把map的元素映射到指定类的对象上,映射通过AlphaVantageField注解指定
private <T> T convert(Map<String, String> items, Class<?> clazz) throws Exception {
Constructor<?> c = clazz.getDeclaredConstructor(new Class[]{});
T target = (T) c.newInstance();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (!field.isAnnotationPresent(AlphaVantageField.class) && !clazz.isAnnotationPresent(AlphaVantageField.class))
continue;
String format = "";
String value = null;
if (field.isAnnotationPresent(AlphaVantageField.class)) {
AlphaVantageField annot = field.getAnnotation(AlphaVantageField.class);
///< 根据order值取数据项值
if (annot.order() != 0) {
value = getValueByOrder(items, annot.order());
}
format = field.getAnnotation(AlphaVantageField.class).format();
} else if (clazz.isAnnotationPresent(AlphaVantageField.class)) {
/// 根据属性名推导出key值
///< 把属性名分拆成首字母大写的单词
String s1 = Util.splitCamelCase(field.getName());
String s2 = StringUtils.capitalize(s1);
String key = getKeyLike(items, s2);
format = clazz.getAnnotation(AlphaVantageField.class).format();
value = items.get(key);
}
Util.setPropertyValue(target, clazz, field.getName(), value, format);
}
return target;
}
///< 模糊匹配map中的key,返回第1个符合的key.
private String getKeyLike(Map<String, String> items, String key) {
List<String> keys = items.entrySet().stream().filter(e -> e.getKey().endsWith(key)).map(Map.Entry::getKey).collect(Collectors.toList());
return keys.size() == 0 ? null : keys.get(0);
}
///< 获取指定order的元素的值
private String getValueByOrder(Map<String, String> items, int order) {
String prefix = String.format("%d.", order);
List<String> keys = items.entrySet().stream().filter(e -> e.getKey().startsWith(prefix)).map(Map.Entry::getKey).collect(Collectors.toList());
return keys.size() == 0 ? null : items.get(keys.get(0));
}
有2种注解方式:
在DailyPrice的属性上注解,指定order值,如
@AlphaVantageField(order=1)
private BigDecimal open;
for (Map.Entry<String, Map<String,String>> entry : v.getItems().entrySet()) {
DailyPrice dailyPrice = convert(entry.getValue(), DailyPrice.class);
Date day = new SimpleDateFormat("yyyy-MM-dd").parse(entry.getKey());
dailyPrice.setCode(stockCode);
dailyPrice.setDay(day);
dailyPriceRepository.save(dailyPrice);
}
返回的日线数据用对象定义(DailyItem).
类成员用@JsonProperty映射.
这种方式不使用自定义注解.
@Data
public class DailyResponse {
@JsonProperty("Meta Data")
Map<String,String> metaData;
@JsonProperty("Time Series (Daily)")
Map<String, DailyItem> items;
}
@Data
public class DailyItem {
@JsonProperty("1. open")
BigDecimal open;
@JsonProperty("2. high")
BigDecimal high;
@JsonProperty("3. low")
BigDecimal low;
@JsonProperty("4. close")
BigDecimal close;
@JsonProperty("5. volume")
BigDecimal volume;
}
for (Map.Entry<String, DailyItem> entry : v.getItems().entrySet()) {
DailyPrice dailyPrice = new DailyPrice();
dailyPrice.setOpen(entry.getValue().getOpen());
dailyPrice.setHighest(entry.getValue().getHigh());
dailyPrice.setLowest(entry.getValue().getLow());
dailyPrice.setClose(entry.getValue().getClose());
dailyPrice.setVolume(entry.getValue().getVolume());
Date day = new SimpleDateFormat("yyyy-MM-dd").parse(entry.getKey());
dailyPrice.setCode(stockCode);
dailyPrice.setDay(day);
dailyPriceRepository.save(dailyPrice);
}
在上述代码的基础上,把DailyItem作为DailyResponse的内部类.
@Data
public class DailyResponse {
@Data
static class DailyItem {
@JsonProperty("1. open")
private BigDecimal open;
@JsonProperty("2. high")
private BigDecimal high;
@JsonProperty("3. low")
private BigDecimal low;
@JsonProperty("4. close")
private BigDecimal close;
@JsonProperty("5. volume")
private BigDecimal volume;
}
@JsonProperty("Meta Data")
Map<String,String> metaData;
@JsonProperty("Time Series (Daily)")
Map<String, DailyItem> items;
}
for (Map.Entry<String, DailyResponse.DailyItem> entry : v.getItems().entrySet()) {
DailyPrice dailyPrice = new DailyPrice();
dailyPrice.setOpen(entry.getValue().getOpen());
dailyPrice.setHighest(entry.getValue().getHigh());
dailyPrice.setLowest(entry.getValue().getLow());
dailyPrice.setClose(entry.getValue().getClose());
dailyPrice.setVolume(entry.getValue().getVolume());
Date day = new SimpleDateFormat("yyyy-MM-dd").parse(entry.getKey());
dailyPrice.setCode(stockCode);
dailyPrice.setDay(day);
dailyPriceRepository.save(dailyPrice);
}
DailyItem必须是static类. 否则运行时报异常:
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.keeption.kioph.provider.alphavantage.DailyResponse$DailyItem` (although at least one Creator exists):
can only instantiate non-static inner class by using default, no-argument constructor
。非注解内部类实现
解析一次由ObjectMapper完成.
不需要额外自定义注解.
这种方式是直接使用FasterXML Jackson实现.
。注解实现
可以在DailyItem上使用注解而不需要每个属性上注解,可以得到最少的应用代码.
但如果一个属性名尾部与另一属性名称相同,这种情况目前不支持.
以上代码只是尝试不同的实现方式的测试代码,实现方式上没有刻意统一。
如DailyResponse.metaData可以用DailyItem类似的方式,用MetaData取代Map<String,String>类型.
但MetaData作为独立的公共类,因为其他API也需要使用.