|
|
//// ChartViewController.m// HC//// Created by huilinLi on 2025/11/27.//
#import "ChartViewController.h"#import "StockKLineModel.h"#import "StockInfoCardView.h"
static CGFloat kLineUnitWidth = 5.0; // 单位宽度static CGFloat kPriceLabelAreaWidth = 35.0;static CGFloat kPriceLabelPadding = 10.0;static CGFloat kKLineHeight = 300.0;static CGFloat kContainerHeight = 120.0;
@interface ChartViewController () <UIGestureRecognizerDelegate, UIScrollViewDelegate>
@property (nonatomic, strong) StockInfoCardView *cardContainer;@property (nonatomic, strong) UIView *kSelectContainer;@property (nonatomic, strong) UIView *kLineContainer;@property (nonatomic, strong) UIView *macdContainer;@property (nonatomic, strong) UIView *kdjContainer;@property (nonatomic, strong) UIView *otherContainer;@property (nonatomic, strong) UIScrollView *kLineScrollView;@property (nonatomic, strong) NSArray *kLineData;@property (nonatomic, assign) NSInteger visibleKLineCount;@property (nonatomic, strong) NSArray<UILabel *> *priceLabels;@property (nonatomic, strong) UILabel *startDateLabel;@property (nonatomic, strong) UILabel *endDateLabel;@property (nonatomic, strong) UILabel *maLegendLabel; // MA5@property (nonatomic, strong) UILabel *macdLegendLabel; // DIF DEA@property (nonatomic, strong) UILabel *kdjLegendLabel; // K D@property (nonatomic, strong) UILabel *highPriceMarkLabel;@property (nonatomic, strong) UILabel *lowPriceMarkLabel;@property (nonatomic, assign) CGFloat currentMaxPrice;@property (nonatomic, assign) CGFloat currentMinPrice;@property (nonatomic, assign) CGFloat macdMaxValue;@property (nonatomic, assign) CGFloat macdMinValue;@property (nonatomic, assign) CGFloat kdjMaxValue;@property (nonatomic, assign) CGFloat kdjMinValue;
// K线图层@property (nonatomic, strong) CAShapeLayer *redCandleLayer;@property (nonatomic, strong) CAShapeLayer *greenCandleLayer;
// 均线图层@property (nonatomic, strong) CAShapeLayer *ma5Layer;@property (nonatomic, strong) CAShapeLayer *ma10Layer;@property (nonatomic, strong) CAShapeLayer *ma30Layer;
// MACD图层@property (nonatomic, strong) CAShapeLayer *macdRedBarLayer;@property (nonatomic, strong) CAShapeLayer *macdGreenBarLayer;@property (nonatomic, strong) CAShapeLayer *difLayer;@property (nonatomic, strong) CAShapeLayer *deaLayer;@property (nonatomic, strong) CAShapeLayer *zeroLineLayer;
// KDJ图层@property (nonatomic, strong) CAShapeLayer *kLayer;@property (nonatomic, strong) CAShapeLayer *dLayer;@property (nonatomic, strong) CAShapeLayer *jLayer;
@property (nonatomic, strong) UIView *crossVerticalLine;@property (nonatomic, strong) UIView *crossHorizontalLine;@property (nonatomic, strong) UILabel *crossPriceLabel;@property (nonatomic, strong) UILabel *crossDateLabel;@property (nonatomic, assign) BOOL isLongPressing;
@end
@implementation ChartViewController
#pragma mark - viewDidLoad- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blackColor]; self.visibleKLineCount = 40;
[self generateMockData]; [self calculateMA]; [self calculateMACD]; [self calculateKDJ]; [self setupSubviews]; [self setupConstraints]; [self setupDojiViews]; CGFloat chartVisibleWidth = self.view.bounds.size.width - kPriceLabelAreaWidth; if (chartVisibleWidth > 0) { kLineUnitWidth = chartVisibleWidth / self.visibleKLineCount; }
dispatch_async(dispatch_get_main_queue(), ^{ CGFloat totalChartWidth = kLineUnitWidth * self.kLineData.count; self.kLineScrollView.contentSize = CGSizeMake(totalChartWidth, self.kLineContainer.bounds.size.height); // 滚到最右侧 if (self.kLineScrollView.contentSize.width > self.kLineScrollView.bounds.size.width) { CGPoint offset = CGPointMake(self.kLineScrollView.contentSize.width - self.kLineScrollView.bounds.size.width, 0);//(横向偏移,纵向) [self.kLineScrollView setContentOffset:offset animated:NO];// 禁用动画效果 } [self drawAllCharts]; });}
#pragma mark - 绘制
- (void)drawAllCharts { if (self.kLineData.count == 0) return; // 获取当前ScrollView滚到了哪里 CGFloat contentOffsetX = self.kLineScrollView.contentOffset.x; // 可视区域宽度 CGFloat visibleWidth = self.kLineScrollView.bounds.size.width; NSInteger startIndex = floor(contentOffsetX / kLineUnitWidth);// 向下取整 NSInteger endIndex = ceil((contentOffsetX + visibleWidth) / kLineUnitWidth);// 向上取整 // 防止越界 if (startIndex < 0) startIndex = 0; if (endIndex >= self.kLineData.count) endIndex = self.kLineData.count - 1; if (startIndex > endIndex) endIndex = startIndex; // 把start,end和偏移量传给所有子视图 [self drawKLineChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX]; [self drawMACDChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX]; [self drawKDJChartFromIndex:startIndex toIndex:endIndex contentOffset:contentOffsetX]; // 更新指标参数 if (!self.isLongPressing) { NSInteger visibleEndIndex = endIndex; CGFloat visibleRightX = contentOffsetX + self.kLineScrollView.bounds.size.width; NSInteger calculatedIndex = floor(visibleRightX / kLineUnitWidth) - 1; if (calculatedIndex < 0) calculatedIndex = 0; if (calculatedIndex >= self.kLineData.count) calculatedIndex = self.kLineData.count - 1; visibleEndIndex = calculatedIndex; [self updateLegendsWithIndex:visibleEndIndex]; }}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (self.isLongPressing) { self.isLongPressing = NO; self.crossVerticalLine.hidden = YES; self.crossHorizontalLine.hidden = YES; self.crossPriceLabel.hidden = YES; self.crossDateLabel.hidden = YES; } [self drawAllCharts];}
#pragma mark - 绘制K线+均线
- (void)drawKLineChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX { // 图层大小=容器大小 CGRect bounds = self.kLineContainer.bounds; self.redCandleLayer.frame = bounds; self.greenCandleLayer.frame = bounds; self.ma5Layer.frame = bounds; self.ma10Layer.frame = bounds; self.ma30Layer.frame = bounds; CGFloat chartHeight = bounds.size.height; // 最高最低价 CGFloat maxPrice = 0, minPrice = 0; NSInteger maxIndex = -1, minIndex = -1; [self calculateMinMaxPriceForStartIndex:startIndex endIndex:endIndex maxPrice:&maxPrice minPrice:&minPrice maxIndex:&maxIndex minIndex:&minIndex];//将计算出的最高价赋值给max/minPrice self.currentMaxPrice = maxPrice; self.currentMinPrice = minPrice; CGFloat priceRange = maxPrice - minPrice; if (priceRange <= 0) { self.redCandleLayer.path = nil; return; } // 更新价格和日期 [self updatePriceLabelsWithMaxPrice:maxPrice minPrice:minPrice]; [self updateDateLabelsStartIndex:startIndex endIndex:endIndex];
// 准备路径 UIBezierPath *redPath = [UIBezierPath bezierPath];// 涨的路径 UIBezierPath *greenPath = [UIBezierPath bezierPath];// 跌的路径 UIBezierPath *ma5Path = [UIBezierPath bezierPath];// 均线路径 UIBezierPath *ma10Path = [UIBezierPath bezierPath]; UIBezierPath *ma30Path = [UIBezierPath bezierPath]; CGFloat kLineWidth = kLineUnitWidth * 0.8;// k线实体宽度,0.2是间隔 BOOL f5 = YES, f10 = YES, f30 = YES; // 路径是否是第一个点(第一个点用moveToPoint,后面用addLineToPoint)
for (NSInteger i = startIndex; i <= endIndex; i++) { StockKLineModel *model = self.kLineData[i]; // 坐标转换 CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX]; CGFloat xLeft = xCenter - kLineWidth / 2.0;
// 高度 * (最高价 - 当前价)/ 价格区间 CGFloat yOpen = chartHeight * (maxPrice - model.open) / priceRange; CGFloat yClose = chartHeight * (maxPrice - model.close) / priceRange; CGFloat yHigh = chartHeight * (maxPrice - model.high) / priceRange; CGFloat yLow = chartHeight * (maxPrice - model.low) / priceRange; UIBezierPath *targetPath = (model.close >= model.open) ? redPath : greenPath; [targetPath moveToPoint:CGPointMake(xCenter, yHigh)]; [targetPath addLineToPoint:CGPointMake(xCenter, yLow)];// 影线 // 实体 [targetPath appendPath:[UIBezierPath bezierPathWithRect: CGRectMake(xLeft,// 左边界 MIN(yOpen, yClose),// 上边界 kLineWidth,// 宽 MAX(1.0, fabs(yClose - yOpen)))]];// 高 // 绘制均线 void (^drawMA)(UIBezierPath*, CGFloat, BOOL*) = ^(UIBezierPath *p, CGFloat v, BOOL *f) { if (v > 0) { CGFloat y = chartHeight * (maxPrice - v) / priceRange;// y轴 if (*f) {// 是第一个点 [p moveToPoint:CGPointMake(xCenter, y)];// 移动到起点 *f = NO; }// 起点已设置 else { [p addLineToPoint:CGPointMake(xCenter, y)]; }// 连线,从上一个点链接这个点 } }; drawMA(ma5Path, model.MA5, &f5); drawMA(ma10Path, model.MA10, &f10); drawMA(ma30Path, model.MA30, &f30); } [CATransaction begin];// 开启Core Animation事务 [CATransaction setDisableActions:YES];// 禁止动画 self.redCandleLayer.path = redPath.CGPath;// 图层绑定 self.greenCandleLayer.path = greenPath.CGPath; self.ma5Layer.path = ma5Path.CGPath; self.ma10Layer.path = ma10Path.CGPath; self.ma30Layer.path = ma30Path.CGPath; [CATransaction commit];// 事务提交 // 更新最高最低价的箭头位置 [self updateMaxMinArrowsWithMaxIndex:maxIndex minIndex:minIndex maxPrice:maxPrice minPrice:minPrice chartHeight:chartHeight range:priceRange contentOffsetX:contentOffsetX];}
#pragma mark - 绘制MACD- (void)drawMACDChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX { CGRect bounds = self.macdContainer.bounds; self.macdRedBarLayer.frame = bounds; self.macdGreenBarLayer.frame = bounds; self.difLayer.frame = bounds; self.deaLayer.frame = bounds; self.zeroLineLayer.frame = bounds; CGFloat h = bounds.size.height; // 计算极值 CGFloat maxV = -MAXFLOAT, minV = MAXFLOAT;// 先给正负无穷 for (NSInteger i = startIndex; i <= endIndex; i++) { StockKLineModel *m = self.kLineData[i]; maxV = MAX(maxV, MAX(m.macdBar, MAX(m.dif, m.dea))); minV = MIN(minV, MIN(m.macdBar, MIN(m.dif, m.dea))); } if (maxV == minV) { maxV += 1; minV -= 1; } self.macdMaxValue = maxV; self.macdMinValue = minV; CGFloat range = maxV - minV; CGFloat zeroY = h * (maxV - 0) / range; UIBezierPath *rPath = [UIBezierPath bezierPath]; UIBezierPath *gPath = [UIBezierPath bezierPath]; UIBezierPath *dPath = [UIBezierPath bezierPath]; UIBezierPath *ePath = [UIBezierPath bezierPath]; UIBezierPath *zPath = [UIBezierPath bezierPath]; [zPath moveToPoint:CGPointMake(0, zeroY)];// 0轴起点 [zPath addLineToPoint:CGPointMake(bounds.size.width, zeroY)];// 0轴终点 CGFloat barWidth = kLineUnitWidth * 0.2; BOOL first = YES;// 快线慢线起点标记 for (NSInteger i = startIndex; i <= endIndex; i++) { StockKLineModel *m = self.kLineData[i]; CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX]; CGFloat xLeft = xCenter - barWidth/2.0; CGFloat yBar = h * (maxV - m.macdBar) / range; CGFloat barY = (m.macdBar > 0) ? yBar : zeroY;// 上边界 CGFloat barH = MAX(0.5, fabs(zeroY - yBar));// 高度,大于0.5,省得看不见
UIBezierPath *tp = (m.macdBar > 0) ? rPath : gPath; [tp appendPath:[UIBezierPath bezierPathWithRect: CGRectMake(xLeft, barY, barWidth, barH)]]; CGFloat yD = h * (maxV - m.dif) / range; CGFloat yE = h * (maxV - m.dea) / range; if (first) { [dPath moveToPoint:CGPointMake(xCenter, yD)]; [ePath moveToPoint:CGPointMake(xCenter, yE)]; first = NO; } else { [dPath addLineToPoint:CGPointMake(xCenter, yD)]; [ePath addLineToPoint:CGPointMake(xCenter, yE)]; } } [CATransaction begin]; [CATransaction setDisableActions:YES]; self.macdRedBarLayer.path = rPath.CGPath; self.macdGreenBarLayer.path = gPath.CGPath; self.difLayer.path = dPath.CGPath; self.deaLayer.path = ePath.CGPath; self.zeroLineLayer.path = zPath.CGPath; [CATransaction commit];}
#pragma mark - 绘制KDJ- (void)drawKDJChartFromIndex:(NSInteger)startIndex toIndex:(NSInteger)endIndex contentOffset:(CGFloat)contentOffsetX { self.kLayer.frame = self.kdjContainer.bounds; self.dLayer.frame = self.kdjContainer.bounds; self.jLayer.frame = self.kdjContainer.bounds; CGFloat h = self.kdjContainer.bounds.size.height; CGFloat maxV = 0, minV = 100; for (NSInteger i = startIndex; i <= endIndex; i++) { StockKLineModel *m = self.kLineData[i]; maxV = MAX(maxV, MAX(m.K, MAX(m.D, m.J))); minV = MIN(minV, MIN(m.K, MIN(m.D, m.J))); } self.kdjMaxValue = maxV; self.kdjMinValue = minV; CGFloat range = maxV - minV; if (range <= 0) range = 1; UIBezierPath *kp = [UIBezierPath bezierPath]; UIBezierPath *dp = [UIBezierPath bezierPath]; UIBezierPath *jp = [UIBezierPath bezierPath]; BOOL first = YES; for (NSInteger i = startIndex; i <= endIndex; i++) { StockKLineModel *m = self.kLineData[i]; CGFloat xCenter = [self getScreenCenterXAtIndex:i contentOffset:contentOffsetX]; CGFloat yK = h * (maxV - m.K) / range; CGFloat yD = h * (maxV - m.D) / range; CGFloat yJ = h * (maxV - m.J) / range; if (first) { [kp moveToPoint:CGPointMake(xCenter, yK)]; [dp moveToPoint:CGPointMake(xCenter, yD)]; [jp moveToPoint:CGPointMake(xCenter, yJ)]; first = NO; } else { [kp addLineToPoint:CGPointMake(xCenter, yK)]; [dp addLineToPoint:CGPointMake(xCenter, yD)]; [jp addLineToPoint:CGPointMake(xCenter, yJ)]; } } [CATransaction begin]; [CATransaction setDisableActions:YES]; self.kLayer.path = kp.CGPath; self.dLayer.path = dp.CGPath; self.jLayer.path = jp.CGPath; [CATransaction commit];}
#pragma mark - 坐标计算// 获取某根K线在屏幕上的x轴中心坐标- (CGFloat)getScreenCenterXAtIndex:(NSInteger)index contentOffset:(CGFloat)offsetX { // 半个单位宽+索引*宽 CGFloat x = kLineUnitWidth * (index + 0.5); // 屏幕坐标 = 绝对坐标 - 滚动偏移量 + 左侧空白区 return x - offsetX + kPriceLabelAreaWidth;}
#pragma mark - 极值箭头更新- (void)updateMaxMinArrowsWithMaxIndex:(NSInteger)maxIdx minIndex:(NSInteger)minIdx maxPrice:(CGFloat)maxPrice minPrice:(CGFloat)minPrice chartHeight:(CGFloat)height range:(CGFloat)range contentOffsetX:(CGFloat)offsetX { void (^updateLabel)(UILabel *, NSInteger, CGFloat) = ^(UILabel *label, NSInteger index, CGFloat price) { if (index >= 0 && index < self.kLineData.count) { label.hidden = NO; CGFloat xCenter = [self getScreenCenterXAtIndex:index contentOffset:offsetX]; CGFloat y = height * (maxPrice - price) / range;// y轴位置 // 默认放在k线右边 label.text = [NSString stringWithFormat:@"← %.2f", price]; [label sizeToFit];// 根据文字尺寸自适应标签长度 CGFloat targetX = xCenter + kLineUnitWidth/2.0 + 2 + label.bounds.size.width/2.0; label.center = CGPointMake(targetX, y); // 如果label不在屏幕内,就放到k线左边 if (CGRectGetMaxX(label.frame) > self.kLineContainer.bounds.size.width) { label.text = [NSString stringWithFormat:@"%.2f →", price]; [label sizeToFit]; targetX = xCenter - kLineUnitWidth/2.0 - 2 - label.bounds.size.width/2.0; label.center = CGPointMake(targetX, y); } } else { label.hidden = YES; } }; StockKLineModel *maxModel = self.kLineData[maxIdx]; updateLabel(self.highPriceMarkLabel, maxIdx, maxModel.high); StockKLineModel *minModel = self.kLineData[minIdx]; updateLabel(self.lowPriceMarkLabel, minIdx, minModel.low);}
#pragma mark - 更新指标数值- (void)updateLegendsWithIndex:(NSInteger)index { if (index < 0 || index >= self.kLineData.count) return; StockKLineModel *m = self.kLineData[index];
NSMutableAttributedString * (^createStr)(NSString *, UIColor *) = ^(NSString *txt, UIColor *col) { return [[NSMutableAttributedString alloc] initWithString:txt attributes:@{NSForegroundColorAttributeName: col}]; }; // MA NSMutableAttributedString *maStr = [[NSMutableAttributedString alloc] init]; [maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA5:%.2f ", m.MA5], [UIColor yellowColor])]; [maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA10:%.2f ", m.MA10], [UIColor magentaColor])]; [maStr appendAttributedString:createStr([NSString stringWithFormat:@"MA30:%.2f", m.MA30], [UIColor cyanColor])]; self.maLegendLabel.attributedText = maStr; // MACD NSMutableAttributedString *macdStr = [[NSMutableAttributedString alloc] init]; [macdStr appendAttributedString:createStr([NSString stringWithFormat:@"DIF:%.2f ", m.dif], [UIColor whiteColor])]; [macdStr appendAttributedString:createStr([NSString stringWithFormat:@"DEA:%.2f ", m.dea], [UIColor yellowColor])]; UIColor *barColor = (m.macdBar > 0) ? [UIColor redColor] : [UIColor greenColor]; [macdStr appendAttributedString:createStr([NSString stringWithFormat:@"MACD:%.2f", m.macdBar], barColor)]; self.macdLegendLabel.attributedText = macdStr; // KDJ NSMutableAttributedString *kdjStr = [[NSMutableAttributedString alloc] init]; [kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"K:%.2f ", m.K], [UIColor whiteColor])]; [kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"D:%.2f ", m.D], [UIColor yellowColor])]; [kdjStr appendAttributedString:createStr([NSString stringWithFormat:@"J:%.2f", m.J], [UIColor magentaColor])]; self.kdjLegendLabel.attributedText = kdjStr;}
#pragma mark - 手势与缩放- (void)handlePinchGesture:(UIPinchGestureRecognizer *)gesture { if (self.isLongPressing) {// 缩放不显示十字星 self.isLongPressing = NO; self.crossVerticalLine.hidden = YES; self.crossHorizontalLine.hidden = YES; self.crossPriceLabel.hidden = YES; self.crossDateLabel.hidden = YES; } if (gesture.state == UIGestureRecognizerStateChanged) { CGFloat scale = gesture.scale; CGFloat minUnitWidth = (self.view.bounds.size.width - kPriceLabelAreaWidth) / 500.0; CGFloat maxUnitWidth = 40.0; CGFloat newUnitWidth = MAX(minUnitWidth, MIN(kLineUnitWidth * scale, maxUnitWidth)); // 屏幕中心点缩放 CGFloat ratio = (self.kLineScrollView.contentOffset.x + self.kLineScrollView.bounds.size.width/2.0) / self.kLineScrollView.contentSize.width; kLineUnitWidth = newUnitWidth;// 全局更新 // 新的滚动视图的内容宽度 CGFloat newContentWidth = kLineUnitWidth * self.kLineData.count; self.kLineScrollView.contentSize = CGSizeMake(newContentWidth, self.kLineContainer.bounds.size.height); // 新的偏移量 CGFloat newOffset = ratio * newContentWidth - self.kLineScrollView.bounds.size.width/2.0; self.kLineScrollView.contentOffset = CGPointMake(MAX(0, newOffset), 0); [self drawAllCharts]; gesture.scale = 1.0; }}
#pragma mark - 数据生成- (void)generateMockData { NSMutableArray *arr = [NSMutableArray array]; CGFloat lastClose = 100.0; for (int i = 0; i < 2000; i++) { StockKLineModel *model = [[StockKLineModel alloc] init]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:-(2000 - i) * 24 * 3600]; NSDateFormatter *fmt = [[NSDateFormatter alloc] init]; [fmt setDateFormat:@"yyyy-MM-dd"]; model.date = [fmt stringFromDate:date]; CGFloat volatility = lastClose * 0.02; CGFloat randomChange = ((arc4random() % 100) / 100.0 - 0.5) * 2 * volatility; model.open = lastClose + ((arc4random() % 100) / 100.0 - 0.5) * volatility * 0.5; model.close = model.open + randomChange; CGFloat maxOC = MAX(model.open, model.close); CGFloat minOC = MIN(model.open, model.close); model.high = maxOC + (arc4random() % 100) / 100.0 * 1.0; model.low = minOC - (arc4random() % 100) / 100.0 * 1.0; if (model.low < 0) model.low = 0.01; [arr addObject:model]; lastClose = model.close; } self.kLineData = arr;}
- (void)calculateMA { for (int i = 0; i < self.kLineData.count; i++) { StockKLineModel *model = self.kLineData[i]; model.MA5 = [self getMAWithIndex:i count:5]; model.MA10 = [self getMAWithIndex:i count:10]; model.MA30 = [self getMAWithIndex:i count:30]; }}- (CGFloat)getMAWithIndex:(NSInteger)index count:(NSInteger)count { if (index < count - 1) return 0; CGFloat sum = 0; for (NSInteger i = index; i > index - count; i--) { StockKLineModel *m = self.kLineData[i]; sum += m.close; } return sum / count;}- (void)calculateMACD { if (self.kLineData.count == 0) return; const CGFloat kShortEMA = 2.0 / (12 + 1); const CGFloat kLongEMA = 2.0 / (26 + 1); const CGFloat kSignalEMA = 2.0 / (9 + 1); CGFloat lastShortEMA = 0, lastLongEMA = 0, lastDEA = 0; for (int i = 0; i < self.kLineData.count; i++) { StockKLineModel *model = self.kLineData[i]; CGFloat close = model.close; if (i == 0) { lastShortEMA = close; lastLongEMA = close; model.dif = 0; model.dea = 0; model.macdBar = 0; } else { lastShortEMA = kShortEMA * close + (1 - kShortEMA) * lastShortEMA; lastLongEMA = kLongEMA * close + (1 - kLongEMA) * lastLongEMA; model.dif = lastShortEMA - lastLongEMA; model.dea = kSignalEMA * model.dif + (1 - kSignalEMA) * lastDEA; model.macdBar = 2.0 * (model.dif - model.dea); } lastDEA = model.dea; }}- (void)calculateKDJ { CGFloat k = 50.0, d = 50.0; for (int i = 0; i < self.kLineData.count; i++) { StockKLineModel *model = self.kLineData[i]; NSInteger startIndex = MAX(0, i - 8); CGFloat maxHigh = -MAXFLOAT; CGFloat minLow = MAXFLOAT; for (NSInteger j = startIndex; j <= i; j++) { StockKLineModel *m = self.kLineData[j]; maxHigh = MAX(maxHigh, m.high); minLow = MIN(minLow, m.low); } CGFloat rsv = 0; if (maxHigh != minLow) rsv = (model.close - minLow) / (maxHigh - minLow) * 100.0; k = (2.0 * k + rsv) / 3.0; d = (2.0 * d + k) / 3.0; model.K = k; model.D = d; model.J = 3.0 * k - 2.0 * d; }}
#pragma mark 计算极值- (void)calculateMinMaxPriceForStartIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex maxPrice:(CGFloat *)maxPrice minPrice:(CGFloat *)minPrice maxIndex:(NSInteger *)maxIdx minIndex:(NSInteger *)minIdx { *maxPrice = -MAXFLOAT; *minPrice = MAXFLOAT; *maxIdx = -1; *minIdx = -1; if (self.kLineData.count == 0) return; for (NSInteger i = startIndex; i <= endIndex; i++) { StockKLineModel *m = self.kLineData[i]; if (m.high > *maxPrice) { *maxPrice = m.high; *maxIdx = i; } if (m.low < *minPrice) { *minPrice = m.low; *minIdx = i; } if (m.MA5 > 0) { *maxPrice = MAX(*maxPrice, m.MA5); *minPrice = MIN(*minPrice, m.MA5); } if (m.MA10 > 0) { *maxPrice = MAX(*maxPrice, m.MA10); *minPrice = MIN(*minPrice, m.MA10); } if (m.MA30 > 0) { *maxPrice = MAX(*maxPrice, m.MA30); *minPrice = MIN(*minPrice, m.MA30); } } if (*maxPrice > *minPrice) { CGFloat d = *maxPrice - *minPrice; *maxPrice += d * 0.05; *minPrice -= d * 0.05; }}
#pragma mark - setupSubviews-(void) setupSubviews { UIColor *bgColor = [UIColor colorWithRed:26.0/255.0 green:26.0/255.0 blue:26.0/255.0 alpha:1.0]; _cardContainer = [[StockInfoCardView alloc] init]; _cardContainer.translatesAutoresizingMaskIntoConstraints = NO; [_cardContainer setupView]; [self.view addSubview:_cardContainer];
_kSelectContainer = [[UIView alloc] init]; _kSelectContainer.backgroundColor = bgColor; _kSelectContainer.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:_kSelectContainer]; [self addKSelectOptions]; self.kLineContainer = [self createContainerViewWithColor:bgColor]; self.macdContainer = [self createContainerViewWithColor:bgColor]; self.kdjContainer = [self createContainerViewWithColor:bgColor]; self.otherContainer = [self createContainerViewWithColor:bgColor]; self.kLineScrollView = [[UIScrollView alloc] init]; self.kLineScrollView.backgroundColor = [UIColor clearColor]; self.kLineScrollView.showsHorizontalScrollIndicator = NO; self.kLineScrollView.translatesAutoresizingMaskIntoConstraints = NO; self.kLineScrollView.delegate = self; [self.kLineContainer addSubview:self.kLineScrollView]; [NSLayoutConstraint activateConstraints:@[ [self.kLineScrollView.topAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor], [self.kLineScrollView.bottomAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor], [self.kLineScrollView.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth], [self.kLineScrollView.trailingAnchor constraintEqualToAnchor:self.kLineContainer.trailingAnchor], ]]; UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)]; [self.kLineScrollView addGestureRecognizer:pinchGesture]; [self setupAllLayers]; [self setupLabels]; self.maLegendLabel = [self createLegendLabel]; [self.view addSubview:self.maLegendLabel]; self.macdLegendLabel = [self createLegendLabel]; [self.view addSubview:self.macdLegendLabel]; self.kdjLegendLabel = [self createLegendLabel]; [self.view addSubview:self.kdjLegendLabel]; [NSLayoutConstraint activateConstraints:@[ [self.maLegendLabel.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth], [self.maLegendLabel.bottomAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor], [self.macdLegendLabel.leadingAnchor constraintEqualToAnchor:self.macdContainer.leadingAnchor constant:kPriceLabelAreaWidth], [self.macdLegendLabel.bottomAnchor constraintEqualToAnchor:self.macdContainer.topAnchor], [self.kdjLegendLabel.leadingAnchor constraintEqualToAnchor:self.kdjContainer.leadingAnchor constant:kPriceLabelAreaWidth], [self.kdjLegendLabel.bottomAnchor constraintEqualToAnchor:self.kdjContainer.topAnchor] ]]; self.highPriceMarkLabel = [self createMarkLabel]; [self.kLineContainer addSubview:self.highPriceMarkLabel]; self.lowPriceMarkLabel = [self createMarkLabel]; [self.kLineContainer addSubview:self.lowPriceMarkLabel];}
- (UIView *)createContainerViewWithColor:(UIColor *)color { UIView *uiView = [[UIView alloc] init]; uiView.backgroundColor = color; uiView.translatesAutoresizingMaskIntoConstraints = NO; uiView.clipsToBounds = YES; [self.view addSubview:uiView]; return uiView;}
#pragma mark - setupConstraints- (void)setupConstraints { [NSLayoutConstraint activateConstraints:@[ [_cardContainer.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor], [_cardContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [_cardContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor], [_cardContainer.heightAnchor constraintEqualToConstant:100], [_kSelectContainer.topAnchor constraintEqualToAnchor:_cardContainer.bottomAnchor constant:5], [_kSelectContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [_kSelectContainer.widthAnchor constraintEqualToAnchor:self.view.widthAnchor], [_kSelectContainer.heightAnchor constraintEqualToConstant:40], ]]; // 循环设置图表容器约束 NSArray *containers = @[self.kLineContainer, self.macdContainer, self.kdjContainer, self.otherContainer]; NSArray *heights = @[@(kKLineHeight), @(kContainerHeight), @(kContainerHeight), @(kContainerHeight)]; UIView *previousView = _kSelectContainer; for (int i = 0; i < containers.count; i++) { UIView *container = containers[i]; // 间距 第一个是15,MACD是30,其他是15 CGFloat spacing = (i == 1) ? 30.0 : 15.0; [NSLayoutConstraint activateConstraints:@[ [container.topAnchor constraintEqualToAnchor:previousView.bottomAnchor constant:spacing], [container.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], [container.widthAnchor constraintEqualToAnchor:self.view.widthAnchor], [container.heightAnchor constraintEqualToConstant:[heights[i] floatValue]] ]]; previousView = container; }}
- (void)setupAllLayers { // K线 _redCandleLayer = [self createLayerColor:[UIColor redColor] width:1.0 filled:YES]; _greenCandleLayer = [self createLayerColor:[UIColor greenColor] width:1.0 filled:YES]; [self.kLineContainer.layer insertSublayer:_redCandleLayer atIndex:0]; [self.kLineContainer.layer insertSublayer:_greenCandleLayer atIndex:0]; // 均线 _ma5Layer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO]; _ma10Layer = [self createLayerColor:[UIColor magentaColor] width:1.0 filled:NO]; _ma30Layer = [self createLayerColor:[UIColor cyanColor] width:1.0 filled:NO]; [self.kLineContainer.layer insertSublayer:_ma5Layer below:_redCandleLayer]; [self.kLineContainer.layer insertSublayer:_ma10Layer below:_ma5Layer]; [self.kLineContainer.layer insertSublayer:_ma30Layer below:_ma10Layer]; // MACD _macdRedBarLayer = [self createLayerColor:[UIColor redColor] width:0 filled:YES]; _macdGreenBarLayer = [self createLayerColor:[UIColor greenColor] width:0 filled:YES]; _difLayer = [self createLayerColor:[UIColor whiteColor] width:1.0 filled:NO]; _deaLayer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO]; _zeroLineLayer = [self createLayerColor:[UIColor grayColor] width:0.5 filled:NO]; [self.macdContainer.layer insertSublayer:_zeroLineLayer atIndex:0]; [self.macdContainer.layer insertSublayer:_macdRedBarLayer above:_zeroLineLayer]; [self.macdContainer.layer insertSublayer:_macdGreenBarLayer above:_macdRedBarLayer]; [self.macdContainer.layer insertSublayer:_difLayer above:_macdGreenBarLayer]; [self.macdContainer.layer insertSublayer:_deaLayer above:_difLayer]; // KDJ _kLayer = [self createLayerColor:[UIColor whiteColor] width:1.0 filled:NO]; _dLayer = [self createLayerColor:[UIColor yellowColor] width:1.0 filled:NO]; _jLayer = [self createLayerColor:[UIColor magentaColor] width:1.0 filled:NO]; [self.kdjContainer.layer insertSublayer:_kLayer atIndex:0]; [self.kdjContainer.layer insertSublayer:_dLayer above:_kLayer]; [self.kdjContainer.layer insertSublayer:_jLayer above:_dLayer];}
- (CAShapeLayer *)createLayerColor:(UIColor *)color width:(CGFloat)width filled:(BOOL)fill { CAShapeLayer *layer = [CAShapeLayer layer]; layer.lineWidth = width; layer.strokeColor = color.CGColor; layer.fillColor = fill ? color.CGColor : [UIColor clearColor].CGColor; layer.lineCap = kCALineCapSquare; layer.lineJoin = kCALineJoinRound;// 圆角连接,避免折线拐角出现尖锐锯齿 return layer;}
- (UILabel *)createLegendLabel { UILabel *label = [[UILabel alloc] init]; label.font = [UIFont systemFontOfSize:10]; label.textColor = [UIColor whiteColor]; label.backgroundColor = [UIColor clearColor]; label.translatesAutoresizingMaskIntoConstraints = NO; return label;}
- (UILabel *)createMarkLabel { UILabel *label = [[UILabel alloc] init]; label.font = [UIFont systemFontOfSize:10]; label.textColor = [UIColor whiteColor]; label.backgroundColor = [UIColor clearColor]; label.hidden = YES; return label;}
- (void)setupLabels { NSMutableArray *priceLabelArr = [NSMutableArray array]; CGFloat chartUsableHeight = kKLineHeight - kPriceLabelPadding; for (NSInteger i = 0; i < 5; i++) { UILabel *label = [[UILabel alloc] init]; label.text = @"--"; label.textColor = [UIColor lightGrayColor]; label.font = [UIFont systemFontOfSize:10]; label.translatesAutoresizingMaskIntoConstraints = NO; [self.kLineContainer addSubview:label]; [NSLayoutConstraint activateConstraints:@[ [label.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:2], [label.topAnchor constraintEqualToAnchor:self.kLineContainer.topAnchor constant: (chartUsableHeight / 4 * i)] ]]; [priceLabelArr addObject:label]; } self.priceLabels = priceLabelArr; self.startDateLabel = [[UILabel alloc] init]; self.startDateLabel.textColor = [UIColor lightGrayColor]; self.startDateLabel.font = [UIFont systemFontOfSize:10]; self.startDateLabel.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:self.startDateLabel]; self.endDateLabel = [[UILabel alloc] init]; self.endDateLabel.textColor = [UIColor lightGrayColor]; self.endDateLabel.font = [UIFont systemFontOfSize:10]; self.endDateLabel.textAlignment = NSTextAlignmentRight; self.endDateLabel.translatesAutoresizingMaskIntoConstraints = NO; [self.view addSubview:self.endDateLabel]; [NSLayoutConstraint activateConstraints:@[ [self.startDateLabel.leadingAnchor constraintEqualToAnchor:self.kLineContainer.leadingAnchor constant:kPriceLabelAreaWidth], [self.startDateLabel.topAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor constant:2], [self.endDateLabel.trailingAnchor constraintEqualToAnchor:self.kLineContainer.trailingAnchor], [self.endDateLabel.topAnchor constraintEqualToAnchor:self.kLineContainer.bottomAnchor constant:2], ]];}
#pragma mark - 左侧价格刻度计算- (void)updatePriceLabelsWithMaxPrice:(CGFloat)maxPrice minPrice:(CGFloat)minPrice { CGFloat range = maxPrice - minPrice; for (NSInteger i = 0; i < 5; i++) { self.priceLabels[i].text = [NSString stringWithFormat:@"%.2f", maxPrice - range / 4.0 * i]; }}
# pragma mark - 日期计算- (void)updateDateLabelsStartIndex:(NSInteger)startIndex endIndex:(NSInteger)endIndex { if (self.kLineData.count > 0) { if (startIndex < self.kLineData.count) self.startDateLabel.text = ((StockKLineModel *)self.kLineData[startIndex]).date; if (endIndex < self.kLineData.count) self.endDateLabel.text = ((StockKLineModel *)self.kLineData[endIndex]).date; }}
#pragma mark - k线选择- (void)addKSelectOptions { NSArray *titles = @[@"分时",@"日k", @"周k", @"月k", @"更多", @"设置"]; UIStackView *stackView = [[UIStackView alloc] init]; stackView.axis = UILayoutConstraintAxisHorizontal; stackView.distribution = UIStackViewDistributionFillEqually; stackView.alignment = UIStackViewAlignmentCenter; stackView.spacing = 5.0; stackView.translatesAutoresizingMaskIntoConstraints = NO; for (NSString *title in titles) { UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; [button setTitle:title forState:UIControlStateNormal]; button.titleLabel.font = [UIFont systemFontOfSize:14]; [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [stackView addArrangedSubview:button]; } [_kSelectContainer addSubview:stackView]; [NSLayoutConstraint activateConstraints:@[ [stackView.leadingAnchor constraintEqualToAnchor:_kSelectContainer.leadingAnchor constant:10], [stackView.trailingAnchor constraintEqualToAnchor:_kSelectContainer.trailingAnchor constant:-10], [stackView.topAnchor constraintEqualToAnchor:_kSelectContainer.topAnchor constant:15], [stackView.bottomAnchor constraintEqualToAnchor:_kSelectContainer.bottomAnchor constant:-15] ]];}
#pragma mark - 十字星- (void)setupDojiViews { // 竖线 self.crossVerticalLine = [[UIView alloc] init]; self.crossVerticalLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8]; self.crossVerticalLine.hidden = YES; self.crossVerticalLine.userInteractionEnabled = NO; // 不挡手势 [self.view addSubview:self.crossVerticalLine]; // 横线 self.crossHorizontalLine = [[UIView alloc] init]; self.crossHorizontalLine.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8]; self.crossHorizontalLine.hidden = YES; self.crossHorizontalLine.userInteractionEnabled = NO; [self.view addSubview:self.crossHorizontalLine]; // 价格 self.crossPriceLabel = [[UILabel alloc] init]; self.crossPriceLabel.backgroundColor = [UIColor systemBlueColor]; self.crossPriceLabel.textColor = [UIColor whiteColor]; self.crossPriceLabel.font = [UIFont systemFontOfSize:9]; self.crossPriceLabel.textAlignment = NSTextAlignmentCenter; self.crossPriceLabel.clipsToBounds = YES; self.crossPriceLabel.layer.cornerRadius = 2.0; self.crossPriceLabel.hidden = YES; [self.view addSubview:self.crossPriceLabel]; // 日期 self.crossDateLabel = [[UILabel alloc] init]; self.crossDateLabel.backgroundColor = [UIColor systemBlueColor]; self.crossDateLabel.textColor = [UIColor whiteColor]; self.crossDateLabel.font = [UIFont systemFontOfSize:9]; self.crossDateLabel.textAlignment = NSTextAlignmentCenter;// 居中对齐 //NSTextAlignmentJustified 两端对齐 self.crossDateLabel.clipsToBounds = YES; self.crossDateLabel.layer.cornerRadius = 2.0; self.crossDateLabel.hidden = YES; [self.view addSubview:self.crossDateLabel]; NSArray *targetViews = @[self.kLineScrollView, self.macdContainer, self.kdjContainer]; for (UIView *view in targetViews) { // 必须在循环里创建新的手势对象,因为一个手势只能绑定一个View UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; longPress.minimumPressDuration = 0.3; // 设置触发时间 [view addGestureRecognizer:longPress]; }}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { CGPoint touchPoint = [gesture locationInView:self.kLineScrollView]; CGPoint touchPointInView = [gesture locationInView:self.view]; CGFloat offsetX = self.kLineScrollView.contentOffset.x; NSInteger index = floor(touchPoint.x / kLineUnitWidth);// 向下取整 if (index < 0) index = 0; if (index >= self.kLineData.count) index = self.kLineData.count - 1; if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged) {// 手势开始/移动 self.isLongPressing = YES; self.crossVerticalLine.hidden = NO; self.crossHorizontalLine.hidden = NO; self.crossPriceLabel.hidden = NO; self.crossDateLabel.hidden = NO;// 显示十字星 // 更新竖线位置 CGFloat xCenterInScroll = kLineUnitWidth * 0.5 + kLineUnitWidth * index; //滚动视图x坐标 - 滚动偏移量 + 价格标签宽度 CGFloat xCenterInView = xCenterInScroll - offsetX + kPriceLabelAreaWidth; // 不要越界 if (xCenterInView < kPriceLabelAreaWidth || xCenterInView > self.view.bounds.size.width) return; CGFloat topY = self.kLineContainer.frame.origin.y;// k线容器在主视图的顶部y CGFloat bottomY = CGRectGetMaxY(self.otherContainer.frame);// 空容器的底部 self.crossVerticalLine.frame = CGRectMake(xCenterInView, topY, 0.5, bottomY - topY); CGFloat displayValue = 0.0; BOOL isTouchValid = NO; // 用于标记是否摸到了有效的图表区域 // 判断手指在哪个容器里 if (CGRectContainsPoint(self.kLineContainer.frame, touchPointInView)) {// CGRectContainsPoint(矩形区域, 点) 布尔: isTouchValid = YES; CGFloat relativeY = touchPointInView.y - self.kLineContainer.frame.origin.y; CGFloat height = self.kLineContainer.frame.size.height; displayValue = self.currentMaxPrice - (relativeY / height) * (self.currentMaxPrice - self.currentMinPrice); } else if (CGRectContainsPoint(self.macdContainer.frame, touchPointInView)) { isTouchValid = YES; CGFloat relativeY = touchPointInView.y - self.macdContainer.frame.origin.y; CGFloat height = self.macdContainer.frame.size.height; displayValue = self.macdMaxValue - (relativeY / height) * (self.macdMaxValue - self.macdMinValue); } else if (CGRectContainsPoint(self.kdjContainer.frame, touchPointInView)) { isTouchValid = YES; CGFloat relativeY = touchPointInView.y - self.kdjContainer.frame.origin.y; CGFloat height = self.kdjContainer.frame.size.height; displayValue = self.kdjMaxValue - (relativeY / height) * (self.kdjMaxValue - self.kdjMinValue); } // 只有触摸在图表内才更新横线 if (isTouchValid) { self.crossHorizontalLine.frame = CGRectMake(kPriceLabelAreaWidth, touchPointInView.y, self.view.bounds.size.width - kPriceLabelAreaWidth, 0.5); self.crossPriceLabel.text = [NSString stringWithFormat:@" %.2f ", displayValue]; [self.crossPriceLabel sizeToFit]; self.crossPriceLabel.center = CGPointMake(kPriceLabelAreaWidth / 2.0, touchPointInView.y); } StockKLineModel *model = self.kLineData[index]; self.crossDateLabel.text = [NSString stringWithFormat:@" %@ ", model.date]; [self.crossDateLabel sizeToFit]; CGFloat dateLabelY = CGRectGetMaxY(self.kLineContainer.frame); self.crossDateLabel.center = CGPointMake(xCenterInView, dateLabelY); [self updateLegendsWithIndex:index]; } else if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) {// 结束/取消 self.isLongPressing = NO; self.crossVerticalLine.hidden = YES; self.crossHorizontalLine.hidden = YES; self.crossPriceLabel.hidden = YES; self.crossDateLabel.hidden = YES; [self drawAllCharts]; }}
@end
|