package com.yami.trading.service.item;
|
|
import cn.hutool.core.util.StrUtil;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.yami.trading.bean.data.domain.Kline;
|
import com.yami.trading.bean.data.domain.Realtime;
|
import com.yami.trading.bean.item.domain.Item;
|
import com.yami.trading.bean.item.domain.ItemPreMarketConfig;
|
import com.yami.trading.bean.item.dto.ItemPreMarketConfigDTO;
|
import com.yami.trading.bean.item.dto.ItemPreMarketConfigSaveModel;
|
import com.yami.trading.common.exception.YamiShopBindException;
|
import com.yami.trading.dao.item.ItemPreMarketConfigMapper;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import javax.annotation.PostConstruct;
|
import java.math.BigDecimal;
|
import java.time.LocalTime;
|
import java.time.ZoneId;
|
import java.time.ZonedDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeParseException;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.stream.Collectors;
|
|
@Service
|
@Transactional
|
public class ItemPreMarketService extends ServiceImpl<ItemPreMarketConfigMapper, ItemPreMarketConfig> {
|
|
private static final ZoneId US_EASTERN = ZoneId.of("America/New_York");
|
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("H:mm");
|
|
private final Map<String, ItemPreMarketConfig> cache = new ConcurrentHashMap<>();
|
|
@Autowired
|
private ItemService itemService;
|
|
@PostConstruct
|
public void loadCache() {
|
refreshCache();
|
}
|
|
public void refreshCache() {
|
cache.clear();
|
List<ItemPreMarketConfig> list = list(new LambdaQueryWrapper<ItemPreMarketConfig>()
|
.eq(ItemPreMarketConfig::getDelFlag, 0)
|
.eq(ItemPreMarketConfig::getEnabled, 1));
|
for (ItemPreMarketConfig config : list) {
|
if (StrUtil.isBlank(config.getSymbol())) {
|
continue;
|
}
|
cache.put(config.getSymbol(), config);
|
Item item = itemService.findBySymbol(config.getSymbol());
|
if (item != null && StrUtil.isNotBlank(item.getSymbol())
|
&& !item.getSymbol().equals(config.getSymbol())) {
|
cache.put(item.getSymbol(), config);
|
}
|
}
|
}
|
|
public IPage<ItemPreMarketConfigDTO> listRecord(Page<ItemPreMarketConfig> page, String symbol) {
|
LambdaQueryWrapper<ItemPreMarketConfig> wrapper = new LambdaQueryWrapper<ItemPreMarketConfig>()
|
.eq(ItemPreMarketConfig::getDelFlag, 0)
|
.orderByDesc(ItemPreMarketConfig::getCreateTime);
|
if (StrUtil.isNotBlank(symbol)) {
|
wrapper.like(ItemPreMarketConfig::getSymbol, symbol);
|
}
|
IPage<ItemPreMarketConfig> entityPage = page(page, wrapper);
|
Page<ItemPreMarketConfigDTO> dtoPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
dtoPage.setRecords(entityPage.getRecords().stream().map(this::toDto).collect(Collectors.toList()));
|
return dtoPage;
|
}
|
|
public ItemPreMarketConfigDTO getDetail(String uuid) {
|
ItemPreMarketConfig config = getById(uuid);
|
if (config == null || config.getDelFlag() != null && config.getDelFlag() != 0) {
|
throw new YamiShopBindException("盘前配置不存在");
|
}
|
return toDto(config);
|
}
|
|
public void saveConfig(ItemPreMarketConfigSaveModel model) {
|
Item item = itemService.findBySymbol(model.getSymbol());
|
if (item == null) {
|
throw new YamiShopBindException("股票不存在");
|
}
|
if (!Item.US_STOCKS.equalsIgnoreCase(item.getType())
|
&& !Item.US_STOCKS.equalsIgnoreCase(item.getOpenCloseType())) {
|
throw new YamiShopBindException("仅支持美股配置盘前");
|
}
|
if (model.getPrePrice() == null || model.getPrePrice().compareTo(BigDecimal.ZERO) <= 0) {
|
throw new YamiShopBindException("盘前价格必须大于0");
|
}
|
parseTime(model.getStartTime());
|
parseTime(model.getEndTime());
|
|
if (StrUtil.isBlank(model.getUuid())) {
|
long count = count(new LambdaQueryWrapper<ItemPreMarketConfig>()
|
.eq(ItemPreMarketConfig::getSymbol, model.getSymbol())
|
.eq(ItemPreMarketConfig::getDelFlag, 0));
|
if (count > 0) {
|
throw new YamiShopBindException("该股票已存在盘前配置");
|
}
|
ItemPreMarketConfig config = new ItemPreMarketConfig();
|
fillConfig(config, model);
|
save(config);
|
} else {
|
ItemPreMarketConfig config = getById(model.getUuid());
|
if (config == null) {
|
throw new YamiShopBindException("盘前配置不存在");
|
}
|
fillConfig(config, model);
|
updateById(config);
|
}
|
refreshCache();
|
}
|
|
public void deleteConfig(String uuid) {
|
ItemPreMarketConfig config = getById(uuid);
|
if (config == null) {
|
throw new YamiShopBindException("盘前配置不存在");
|
}
|
removeById(uuid);
|
refreshCache();
|
}
|
|
public boolean isPreMarketTradingActive(String symbol) {
|
ItemPreMarketConfig config = resolveConfig(symbol);
|
return config != null && isInWindow(config);
|
}
|
|
public Realtime applyPreMarketPrice(Realtime realtime) {
|
if (realtime == null || StrUtil.isBlank(realtime.getSymbol())) {
|
return realtime;
|
}
|
ItemPreMarketConfig config = resolveConfig(realtime.getSymbol());
|
if (config != null && isInWindow(config)) {
|
double price = config.getPrePrice().doubleValue();
|
realtime.setPreMarketActive(true);
|
realtime.setPreMarketPrice(price);
|
// 仅覆盖当前价;open/high/low/涨跌幅等保持原始行情数据
|
realtime.setClose(price);
|
} else {
|
realtime.setPreMarketActive(false);
|
realtime.setPreMarketPrice(null);
|
}
|
return realtime;
|
}
|
|
public Double getActivePreMarketPrice(String symbol) {
|
ItemPreMarketConfig config = resolveConfig(symbol);
|
if (config != null && isInWindow(config)) {
|
return config.getPrePrice().doubleValue();
|
}
|
return null;
|
}
|
|
/** 按 item 标准 symbol / remarks / 大小写 匹配盘前配置 */
|
private ItemPreMarketConfig resolveConfig(String symbol) {
|
if (StrUtil.isBlank(symbol)) {
|
return null;
|
}
|
ItemPreMarketConfig config = cache.get(symbol);
|
if (config != null) {
|
return config;
|
}
|
Item item = itemService.findBySymbol(symbol);
|
if (item != null && StrUtil.isNotBlank(item.getSymbol())) {
|
config = cache.get(item.getSymbol());
|
if (config != null) {
|
return config;
|
}
|
}
|
for (Map.Entry<String, ItemPreMarketConfig> entry : cache.entrySet()) {
|
if (entry.getKey().equalsIgnoreCase(symbol)) {
|
return entry.getValue();
|
}
|
}
|
return null;
|
}
|
|
/** 盘前时段仅覆盖 K 线最后一根的最新价 */
|
public void applyPreMarketToLatestKline(String symbol, List<Kline> klines) {
|
if (klines == null || klines.isEmpty()) {
|
return;
|
}
|
Double price = getActivePreMarketPrice(symbol);
|
if (price == null) {
|
return;
|
}
|
Kline last = klines.get(klines.size() - 1);
|
last.setClose(price);
|
if (price > last.getHigh()) {
|
last.setHigh(price);
|
}
|
if (price < last.getLow()) {
|
last.setLow(price);
|
}
|
}
|
|
private void fillConfig(ItemPreMarketConfig config, ItemPreMarketConfigSaveModel model) {
|
config.setSymbol(model.getSymbol());
|
config.setStartTime(normalizeTime(model.getStartTime()));
|
config.setEndTime(normalizeTime(model.getEndTime()));
|
config.setPrePrice(model.getPrePrice());
|
config.setEnabled(model.getEnabled() == null ? 1 : model.getEnabled());
|
}
|
|
private ItemPreMarketConfigDTO toDto(ItemPreMarketConfig config) {
|
ItemPreMarketConfigDTO dto = new ItemPreMarketConfigDTO();
|
dto.setUuid(config.getUuid());
|
dto.setSymbol(config.getSymbol());
|
dto.setStartTime(config.getStartTime());
|
dto.setEndTime(config.getEndTime());
|
dto.setPrePrice(config.getPrePrice());
|
dto.setEnabled(config.getEnabled());
|
try {
|
Item item = itemService.findBySymbol(config.getSymbol());
|
if (item != null) {
|
dto.setSymbolName(item.getName());
|
}
|
} catch (Exception ignored) {
|
}
|
return dto;
|
}
|
|
private boolean isInWindow(ItemPreMarketConfig config) {
|
LocalTime start = parseTime(config.getStartTime());
|
LocalTime end = parseTime(config.getEndTime());
|
LocalTime now = ZonedDateTime.now(US_EASTERN).toLocalTime();
|
if (start.equals(end)) {
|
return true;
|
}
|
if (start.isBefore(end)) {
|
return !now.isBefore(start) && now.isBefore(end);
|
}
|
return !now.isBefore(start) || now.isBefore(end);
|
}
|
|
private LocalTime parseTime(String time) {
|
if (StrUtil.isBlank(time)) {
|
throw new YamiShopBindException("时间格式错误");
|
}
|
try {
|
return LocalTime.parse(time.trim(), TIME_FMT);
|
} catch (DateTimeParseException e) {
|
throw new YamiShopBindException("时间格式错误,请使用 HH:mm,如 9:30");
|
}
|
}
|
|
private String normalizeTime(String time) {
|
return parseTime(time).format(TIME_FMT);
|
}
|
}
|