├── .gitignore
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── BMKLine.xcscheme
│ └── BMKLineTests.xcscheme
├── Demo
└── Playground
│ ├── Playground.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Playground.xcscheme
│ └── Playground
│ ├── Info.plist
│ ├── Playground.entitlements
│ ├── Resource
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── background.colorset
│ │ │ └── Contents.json
│ │ ├── border.colorset
│ │ │ └── Contents.json
│ │ ├── green.colorset
│ │ │ └── Contents.json
│ │ ├── indicator1.colorset
│ │ │ └── Contents.json
│ │ ├── indicator2.colorset
│ │ │ └── Contents.json
│ │ ├── indicator3.colorset
│ │ │ └── Contents.json
│ │ └── red.colorset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ └── UIColor+Exentension.swift
│ └── Source
│ ├── Application
│ └── AppDelegate.swift
│ ├── Models.swift
│ ├── ViewController.swift
│ └── Websocket
│ ├── Model.swift
│ └── Websocket.swift
├── Images
└── Cover.png
├── LICENSE
├── Package.swift
├── README.md
├── README_CN.md
├── Sources
└── Stockee
│ ├── ChartRenders
│ ├── Auxiliary
│ │ ├── ExtremePriceIndicator.swift
│ │ ├── GridIndicator.swift
│ │ ├── LatestPriceIndicator.swift
│ │ ├── SelectedTimeIndicator.swift
│ │ ├── SelectedYIndicator.swift
│ │ ├── TimeAnnotation.swift
│ │ ├── View
│ │ │ └── IndicatorLabel.swift
│ │ └── YAxisAnnotation.swift
│ ├── Layers
│ │ ├── GridLayer.swift
│ │ ├── LineChartLayer.swift
│ │ ├── ShapeLayer.swift
│ │ └── TimeShareLayer.swift
│ ├── Main
│ │ ├── BOLLChart.swift
│ │ ├── CandlestickChart.swift
│ │ ├── EMAChart.swift
│ │ ├── MAChart.swift
│ │ ├── SARChart.swift
│ │ └── TimeShareChart.swift
│ └── Sub
│ │ ├── KDJChart.swift
│ │ ├── MACDChart.swift
│ │ ├── RSIChart.swift
│ │ └── VolumeChart.swift
│ ├── Core
│ ├── ChartRenderer.swift
│ ├── Configuration.swift
│ ├── ContextValues.swift
│ ├── ExtremePointRetrievableCollection.swift
│ ├── NumberFormatting.swift
│ ├── Quote.swift
│ ├── QuoteLayout.swift
│ ├── QuoteProcessing.swift
│ ├── ReadonlyOffsetArray.swift
│ └── RendererContext.swift
│ ├── IndicatorAlgorithm
│ ├── AnalysisAlgorithm.swift
│ ├── BollingerBands.swift
│ ├── ExponentialMovingAverage.swift
│ ├── Extensions
│ │ └── OffsetCollection+Indictaor.swift
│ ├── KDJAlgorithm.swift
│ ├── MACDAlgorithm.swift
│ ├── MovingAverage.swift
│ ├── Processor
│ │ └── IndicatorQuoteProcessor.swift
│ ├── RSAlgorithm.swift
│ ├── RSIAlgorithm.swift
│ └── SARAlgorithm.swift
│ ├── Utils
│ ├── Binding.swift
│ ├── CGMathExtensions.swift
│ ├── CGPath+Extensions.swift
│ ├── Delegate.swift
│ ├── Patch.swift
│ ├── PixelAlign.swift
│ └── Range+Extensions.swift
│ └── View
│ ├── Caption
│ └── CaptionView.swift
│ ├── ChartDescriptor.swift
│ ├── ChartGroup.swift
│ ├── ChartGroupBuilder.swift
│ ├── ChartRendererBuilder.swift
│ ├── ChartView.swift
│ ├── Indicator
│ └── SelectionIndicatorDrawer.swift
│ └── Interaction
│ ├── LongPressInteraction.swift
│ ├── SelectionResetInteraction.swift
│ ├── TapInteraction.swift
│ └── ZoomInteraction.swift
├── Stockee.podspec
├── Stockee.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ ├── IDETemplateMacros.plist
│ └── IDEWorkspaceChecks.plist
└── Tests
└── StockeeTests
├── Core
├── AnyChartRendererTests.swift
├── ContextValuesTests.swift
└── QuoteProcessingTests.swift
├── Indicators
├── AFWrapperTests.swift
├── BOLLTests.swift
├── EMATests.swift
├── MACDTests.swift
├── MovingAverageTests.swift
├── RSITests.swift
├── RSTests.swift
└── SARTests.swift
├── UtilTest
└── ReadonlyOffsetArrayTests.swift
└── Utils
├── Candle.swift
├── Processors.swift
├── Renderers.swift
└── Rounded.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | # Xcode
9 | *.pbxuser
10 | !default.pbxuser
11 | *.mode1v3
12 | !default.mode1v3
13 | *.mode2v3
14 | !default.mode2v3
15 | *.perspectivev3
16 | !default.perspectivev3
17 | xcuserdata/
18 | *.xccheckout
19 | profile
20 | *.moved-aside
21 | DerivedData
22 | *.hmap
23 | *.ipa
24 |
25 | # Bundler
26 | .bundle
27 | Demo/Pods/
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - vertical_parameter_alignment_on_call
3 |
4 | disabled_rules:
5 | - force_try
6 | - force_cast
7 | - nesting
8 | - block_based_kvo
9 | - opening_brace
10 | - no_fallthrough_only
11 | - for_where
12 | - unhandled_throwing_task
13 | - unneeded_synthesized_initializer
14 |
15 | analyzer_rules:
16 | - unused_declaration
17 | - unused_import
18 |
19 | excluded:
20 | - .build
21 | - "**/Package.swift"
22 | - "**/Generated"
23 |
24 |
25 | identifier_name:
26 | allowed_symbols:
27 | - "_"
28 | max_length:
29 | warning: 60
30 | error: 100
31 | min_length:
32 | warning: 2
33 | excluded:
34 | - x
35 | - y
36 | - z
37 |
38 | type_name:
39 | allowed_symbols:
40 | - "_"
41 | max_length:
42 | warning: 60
43 | error: 100
44 | min_length:
45 | warning: 2
46 |
47 | function_parameter_count:
48 | warning: 6
49 |
50 | vertical_whitespace:
51 | max_empty_lines: 2
52 |
53 | file_length:
54 | warning: 2000
55 | error: 3000
56 |
57 | line_length:
58 | warning: 1000
59 | error: 2000
60 |
61 | type_body_length:
62 | warning: 1000
63 | error: 1500
64 |
65 | function_body_length:
66 | warning: 300
67 | error: 500
68 |
69 | cyclomatic_complexity:
70 | warning: 40
71 | error: 50
72 |
73 | large_tuple:
74 | warning: 3
75 | error: 6
76 |
77 | custom_rules:
78 | comment_space:
79 | name: "Space After Comment"
80 | regex: '//\S'
81 | match_kinds:
82 | - comment
83 | message: "There should be a space after a comment delimiter."
84 | severity: warning
85 |
86 | inline_comment_spaces:
87 | name: "Spaces Before Inline Comment"
88 | regex: '\S ?//'
89 | match_kinds:
90 | - comment
91 | message: "There should be more than 2 spaces before an inline comment."
92 | severity: warning
93 | reporter: "xcode"
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/BMKLine.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/BMKLineTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
16 |
18 |
24 |
25 |
26 |
27 |
28 |
38 |
39 |
45 |
46 |
48 |
49 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground.xcodeproj/xcshareddata/xcschemes/Playground.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Playground.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xFF",
9 | "green" : "0xFF",
10 | "red" : "0xFF"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x20",
27 | "green" : "0x16",
28 | "red" : "0x14"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/border.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xEF",
9 | "green" : "0xEA",
10 | "red" : "0xE7"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x34",
27 | "green" : "0x27",
28 | "red" : "0x25"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/green.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x22",
9 | "green" : "0xBD",
10 | "red" : "0x44"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x3E",
27 | "green" : "0xB0",
28 | "red" : "0x58"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/indicator1.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xCC",
9 | "green" : "0xBB",
10 | "red" : "0xB2"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xCC",
27 | "green" : "0xBB",
28 | "red" : "0xB2"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/indicator2.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x75",
9 | "green" : "0xC3",
10 | "red" : "0xCC"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x75",
27 | "green" : "0xC3",
28 | "red" : "0xCC"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/indicator3.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xC0",
9 | "green" : "0x6D",
10 | "red" : "0x85"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0xC0",
27 | "green" : "0x6D",
28 | "red" : "0x85"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Assets.xcassets/red.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0x2F",
9 | "green" : "0x53",
10 | "red" : "0xFF"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x35",
27 | "green" : "0x52",
28 | "red" : "0xDE"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Resource/UIColor+Exentension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Exentension.swift
3 | // Playground
4 | //
5 | // Created by Octree on 2022/9/7.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | extension UIColor {
30 | enum Stockee {
31 | static var background: UIColor { UIColor(named: #function)! }
32 | static var red: UIColor { UIColor(named: #function)! }
33 | static var green: UIColor { UIColor(named: #function)! }
34 | static var border: UIColor { UIColor(named: #function)! }
35 | static var indicator1: UIColor { UIColor(named: #function)! }
36 | static var indicator2: UIColor { UIColor(named: #function)! }
37 | static var indicator3: UIColor { UIColor(named: #function)! }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Source/Application/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Playground
4 | //
5 | // Created by Octree on 2022/3/10.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | @main
30 | class AppDelegate: UIResponder, UIApplicationDelegate {
31 | var window: UIWindow?
32 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
33 | // Override point for customization after application launch.
34 | return true
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Source/Models.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Models.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/4/8.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import Foundation
28 |
29 | public struct ChartOption: OptionSet, Equatable, Sendable {
30 | public typealias RawValue = UInt
31 | public var rawValue: UInt
32 |
33 | public nonisolated(unsafe) static var ma: ChartOption = .init(rawValue: 1 << 0)
34 | public nonisolated(unsafe) static var ema: ChartOption = .init(rawValue: 1 << 1)
35 | public nonisolated(unsafe) static var boll: ChartOption = .init(rawValue: 1 << 2)
36 | public nonisolated(unsafe) static var sar: ChartOption = .init(rawValue: 1 << 3)
37 | public nonisolated(unsafe) static var vol: ChartOption = .init(rawValue: 1 << 4)
38 | public nonisolated(unsafe) static var kdj: ChartOption = .init(rawValue: 1 << 5)
39 | public nonisolated(unsafe) static var rsi: ChartOption = .init(rawValue: 1 << 6)
40 | public nonisolated(unsafe) static var macd: ChartOption = .init(rawValue: 1 << 7)
41 |
42 | public init(rawValue: UInt) {
43 | self.rawValue = rawValue
44 | }
45 | }
46 |
47 | public extension ChartOption {
48 | init?(name: String) {
49 | switch name.lowercased() {
50 | case "ma":
51 | self = .ma
52 | case "ema":
53 | self = .ema
54 | case "boll":
55 | self = .boll
56 | case "sar":
57 | self = .sar
58 | case "vol":
59 | self = .vol
60 | case "kdj":
61 | self = .kdj
62 | case "rsi":
63 | self = .rsi
64 | case "macd":
65 | self = .macd
66 | default:
67 | return nil
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Source/Websocket/Model.swift:
--------------------------------------------------------------------------------
1 | import Stockee
2 | import UIKit
3 |
4 | public struct SocketQuote: Quote {
5 | public var date: Date
6 | public var start: Int
7 | public var end: Int
8 | public var low: CGFloat
9 | public var high: CGFloat
10 | public var open: CGFloat
11 | public var close: CGFloat
12 | public var volume: CGFloat
13 | }
14 |
15 | extension SocketQuote: Decodable {
16 | enum CodingKeys: String, CodingKey {
17 | case start = "t"
18 | case end = "T"
19 | case open = "o"
20 | case close = "c"
21 | case high = "h"
22 | case low = "l"
23 | case volume = "v"
24 | }
25 |
26 | public init(from decoder: Decoder) throws {
27 | let container = try decoder.container(keyedBy: CodingKeys.self)
28 | start = try container.decode(Int.self, forKey: .start)
29 | end = try container.decode(Int.self, forKey: .end)
30 | date = Date(timeIntervalSince1970: CGFloat(start) / 1000)
31 | low = try Double(container.decode(String.self, forKey: .low))!
32 | high = try Double(container.decode(String.self, forKey: .high))!
33 | open = try Double(container.decode(String.self, forKey: .open))!
34 | close = try Double(container.decode(String.self, forKey: .close))!
35 | volume = try Double(container.decode(String.self, forKey: .volume))!
36 | }
37 |
38 | var candle: Candle {
39 | .init(date: date,
40 | start: start,
41 | end: end,
42 | low: low,
43 | high: high,
44 | open: open,
45 | close: close,
46 | volume: volume)
47 | }
48 | }
49 |
50 | public struct BNWrapper: Decodable {
51 | // swiftlint:disable identifier_name
52 | public var k: SocketQuote
53 | // swiftlint:enable identifier_name
54 | }
55 |
56 | public struct BNQuote: Quote {
57 | public var date: Date
58 | public var start: Int
59 | public var end: Int
60 | public var low: CGFloat
61 | public var high: CGFloat
62 | public var open: CGFloat
63 | public var close: CGFloat
64 | public var volume: CGFloat
65 | }
66 |
67 | extension BNQuote: Decodable {
68 | public init(from decoder: Decoder) throws {
69 | var container = try decoder.unkeyedContainer()
70 | start = try container.decode(Int.self)
71 | date = Date(timeIntervalSince1970: CGFloat(start) / 1000)
72 | open = try Double(container.decode(String.self))!
73 | high = try Double(container.decode(String.self))!
74 | low = try Double(container.decode(String.self))!
75 | close = try Double(container.decode(String.self))!
76 | volume = try Double(container.decode(String.self))!
77 | end = try container.decode(Int.self)
78 | }
79 |
80 | var candle: Candle {
81 | .init(date: date,
82 | start: start,
83 | end: end,
84 | low: low,
85 | high: high,
86 | open: open,
87 | close: close,
88 | volume: volume)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Demo/Playground/Playground/Source/Websocket/Websocket.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Websocket.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/19.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import Foundation
28 |
29 | extension URLSessionWebSocketTask.Message {
30 | private var data: Data {
31 | switch self {
32 | case let .data(data):
33 | return data
34 | case let .string(string):
35 | return string.data(using: .utf8)!
36 | @unknown default:
37 | fatalError()
38 | }
39 | }
40 |
41 | func decode(_ type: E.Type) throws -> E {
42 | try JSONDecoder().decode(E.self, from: data)
43 | }
44 | }
45 |
46 | enum BinanceAPI {
47 | static func getKLine(symbol: String,
48 | interval: String,
49 | endTime: Int? = nil,
50 | limit: Int = 500) async throws -> [Candle] {
51 | var query = "symbol=\(symbol)&interval=\(interval)&limit=\(limit)"
52 | if let endTime = endTime {
53 | query += "&endTime=\(endTime)"
54 | }
55 | let url = URL(string: "https://api.binance.com/api/v3/klines?\(query)")!
56 | let request = URLRequest(url: url)
57 | let (data, _) = try await URLSession(configuration: .default).data(for: request)
58 | return try JSONDecoder().decode([BNQuote].self, from: data).map { $0.candle }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Images/Cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/octree/Stockee/be7547f2520992573141f1c0fd5c938661259299/Images/Cover.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Octree
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Stockee",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | .library(
13 | name: "Stockee",
14 | targets: ["Stockee"]
15 | )
16 | ],
17 | dependencies: [
18 | ],
19 | targets: [
20 | .target(
21 | name: "Stockee",
22 | dependencies: []
23 | ),
24 | .testTarget(
25 | name: "StockeeTests",
26 | dependencies: ["Stockee"]
27 | )
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stockee
2 |
3 | Highly customizable lightweight k-line chart written in swift.
4 |
5 | 
6 | ## Installation
7 |
8 | ### Swift Package Manager
9 |
10 | - File > Swift Packages > Add Package Dependency
11 | - Add https://github.com/octree/Stockee.git
12 | - Select "Up to Next Major" with "1.3.1"
13 |
14 | ### Cocoapods
15 |
16 | ```bash
17 | pod 'Stockee'
18 | ```
19 |
20 | ## Docs
21 | [中文文档](./README_CN.md)
22 | ### Sample
23 |
24 | Use a declarative syntax to build your k-line chart like `SwiftUI`. It's simpler and easier to read.
25 |
26 | ```swift
27 | chartView.descriptor = ChartDescriptor(spacing: 0) {
28 | ChartGroup(height: 200) {
29 | GridIndicator(lineWidth: 1 / UIScreen.main.scale, color: .Stockee.border)
30 | YAxisAnnotation()
31 | CandlestickChart()
32 | MAChart(configuration: .init(period: 5, color: .yellow))
33 | MAChart(configuration: .init(period: 10, color: .teal))
34 | MAChart(configuration: .init(period: 20, color: .purple))
35 | }
36 |
37 | ChartGroup(height: 18) {
38 | TimeAnnotation(dateFormat: "HH:mm")
39 | SelectedTimeIndicator()
40 | }
41 | }
42 | ```
43 |
44 | ## License
45 | **Stockee** is available under the MIT license. See the LICENSE file for more info.
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # Stockee
2 |
3 | K 线图
4 |
5 | ## How To Use
6 |
7 | > 可以直接参考 Playground 里的 `ViewController.swift`,有比较完整的 Demo,使用 Binance API 实现了一个丑陋的 K 线图,支持配置指标、加载更多、更新最新报价等功能
8 |
9 | ### 类
10 |
11 | * **ChartView**:渲染 Chart 的容器,接收数据和 配置的 `ChartDescriptor` 来渲染图表;
12 | * **ChartRenderer**:特定的图表渲染器,例如蜡烛图、MA 指标图等;
13 | * **ChartGroup**:包含一组 ChartRenderer,例如:主图就是一个 Group,每个副图也是一个 Group;
14 | * **CharDescriptor**:包含一组 ChartGroup,用于渲染多组图表;
15 | * **Configuration**:基础配置信息,例如,蜡烛图宽度、上升趋势的颜色、标注的字体等。
16 |
17 | ### 教程
18 |
19 | 1. 定义报价 `struct`
20 |
21 | ```swift
22 | // 需要实现 Quote 协议,提供基础的报价信息
23 | struct Candle: Quote {
24 | // ....
25 | }
26 | ```
27 |
28 | 2. 创建图表容器
29 |
30 | ```swift
31 | // 这里使用范型
32 | // 因为既不想使用协议存储数据,动态分发对性能会有所影响
33 | // 也不想自定义一堆结构体,在内存中占用额外的内存,所以,后面很多类都是范型的
34 | let chartView: ChartView = ChartView(frame: .zero)
35 | /// 因为是使用的 UIScrollView 作为容器,所以,可以使用 ScrollView 的一些特性,例如在四周留出更多的空白空间
36 | chartView.contentInset = .init(top: 16, left: 16, bottom: 16, right: 100)
37 | ```
38 |
39 | 3. 配置信息
40 |
41 | ```swift
42 | chartView.configuration.barWidth = 4
43 | chartView.configuration.style.upColor = .red
44 | // 或者
45 | chartView.configuration = newConfiguration
46 | ```
47 |
48 | * 包含一些影响整个 Chart 渲染的配置信息,例如:每个 Candle 的宽度,上升趋势的颜色等, 更多配置信息可以参考 `Configuration` 这个结构体。
49 |
50 | 4. 配置 `ChartDescriptor`
51 |
52 | * 这里配置各种指标图表或者辅助图表,会直接影响到 K 线图的排版,层级等;
53 | * 实现了一个简单的 `DSL`,可以使用类似 `SwiftUI` 的方式组织图表;
54 | * `ChartView` 需要一个 `ChartDescriptor` 来了解该如何渲染多组图表。
55 | * `ChartDescriptor` 是一组 `ChartGroup` 的集合 Chart 会自上而下的渲染多个 Group;
56 | * 一个 `ChartGroup` 是一组 `ChartRenderer` 的集合,一个 group 的 chart 会渲染在同一个区域,会沿着 Z 轴自下而上的排列,意味着,`ChartRenderer` 在数组中的 Index 越大,它的 `zPosition` 也就越大;
57 | * `ChartRenderer` 是一个 `protocol`,用于渲染一个特定的图表(例如:蜡烛图、EMA 指标、MACD 指标等),具体参考 [目前支持的 ChartRenderer](#目前支持的-chartrenderer) ;
58 |
59 | ```swift
60 | chartView.descriptor = ChartDescriptor(spacing: 0) {
61 | // 主图,高度为 200, 然后配置默认的 Formatter,用于格式化各种指标
62 | ChartGroup(height: 200, preferredFormatter: .defaultPrice(), chartPadding: (2, 4)) {
63 | // 绘制网格
64 | GridIndicator(lineWidth: 1/UIScreen.main.scale, color: UIColor(white: 0.8, alpha: 1))
65 | // 绘制 Y 轴坐标
66 | YAxisAnnotation()
67 | if isTimeShare {
68 | // 如果为分时图,则绘制分时图
69 | TimeShareChart(color: .magenta)
70 | } else {
71 | // 绘制蜡烛图
72 | CandlestickChart()
73 | if chartOptions.contains(.ma) {
74 | // 如果包含 ma,则绘制 MA
75 | // MA6
76 | MAChart(configuration: .init(period: 6, color: .systemBrown))
77 | // MA12
78 | MAChart(configuration: .init(period: 12, color: .systemPink))
79 | }
80 | }
81 | // ... 绘制更多指标
82 | }
83 |
84 | // X 轴
85 | ChartGroup(height: 18) {
86 | // 绘制日期
87 | TimeAnnotation(dateFormat: "HH:mm")
88 | // 绘制当前选择的日期
89 | SelectedTimeIndicator()
90 | }
91 |
92 | if chartOptions.contains(.vol) {
93 | // 成交量图表
94 | ChartGroup(height: 50, preferredFormatter: .volume) {
95 | // 绘制网格
96 | GridIndicator(lineWidth: 1/UIScreen.main.scale, color: UIColor(white: 0.8, alpha: 1))
97 | // 同时也要绘制 Y 轴坐标
98 | YAxisAnnotation(formatter: .volume)
99 | VolumeChart(minHeight: 1)
100 | SelectedYIndicator()
101 | }
102 | }
103 | }
104 | ```
105 |
106 | * 这里展示了一部分配置信息,更完整的可以参考 `ViewController.swift`;
107 | * 建立每个 `ChartGroup` 可以用一个计算属性进行配置;
108 | * 然后使用一个计算属性 `descriptor` 来组合这些 `groups`;
109 |
110 | 5. 数据
111 | ```swift
112 | // 重新加载数据,图表会重新渲染,不保留滚动 Offset
113 | chartView.reloadData(data)
114 | // 在当前数据之前,添加新的数据,会保留滚动 Offset
115 | chartView.prepend(data)
116 | // 替换最后一个 Quote,会保留滚动 Offset
117 | chartView.replaceLast(quote)
118 | ```
119 |
120 |
121 |
122 | > 可以把 `CharView` 看作是一个可以渲染多组图表的一个容器,其本身并不关心每个图表(例如:MA 指标)是如何处数据和渲染图表的,ChartView 会在数据发生改变后,把数据交付给每个图表的数据处理器进行处理,然后把数据存储在 `ContextValues` 中,然后在渲染阶段,把布局信息和 ContextValue 交给具体的 Chart 进行渲染。
123 |
124 |
125 |
126 | ### 目前支持的 ChartRenderer
127 |
128 | #### 主图
129 |
130 | - `CandlestickChart`: 蜡烛图
131 | - `TimeShareChart`:分时图
132 | - `SARChart`:SAR 指标
133 | - `MAChart`:MA 指标
134 | - `EMAChart`:EMA 指标
135 | - `BOLLChart`:BOLL 指标
136 |
137 | #### 副图
138 |
139 | * `VolumeChart`:成交量
140 | * `KDJChart`:KDJ 指标
141 | * `MACDChart`:MACD 指标
142 | * `RSIChart`:RSI 指标
143 |
144 | #### 辅助
145 |
146 | * `ExtremePriceIndicator`:最低、最高成交价
147 | * `GridIndicator`:网格
148 | * `LatestPriceIndicator`:最新成交价
149 | * `SelectedTimeIndicator`: 当前选择的 Quote 的日期
150 | * `SelectedYIndicator`:当前选择的 Y 轴的值
151 | * `TimeAnnotation`:X 轴标注,也就是日期
152 | * `YAxisAnnotation`:Y 轴的标注
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Auxiliary/GridIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridIndicator.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/22.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// 用于绘制 ChartGroup 的网格
30 | public class GridIndicator: ChartRenderer {
31 | public typealias Input = Input
32 | public typealias QuoteProcessor = NopeQuoteProcessor
33 | private let layer = GridLayer()
34 |
35 | /// 创建网格
36 | /// - Parameters:
37 | /// - lineWidth: 网格线的粗细,默认为 1px
38 | /// - color: 网格线的颜色
39 | public init(lineWidth: CGFloat = 1 / UIScreen.main.scale, color: UIColor) {
40 | layer.lineWidth = lineWidth
41 | layer.strokeColor = color.cgColor
42 | }
43 |
44 | public func updateZPosition(_ position: CGFloat) {
45 | layer.zPosition = position
46 | }
47 |
48 | public func setup(in view: ChartView) {
49 | view.layer.addSublayer(layer)
50 | }
51 |
52 | public func render(in view: ChartView, context: Context) {
53 | layer.draw(in: context)
54 | }
55 |
56 | public func tearDown(in view: ChartView) {
57 | layer.removeFromSuperlayer()
58 | }
59 |
60 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? {
61 | nil
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Auxiliary/LatestPriceIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LatestPriceIndicator.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/4/6.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// 用于绘制最新成交价格
30 | public class LatestPriceIndicator: ChartRenderer {
31 | public typealias Input = Input
32 | public typealias QuoteProcessor = NopeQuoteProcessor
33 | private var label: IndicatorLabel = .init()
34 | private var layer: ShapeLayer = {
35 | let layer = ShapeLayer()
36 | layer.lineWidth = 1
37 | layer.fillColor = UIColor.clear.cgColor
38 | layer.lineDashPattern = [2, 2]
39 | return layer
40 | }()
41 |
42 | private var height: CGFloat
43 | private var minWidth: CGFloat
44 | private var maxWidth: CGFloat
45 |
46 | /// 最新成交价的指示器
47 | /// - Parameters:
48 | /// - height: Label 高度
49 | /// - minWidth: 最小宽度
50 | /// - maxWidth: Label 最大宽度
51 | /// - textColor: 文字颜色,默认白色
52 | public init(height: CGFloat = 12,
53 | minWidth: CGFloat = 36,
54 | maxWidth: CGFloat = 80,
55 | textColor: UIColor = .white) {
56 | self.height = height
57 | self.minWidth = minWidth
58 | self.maxWidth = maxWidth
59 | label.label.textColor = textColor
60 | }
61 |
62 | public func updateZPosition(_ position: CGFloat) {
63 | layer.zPosition = position
64 | label.layer.zPosition = position
65 | }
66 |
67 | public func setup(in view: ChartView) {
68 | view.layer.addSublayer(layer)
69 | view.addSubview(label)
70 | }
71 |
72 | public func render(in view: ChartView, context: Context) {
73 | guard let last = context.data.last else {
74 | layer.isHidden = true
75 | label.isHidden = true
76 | return
77 | }
78 | layer.isHidden = false
79 | label.isHidden = false
80 | let style = context.configuration.style
81 | let color = last.close > last.open ? style.upColor : style.downColor
82 | layer.strokeColor = color.cgColor
83 | label.shapeLayer.fillColor = color.cgColor
84 | label.label.font = context.configuration.captionFont
85 | label.label.text = context.preferredFormatter.format(last.close)
86 |
87 | let y = context.yOffset(for: last.close)
88 | var size = label.sizeThatFits(.init(width: maxWidth, height: height))
89 | size.width = min(maxWidth, max(size.width, minWidth))
90 | size.height = height
91 | let maxX = view.contentOffset.x + view.frame.width
92 | let minY = context.contentRect.minY
93 | let maxY = context.groupContentRect.maxY
94 | var frame = CGRect(origin: CGPoint(x: maxX - size.width, y: y - height / 2),
95 | size: size)
96 | frame.origin.y = min(max(frame.origin.y, minY), maxY - height)
97 | label.frame = frame
98 | let path = CGMutablePath()
99 | let midY = frame.midY
100 | path.move(to: .init(x: view.contentOffset.x, y: midY))
101 | path.addLine(to: .init(x: maxX, y: midY))
102 | layer.path = path
103 | }
104 |
105 | public func tearDown(in view: ChartView) {
106 | layer.removeFromSuperlayer()
107 | label.removeFromSuperview()
108 | }
109 |
110 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? {
111 | nil
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Auxiliary/SelectedTimeIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectedTimeIndicator.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/31.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// 用于显示选择的 Quote 的日期
30 | public class SelectedTimeIndicator: ChartRenderer {
31 | public typealias Input = Input
32 | public typealias QuoteProcessor = NopeQuoteProcessor
33 | /// 背景颜色
34 | public var backgroundColor: UIColor {
35 | didSet {
36 | label.backgroundColor = backgroundColor
37 | }
38 | }
39 |
40 | /// 文字颜色
41 | public var textColor: UIColor {
42 | didSet {
43 | label.textColor = textColor
44 | }
45 | }
46 |
47 | /// 日期格式,默认为:yyyy-MM-dd HH:mm
48 | public var formatter: DateFormatter = {
49 | let formatter = DateFormatter()
50 | formatter.dateFormat = "yyyy-MM-dd HH:mm"
51 | return formatter
52 | }()
53 |
54 | /// 水平 Padding,|PaddingH|Text|PaddingH|
55 | public var paddingH: CGFloat = 2
56 |
57 | private let label = UILabel()
58 |
59 | public init(backgroundColor: UIColor = .black, textColor: UIColor = .white) {
60 | self.backgroundColor = backgroundColor
61 | self.textColor = textColor
62 | label.textAlignment = .center
63 | label.backgroundColor = backgroundColor
64 | label.textColor = textColor
65 | label.isHidden = true
66 | }
67 |
68 | public func updateZPosition(_ position: CGFloat) {
69 | label.layer.zPosition = .greatestFiniteMagnitude
70 | }
71 |
72 | public func setup(in view: ChartView) {
73 | view.addSubview(label)
74 | }
75 |
76 | public func render(in view: ChartView, context: Context) {
77 | defer { label.isHidden = context.selectedIndex == nil }
78 | guard let selectedIndex = context.selectedIndex else {
79 | return
80 | }
81 | label.font = context.configuration.captionFont
82 | let midX = context.layout.quoteMidX(at: selectedIndex)
83 | let date = context.data[selectedIndex].date
84 | label.text = formatter.string(from: date)
85 | label.sizeToFit()
86 | var frame = label.frame
87 | let width = frame.width + paddingH * 2
88 | let minX = view.contentOffset.x
89 | let maxX = minX + view.frame.width - width
90 | let x = min(maxX, max(minX, midX - width / 2))
91 | frame.origin = .init(x: x, y: context.contentRect.minY)
92 | frame.size.width = width
93 | frame.size.height = context.contentRect.height
94 | label.frame = frame
95 | }
96 |
97 | public func tearDown(in view: ChartView) {
98 | label.removeFromSuperview()
99 | }
100 |
101 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? {
102 | nil
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Auxiliary/SelectedYIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectedYIndicator.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/4/8.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// 用于绘制当前滑动选择的 Y 轴的值
30 | public class SelectedYIndicator: ChartRenderer {
31 | public typealias Input = Input
32 | public typealias QuoteProcessor = NopeQuoteProcessor
33 | private var label: IndicatorLabel = .init()
34 | private var height: CGFloat
35 | private var minWidth: CGFloat
36 | private var maxWidth: CGFloat
37 |
38 | /// 正在选择的 Y 轴的指示器
39 | /// - Parameters:
40 | /// - height: 高度
41 | /// - minWidth: 最小宽度
42 | /// - maxWidth: 最大宽度
43 | /// - background: 背景颜色
44 | /// - textColor: 文字颜色
45 | public init(height: CGFloat = 12,
46 | minWidth: CGFloat = 36,
47 | maxWidth: CGFloat = 80,
48 | background: UIColor = .red,
49 | textColor: UIColor = .white)
50 | {
51 | self.height = height
52 | self.minWidth = minWidth
53 | self.maxWidth = maxWidth
54 | label.shapeLayer.fillColor = background.cgColor
55 | label.label.textColor = textColor
56 | }
57 |
58 | public func updateZPosition(_ position: CGFloat) {
59 | label.layer.zPosition = .greatestFiniteMagnitude
60 | }
61 |
62 | public func setup(in view: ChartView) {
63 | view.addSubview(label)
64 | }
65 |
66 | public func render(in view: ChartView, context: Context) {
67 | let minY = context.groupContentRect.minY
68 | let maxY = context.groupContentRect.maxY
69 | guard context.extremePoint.max - context.extremePoint.min > 0,
70 | let position = context.indicatorPosition,
71 | position.y >= minY, position.y <= maxY
72 | else {
73 | label.isHidden = true
74 | return
75 | }
76 | let y = position.y
77 | label.isHidden = false
78 | label.label.font = context.configuration.captionFont
79 | let minX = view.contentOffset.x
80 | let maxX = minX + view.frame.width
81 | let midX = (minX + maxX) / 2
82 | let value = context.value(forY: y)
83 | label.label.text = context.preferredFormatter.format(value)
84 | var size = label.sizeThatFits(.init(width: maxWidth, height: height))
85 | size.height = height
86 | size.width = min(maxWidth, max(size.width, minWidth))
87 | if position.x > midX {
88 | label.triangleDirection = .left
89 | label.frame = CGRect(origin: CGPoint(x: maxX - size.width, y: y - size.height / 2),
90 | size: size)
91 | } else {
92 | label.triangleDirection = .right
93 | label.frame = CGRect(origin: CGPoint(x: minX, y: y - size.height / 2),
94 | size: size)
95 | }
96 | }
97 |
98 | public func tearDown(in view: ChartView) {
99 | label.removeFromSuperview()
100 | }
101 |
102 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? {
103 | nil
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Auxiliary/View/IndicatorLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IndicatorLabel.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/4/8.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | public class IndicatorLabel: UIView {
30 | public enum Direction {
31 | case left
32 | case right
33 | }
34 |
35 | public var triangleWidth: CGFloat = 6 {
36 | didSet {
37 | updateLayer()
38 | updateLabelFrame()
39 | invalidateIntrinsicContentSize()
40 | }
41 | }
42 |
43 | public let shapeLayer: ShapeLayer = .init()
44 | public var triangleDirection: Direction = .left {
45 | didSet {
46 | label.textAlignment = triangleDirection == .left ? .left : .right
47 | updateLayer()
48 | updateLabelFrame()
49 | }
50 | }
51 |
52 | override public var frame: CGRect {
53 | didSet {
54 | updateLayer()
55 | updateLabelFrame()
56 | }
57 | }
58 |
59 | public let label: UILabel = .init()
60 | private var left: NSLayoutConstraint!
61 | private var right: NSLayoutConstraint!
62 |
63 | override public init(frame: CGRect) {
64 | super.init(frame: frame)
65 | configureHierarchy()
66 | }
67 |
68 | public required init?(coder: NSCoder) {
69 | super.init(coder: coder)
70 | configureHierarchy()
71 | }
72 |
73 | private func configureHierarchy() {
74 | layer.addSublayer(shapeLayer)
75 | label.numberOfLines = 1
76 | label.lineBreakMode = .byTruncatingMiddle
77 | addSubview(label)
78 | }
79 |
80 | private func updateLayer() {
81 | let path = CGMutablePath()
82 | if triangleDirection == .left {
83 | path.move(to: CGPoint(x: 0, y: bounds.midY))
84 | path.addLine(to: CGPoint(x: triangleWidth, y: 0))
85 | path.addLine(to: CGPoint(x: bounds.width, y: 0))
86 | path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
87 | path.addLine(to: CGPoint(x: triangleWidth, y: bounds.height))
88 | } else {
89 | path.move(to: CGPoint(x: bounds.width, y: bounds.midY))
90 | path.addLine(to: CGPoint(x: bounds.width - triangleWidth, y: 0))
91 | path.addLine(to: CGPoint(x: 0, y: 0))
92 | path.addLine(to: CGPoint(x: 0, y: bounds.height))
93 | path.addLine(to: CGPoint(x: bounds.width - triangleWidth, y: bounds.height))
94 | }
95 | path.closeSubpath()
96 | shapeLayer.path = path
97 | }
98 |
99 | private func updateLabelFrame() {
100 | var frame = bounds
101 | frame.size.width -= triangleWidth
102 | if triangleDirection == .left {
103 | frame.origin.x += triangleWidth
104 | }
105 | label.frame = frame
106 | }
107 |
108 | override public func sizeThatFits(_ size: CGSize) -> CGSize {
109 | var size = size
110 | if size.width != .greatestFiniteMagnitude {
111 | size.width -= triangleWidth
112 | }
113 | var expected = label.sizeThatFits(size)
114 | expected.width += triangleWidth
115 | return expected
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Layers/GridLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridLayer.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/22.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | final class GridLayer: ShapeLayer {
30 | override public init() {
31 | super.init()
32 | setup()
33 | }
34 |
35 | override public init(layer: Any) {
36 | super.init()
37 | setup()
38 | }
39 |
40 | public required init?(coder: NSCoder) {
41 | super.init(coder: coder)
42 | setup()
43 | }
44 |
45 | private func setup() {
46 | fillColor = UIColor.clear.cgColor
47 | }
48 | }
49 |
50 | extension GridLayer {
51 | @MainActor
52 | func draw(in context: RendererContext) {
53 | let width = context.layout.view.frame.width
54 | let height = context.groupContentRect.height
55 |
56 | let minX = context.layout.view.contentOffset.x
57 | let minY = context.groupContentRect.minY
58 | let maxY = context.groupContentRect.maxY
59 | let maxX = width + minX
60 | let path = CGMutablePath()
61 | let vcount = context.layout.verticalGridCount(heigt: height)
62 | let vInterval = height / CGFloat(vcount)
63 | var y = minY
64 | (0 ... vcount).forEach { _ in
65 | path.addHLine(minX: minX, maxX: maxX, y: y)
66 | y += vInterval
67 | }
68 |
69 | let hcount = context.layout.horizontalGridCount(width: width)
70 | let hInterval = width / CGFloat(hcount)
71 | var x = minX
72 | (1 ..< hcount).forEach { _ in
73 | x += hInterval
74 | path.addVLine(minY: minY, maxY: maxY, x: x)
75 | }
76 | self.path = path
77 | }
78 | }
79 |
80 | extension CGMutablePath {
81 | func addHLine(minX: CGFloat, maxX: CGFloat, y: CGFloat) {
82 | move(to: .init(x: minX, y: y))
83 | addLine(to: .init(x: maxX, y: y))
84 | }
85 |
86 | func addVLine(minY: CGFloat, maxY: CGFloat, x: CGFloat) {
87 | move(to: .init(x: x, y: minY))
88 | addLine(to: .init(x: x, y: maxY))
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Layers/ShapeLayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShapeLayer.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/18.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | public class ShapeLayer: CAShapeLayer {
30 | override public func action(forKey event: String) -> CAAction? {
31 | nil
32 | }
33 |
34 | override public class func defaultAction(forKey event: String) -> CAAction? {
35 | nil
36 | }
37 |
38 | public func clear() { path = nil }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Main/EMAChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EMAChart.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/21.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// EMA 图表配置信息
30 | public struct EMAConfiguration: ContextKey {
31 | public typealias Value = ReadonlyOffsetArray
32 |
33 | /// 观察周期
34 | public var period: Int
35 | /// 颜色
36 | public var color: UIColor
37 |
38 | /// 创建 EMA 图表配置信息
39 | /// - Parameters:
40 | /// - period: 观察周期
41 | /// - color: 颜色
42 | public init(period: Int, color: UIColor) {
43 | self.period = period
44 | self.color = color
45 | }
46 | }
47 |
48 | /// EMA 指标绘制
49 | public class EMAChart: ChartRenderer {
50 | public typealias Input = Input
51 | public typealias QuoteProcessor = IndicatorQuoteProcessor>
52 | public let quoteProcessor: IndicatorQuoteProcessor>?
53 |
54 | private let configuration: EMAConfiguration
55 | private let layer = LineChartLayer()
56 |
57 | /// EMA 指标图表
58 | /// - Parameter configuration: 配置信息
59 | public init(configuration: EMAConfiguration) {
60 | self.configuration = configuration
61 | self.quoteProcessor = .init(id: configuration,
62 | algorithm: .init(period: configuration.period))
63 | }
64 |
65 | public func updateZPosition(_ position: CGFloat) {
66 | layer.zPosition = position
67 | }
68 |
69 | public func setup(in view: ChartView) {
70 | view.layer.addSublayer(layer)
71 | }
72 |
73 | public func render(in view: ChartView, context: Context) {
74 | guard let values = context.contextValues[configuration] else {
75 | layer.clear()
76 | return
77 | }
78 | layer.update(with: context,
79 | indicatorValues: values,
80 | color: configuration.color)
81 | }
82 |
83 | public func tearDown(in view: ChartView) {
84 | layer.removeFromSuperlayer()
85 | }
86 |
87 | public func captions(quoteIndex: Int, context: Context) -> [NSAttributedString] {
88 | let value = context.contextValues[configuration]?[quoteIndex].flatMap {
89 | context.preferredFormatter.format($0)
90 | } ?? "--"
91 | let text = "EMA\(configuration.period):\(value)"
92 | return [
93 | .init(string: text, attributes: [
94 | .font: context.configuration.captionFont,
95 | .foregroundColor: configuration.color
96 | ])
97 | ]
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Main/MAChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MAChart.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/19.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// MA 配置信息
30 | public struct MAConfiguration: ContextKey {
31 | public typealias Value = ReadonlyOffsetArray
32 | /// 观察周期
33 | public var period: Int
34 | /// 颜色
35 | public var color: UIColor
36 |
37 | /// 创建 MA 配置信息
38 | /// - Parameters:
39 | /// - period: 观察周期
40 | /// - color: 颜色
41 | public init(period: Int, color: UIColor) {
42 | self.period = period
43 | self.color = color
44 | }
45 | }
46 |
47 | /// MA 指标图表
48 | public class MAChart: ChartRenderer {
49 | public typealias Input = Input
50 | public typealias QuoteProcessor = IndicatorQuoteProcessor>
51 | public let quoteProcessor: IndicatorQuoteProcessor>?
52 |
53 | private let configuration: MAConfiguration
54 | private let layer = LineChartLayer()
55 |
56 | /// 创建 MA 指标图表
57 | /// - Parameter configuration: 配置信息
58 | public init(configuration: MAConfiguration) {
59 | self.configuration = configuration
60 | quoteProcessor = .init(id: configuration,
61 | algorithm: .init(period: configuration.period))
62 | }
63 |
64 | public func updateZPosition(_ position: CGFloat) {
65 | layer.zPosition = position
66 | }
67 |
68 | public func setup(in view: ChartView) {
69 | view.layer.addSublayer(layer)
70 | }
71 |
72 | public func render(in view: ChartView, context: Context) {
73 | guard let values = context.contextValues[configuration] else {
74 | layer.clear()
75 | return
76 | }
77 | layer.update(with: context,
78 | indicatorValues: values,
79 | color: configuration.color)
80 | }
81 |
82 | public func tearDown(in view: ChartView) {
83 | layer.removeFromSuperlayer()
84 | }
85 |
86 | public func captions(quoteIndex: Int, context: Context) -> [NSAttributedString] {
87 | let value = context.contextValues[configuration]?[quoteIndex].flatMap {
88 | context.preferredFormatter.format($0)
89 | } ?? "--"
90 | let text = "MA\(configuration.period):\(value)"
91 | return [
92 | .init(string: text, attributes: [
93 | .font: context.configuration.captionFont,
94 | .foregroundColor: configuration.color
95 | ])
96 | ]
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Main/TimeShareChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeShareChart.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/31.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// 分时图
30 | public final class TimeShareChart: ChartRenderer {
31 | public typealias Input = Input
32 | public typealias QuoteProcessor = NopeQuoteProcessor
33 | /// 颜色
34 | public var color: UIColor {
35 | didSet {
36 | updateGradientColor()
37 | }
38 | }
39 |
40 | private var timeShareLayer: TimeShareLayer = .init()
41 |
42 | /// 创建蜡烛图图表
43 | /// - Parameter color: 颜色
44 | public init(color: UIColor) {
45 | self.color = color
46 | updateGradientColor()
47 | }
48 |
49 | public func setup(in view: ChartView) {
50 | view.layer.addSublayer(timeShareLayer)
51 | }
52 |
53 | public func updateZPosition(_ position: CGFloat) {
54 | timeShareLayer.zPosition = position
55 | }
56 |
57 | public func render(in view: ChartView, context: Context) {
58 | timeShareLayer.update(with: context)
59 | }
60 |
61 | public func tearDown(in view: ChartView) {
62 | timeShareLayer.removeFromSuperlayer()
63 | }
64 |
65 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? {
66 | guard let data = contextValues[QuoteContextKey.self],
67 | data[visibleRange].count > 0
68 | else {
69 | return nil
70 | }
71 | let min = data[visibleRange].map { $0.low }.min()!
72 | let max = data[visibleRange].map { $0.high }.max()!
73 | return (min, max)
74 | }
75 |
76 | // MARK: - Private Methods
77 |
78 | private func updateGradientColor() {
79 | timeShareLayer.update(color: color)
80 | }
81 |
82 | public func captions(quoteIndex: Int, context: Context) -> [NSAttributedString] {
83 | let quote = context.data[quoteIndex]
84 | let text = context.preferredFormatter.format(quote.close)
85 | let color = quote.close > quote.open ? context.configuration.upColor : context.configuration.downColor
86 | return [
87 | .init(string: "Price:\(text)",
88 | attributes: [
89 | .font: context.configuration.captionFont,
90 | .foregroundColor: color
91 | ])
92 | ]
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/Stockee/ChartRenders/Sub/RSIChart.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RSIChart.swift
3 | // Stockee
4 | //
5 | // Created by octree on 2022/3/21.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | /// RSI 配置信息
30 | public struct RSIConfiguration: ContextKey {
31 | public typealias Value = ReadonlyOffsetArray
32 | /// 观察周期
33 | public var period: Int
34 | /// 折线图颜色
35 | public var color: UIColor
36 |
37 | /// 创建一个 RSI 配置信息
38 | /// - Parameters:
39 | /// - period: 观察周期
40 | /// - color: 折线图颜色
41 | public init(period: Int, color: UIColor) {
42 | self.period = period
43 | self.color = color
44 | }
45 | }
46 |
47 | /// RSI 图表
48 | public class RSIChart: ChartRenderer {
49 | public typealias Input = Input
50 | public typealias QuoteProcessor = IndicatorQuoteProcessor>
51 | public let quoteProcessor: QuoteProcessor?
52 | private let configuration: RSIConfiguration
53 | private let layer = LineChartLayer()
54 |
55 | /// 创建 RSI 图表
56 | /// - Parameter configuration: 配置信息
57 | public init(configuration: RSIConfiguration) {
58 | self.configuration = configuration
59 | self.quoteProcessor = .init(id: configuration,
60 | algorithm: .init(period: configuration.period))
61 | }
62 |
63 | public func updateZPosition(_ position: CGFloat) {
64 | layer.zPosition = position
65 | }
66 |
67 | public func setup(in view: ChartView) {
68 | view.layer.addSublayer(layer)
69 | }
70 |
71 | public func render(in view: ChartView, context: Context) {
72 | guard let values = context.contextValues[configuration] else {
73 | clear()
74 | return
75 | }
76 | layer.lineWidth = context.configuration.lineWidth
77 | layer.update(with: context, indicatorValues: values, color: configuration.color)
78 | }
79 |
80 | private func clear() {
81 | layer.clear()
82 | }
83 |
84 | public func tearDown(in view: ChartView) {
85 | layer.removeFromSuperlayer()
86 | }
87 |
88 | public func captions(quoteIndex: Int, context: Context) -> [NSAttributedString] {
89 | let value = context.contextValues[configuration]?[quoteIndex]
90 | let font = context.configuration.captionFont
91 | let text = value.flatMap {
92 | context.preferredFormatter.format($0)
93 | } ?? "--"
94 | return [
95 | .init(string: "RSI(\(configuration.period)):\(text)",
96 | attributes: [
97 | .font: font,
98 | .foregroundColor: configuration.color
99 | ])
100 | ]
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/Stockee/Core/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configuration.swift
3 | // Stockee
4 | //
5 | // Created by Octree on 2022/3/17.
6 | //
7 | // Copyright (c) 2022 Octree
8 | //
9 | // Permission is hereby granted, free of charge, to any person obtaining a copy
10 | // of this software and associated documentation files (the "Software"), to deal
11 | // in the Software without restriction, including without limitation the rights
12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | // copies of the Software, and to permit persons to whom the Software is
14 | // furnished to do so, subject to the following conditions:
15 | //
16 | // The above copyright notice and this permission notice shall be included in
17 | // all copies or substantial portions of the Software.
18 | //
19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 | // THE SOFTWARE.
26 |
27 | import UIKit
28 |
29 | public struct Style {
30 | /// 一些小注释的颜色
31 | public var captionColor: UIColor = .gray
32 | /// 上升趋势的颜色
33 | public var upColor: UIColor = .red
34 | /// 下降趋势的颜色
35 | public var downColor: UIColor = .green
36 | /// 选择状态下,十字线的颜色
37 | public var selectionIndicatorLineColor: UIColor = .lightGray
38 | /// 选择状态下,十字线交点的颜色
39 | public var selectionIndicatorPointColor: UIColor = .orange
40 |
41 | public init() {}
42 | }
43 |
44 | /// 配置信息
45 | @dynamicMemberLookup
46 | public struct Configuration {
47 | /// 每个报价在图表中的宽度,默认 6
48 | public var barWidth: CGFloat = 6
49 | /// 报价之间的间隔,默认 1
50 | public var spacing: CGFloat = 2
51 | /// 影线宽度,默认 1
52 | public var shadowLineWidth: CGFloat = 1
53 | /// 折线图的宽度,默认 1
54 | public var lineWidth: CGFloat = 1
55 | /// 默认为 9
56 | public var captionFont: UIFont = .systemFont(ofSize: 9, weight: .light)
57 | /// 标注说明文字的 padding
58 | public var captionPadding: UIEdgeInsets
59 | /// 标注说明文字的横向间距和行间距
60 | public var captionSpacing: (h: CGFloat, v: CGFloat)
61 | /// 网格的最小间距
62 | public var gridInterval: (h: CGFloat, v: CGFloat)
63 | /// 颜色样式
64 | public var style: Style = .init()
65 |
66 | public init(barWidth: CGFloat = 6,
67 | spacing: CGFloat = 1,
68 | shadowLineWidth: CGFloat = 1,
69 | lineWidth: CGFloat = 1,
70 | captionFont: UIFont = .systemFont(ofSize: 8),
71 | captionPadding: UIEdgeInsets = .init(top: 4, left: 8, bottom: 4, right: 8),
72 | captionSpacing: (h: CGFloat, v: CGFloat) = (4, 2),
73 | gridInterval: (h: CGFloat, v: CGFloat) = (120, 50),
74 | style: Style = .init())
75 | {
76 | self.barWidth = barWidth
77 | self.spacing = spacing
78 | self.shadowLineWidth = shadowLineWidth
79 | self.lineWidth = lineWidth
80 | self.captionFont = captionFont
81 | self.captionPadding = captionPadding
82 | self.captionSpacing = captionSpacing
83 | self.gridInterval = gridInterval
84 | self.style = style
85 | }
86 | }
87 |
88 | public extension Configuration {
89 | subscript(dynamicMember keyPath: WritableKeyPath