├── .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 | ![Cover](./Images/Cover.png) 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) -> V { 90 | _read { 91 | yield style[keyPath: keyPath] 92 | } 93 | _modify { 94 | yield &style[keyPath: keyPath] 95 | } 96 | } 97 | 98 | subscript(dynamicMember keyPath: KeyPath) -> V { 99 | style[keyPath: keyPath] 100 | } 101 | } 102 | 103 | extension Configuration { 104 | func scaled(_ scale: CGFloat) -> Configuration { 105 | .init(barWidth: barWidth * scale, 106 | spacing: spacing, 107 | shadowLineWidth: shadowLineWidth, 108 | lineWidth: lineWidth, 109 | captionFont: captionFont, 110 | captionPadding: captionPadding, 111 | captionSpacing: captionSpacing, 112 | gridInterval: gridInterval, 113 | style: style) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/ContextValues.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndicatorKey.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 Foundation 28 | 29 | /// 用于在 ``ContextValues`` 中接收值的 key 30 | public protocol ContextKey: Hashable { 31 | associatedtype Value 32 | } 33 | 34 | /// 用于在 ``ContextValues`` 中接收交易信息 35 | public enum QuoteContextKey: ContextKey { 36 | public typealias Value = [Input] 37 | } 38 | 39 | /// 用于向 ``ChartRenderer`` 传输一组数据 40 | public struct ContextValues { 41 | private var values: [AnyHashable: Any] = [:] 42 | public subscript(key: Key.Type) -> Key.Value? { 43 | get { 44 | values[ObjectIdentifier(key)] as? Key.Value 45 | } 46 | _modify { 47 | var temp = values[ObjectIdentifier(key)] as? Key.Value 48 | yield &temp 49 | values[ObjectIdentifier(key)] = temp 50 | } 51 | } 52 | 53 | public subscript(key: Key) -> Key.Value? { 54 | get { 55 | values[key] as? Key.Value 56 | } 57 | _modify { 58 | var temp = values[key] as? Key.Value 59 | yield &temp 60 | values[key] = temp 61 | } 62 | } 63 | 64 | subscript(id: AnyHashable) -> Any? { 65 | _read { 66 | yield values[id] 67 | } 68 | _modify { 69 | yield &values[id] 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/ExtremePointRetrievableCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtremePointRetrievableCollection.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 CoreGraphics 28 | import Foundation 29 | 30 | /// 可以获取最大最小值的集合类型 31 | public protocol ExtremePointRetrievableCollection { 32 | func extremePoint(in range: Range) -> (min: CGFloat, max: CGFloat)? 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/NumberFormatting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFormatting.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/24. 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 protocol NumberFormatting { 30 | func format(_ value: CGFloat) -> String 31 | } 32 | 33 | /// 默认的用来格式化价格的 Formatter 34 | public struct DefaultPriceFormatter: NumberFormatting { 35 | /// 整数和小数位数有意义的数字的个数限制 36 | public var significantDigits: Int 37 | /// 小数位最小的长度 38 | public var minimumFractionDigits: Int 39 | 40 | /// 创建一个默认的价格的 Formatter 41 | /// - Parameters: 42 | /// - significantDigits: 整数和小数位数有意义的数字的个数限制 43 | /// - minimumFractionDigits: 小数位最小的长度 44 | public init(significantDigits: Int = 4, minimumFractionDigits: Int = 2) { 45 | self.significantDigits = significantDigits 46 | self.minimumFractionDigits = minimumFractionDigits 47 | } 48 | 49 | public func format(_ value: CGFloat) -> String { 50 | var integer = Int(value) 51 | var digits = 0 52 | while integer != 0 { 53 | digits += 1 54 | integer /= 10 55 | } 56 | let formatter = NumberFormatter() 57 | formatter.maximumSignificantDigits = max(significantDigits, digits + minimumFractionDigits) 58 | formatter.numberStyle = .decimal 59 | return formatter.string(from: value as NSNumber)! 60 | } 61 | } 62 | 63 | /// 让 NumberFormatter 遵守 ``NumberFormatting`` 协议 64 | extension NumberFormatter: NumberFormatting { 65 | public func format(_ value: CGFloat) -> String { 66 | string(from: value as NSNumber)! 67 | } 68 | } 69 | 70 | /// 默认的用来格式化交易量的 Formatter 71 | public struct DefaultVolumeFormatter: NumberFormatting, Sendable { 72 | private static let units = ["K", "M", "B", "T"] 73 | public func format(_ value: CGFloat) -> String { 74 | var unit = "" 75 | var value = value 76 | var units = Self.units 77 | while value >= 1000, units.count > 0 { 78 | value /= 1000 79 | unit = units.removeFirst() 80 | } 81 | let formatter = DefaultPriceFormatter(significantDigits: 2, minimumFractionDigits: 1) 82 | return "\(formatter.format(value))\(unit)" 83 | } 84 | } 85 | 86 | public extension NumberFormatting where Self == DefaultPriceFormatter { 87 | static func defaultPrice(significantDigits: Int = 4, minimumFractionDigits: Int = 2) -> DefaultPriceFormatter { 88 | .init(significantDigits: significantDigits, 89 | minimumFractionDigits: minimumFractionDigits) 90 | } 91 | } 92 | 93 | public extension NumberFormatting where Self == DefaultVolumeFormatter { 94 | static var volume: DefaultVolumeFormatter { .init() } 95 | } 96 | 97 | public extension NumberFormatting where Self == NumberFormatter { 98 | static func maximumFractionDigits(_ limit: Int) -> NumberFormatter { 99 | let formatter = NumberFormatter() 100 | formatter.maximumFractionDigits = limit 101 | formatter.numberStyle = .decimal 102 | return formatter 103 | } 104 | 105 | static func maximumSignificantDigits(_ limit: Int) -> NumberFormatter { 106 | let formatter = NumberFormatter() 107 | formatter.maximumSignificantDigits = limit 108 | formatter.numberStyle = .decimal 109 | return formatter 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/Quote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CandleStickData.swift 3 | // Stockee 4 | // 5 | // Created by Octree on 2022/3/14. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 报价 31 | public protocol Quote { 32 | /// 日期 33 | var date: Date { get } 34 | /// 最低价 35 | var low: CGFloat { get } 36 | /// 最高价 37 | var high: CGFloat { get } 38 | /// 开盘价 39 | var open: CGFloat { get } 40 | /// 收盘价 41 | var close: CGFloat { get } 42 | /// 交易量 43 | var volume: CGFloat { get } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/QuoteLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteLayout.swift 3 | // 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 | /// 用于计算每个 Bar 横向位置的类 30 | @MainActor 31 | public struct QuoteLayout { 32 | unowned var view: ChartView 33 | private var configuration: Configuration { view.scaledConfiguration } 34 | 35 | init(_ view: ChartView) { 36 | self.view = view 37 | } 38 | 39 | public func contentWidth(for data: [Input]) -> CGFloat { 40 | let width = view.frame.width - view.contentInset.left - view.contentInset.right 41 | guard data.count > 0 else { return width } 42 | let contentWidth = (configuration.barWidth + configuration.spacing) * CGFloat(data.count) - configuration.spacing 43 | return max(width, contentWidth) 44 | } 45 | 46 | public func visibleRange() -> Range { 47 | guard view.data.count > 0 else { return .none } 48 | let minX = view.contentOffset.x 49 | let maxX = minX + view.frame.width 50 | let thunkWidth = configuration.barWidth + configuration.spacing 51 | let minIndex = min(view.data.count, max(0, Int(minX / thunkWidth))) 52 | let maxIndex = max(0, min(view.data.count, Int(maxX / thunkWidth) + 1)) 53 | return minIndex ..< maxIndex 54 | } 55 | 56 | public func contentRectToDraw(visibleRange: Range, y: CGFloat, height: CGFloat) -> CGRect { 57 | guard !visibleRange.isEmpty else { return CGRect(x: 0, y: y, width: 0, height: height) } 58 | let thunkWidth = configuration.barWidth + configuration.spacing 59 | let minX = CGFloat(visibleRange.startIndex) * thunkWidth 60 | let maxX = CGFloat(visibleRange.endIndex - 1) * thunkWidth 61 | return CGRect(x: minX, y: y, width: maxX - minX, height: height) 62 | } 63 | } 64 | 65 | public extension QuoteLayout { 66 | var barWidth: CGFloat { 67 | configuration.barWidth 68 | } 69 | 70 | var spacing: CGFloat { 71 | configuration.spacing 72 | } 73 | 74 | func quoteMinX(at index: Int) -> CGFloat { 75 | let thunkWidth = configuration.barWidth + configuration.spacing 76 | return thunkWidth * CGFloat(index) 77 | } 78 | 79 | func quoteMidX(at index: Int) -> CGFloat { 80 | let thunkWidth = configuration.barWidth + configuration.spacing 81 | return thunkWidth * CGFloat(index) + configuration.barWidth / 2 82 | } 83 | 84 | func quoteMaxX(at index: Int) -> CGFloat { 85 | let thunkWidth = configuration.barWidth + configuration.spacing 86 | return thunkWidth * CGFloat(index) + configuration.barWidth 87 | } 88 | 89 | func quoteIndex(at point: CGPoint) -> Int? { 90 | let thunkWidth = configuration.barWidth + configuration.spacing 91 | let index = Int(point.x / thunkWidth) 92 | guard index >= 0, index < view.data.count else { return nil } 93 | return index 94 | } 95 | 96 | func horizontalGridCount(width: CGFloat) -> Int { 97 | max(1, Int(width / configuration.gridInterval.h)) 98 | } 99 | 100 | func verticalGridCount(heigt: CGFloat) -> Int { 101 | max(1, Int(heigt / configuration.gridInterval.v)) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/QuoteProcessing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteProcessing.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 | /// 用于处理 30 | public protocol QuoteProcessing { 31 | associatedtype Input: Quote 32 | associatedtype Output 33 | associatedtype Key: ContextKey where Key.Value == Output 34 | 35 | /// 可以使用自定义 identifier,如果返回 nil,则会使用 Key.self 作为 identifier 36 | var identifier: Key? { get } 37 | func process(_ data: [Input]) -> Output 38 | } 39 | 40 | extension QuoteProcessing { 41 | public var identifier: Key? { nil } 42 | 43 | @inlinable 44 | var absoluteID: AnyHashable { 45 | if let id = identifier { 46 | return id 47 | } else { 48 | return ObjectIdentifier(Key.self) 49 | } 50 | } 51 | 52 | func process(_ data: [Input], writeTo contextValues: inout ContextValues) { 53 | contextValues[Key.self] = process(data) 54 | } 55 | } 56 | 57 | public struct AnyQuoteProcessor { 58 | private(set) var identifier: AnyHashable 59 | private var _process: ([Input]) -> Any 60 | 61 | public init(_ processor: P) where P.Input == Input { 62 | _process = { processor.process($0) } 63 | identifier = processor.absoluteID 64 | } 65 | 66 | func process(_ data: [Input], writeTo contextValues: inout ContextValues) { 67 | contextValues[identifier] = _process(data) 68 | } 69 | 70 | func clearValues(in contextValues: inout ContextValues) { 71 | contextValues[identifier] = nil 72 | } 73 | } 74 | 75 | extension QuoteProcessing { 76 | var typedErased: AnyQuoteProcessor { AnyQuoteProcessor(self) } 77 | } 78 | 79 | public enum NopeContextKey: ContextKey { 80 | public typealias Value = Void 81 | } 82 | 83 | // 一个什么都不做的处理器 84 | public struct NopeQuoteProcessor: QuoteProcessing { 85 | public typealias Input = Input 86 | public typealias Output = Void 87 | public typealias Key = NopeContextKey 88 | public func process(_ data: [Input]) {} 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/ReadonlyOffsetArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadonlyOffsetArray.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 CoreGraphics 28 | import Foundation 29 | 30 | public struct ReadonlyOffsetArray { 31 | public let offset: Int 32 | var storage: [Element] 33 | 34 | /// 创建一个下标被偏移的数组 35 | /// 36 | /// realIndex = index - offset 37 | /// - Parameters: 38 | /// - storage: 数据 39 | /// - offset: 下标偏移量 40 | init(_ storage: [Element], offset: Int) { 41 | self.storage = storage 42 | self.offset = offset 43 | } 44 | 45 | public subscript(index: Int) -> Element? { 46 | let realIndex = index - offset 47 | guard realIndex >= 0, realIndex < storage.count else { 48 | return nil 49 | } 50 | return storage[realIndex] 51 | } 52 | } 53 | 54 | extension ReadonlyOffsetArray { 55 | func realRange(for range: Range) -> Range { 56 | ((range.lowerBound - offset)..<(range.upperBound - offset)) 57 | .clamped(to: storage.startIndex..) -> (ArraySlice, Range) { 63 | let realRange = realRange(for: range) 64 | return (storage[realRange], realRange.startIndex + offset ..< realRange.endIndex + offset) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/Stockee/Core/RendererContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RendererContext.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 | /// 提供给渲染器的上下文信息 30 | public struct RendererContext { 31 | /// 行情信息 32 | public var data: [Input] 33 | /// 配置信息 34 | public var configuration: Configuration 35 | /// 布局信息 36 | public var layout: QuoteLayout 37 | /// 需要渲染的图表在 ChartView 中的区域 38 | public var contentRect: CGRect 39 | /// 当前 Group 在 ChartView 中的区域,包含 Caption 40 | public var groupContentRect: CGRect 41 | /// 需要渲染数据的区间 42 | public var visibleRange: Range 43 | /// 其他的上下文信息,例如:计算之后的各种指标信息就会放在这里 44 | public var contextValues: ContextValues 45 | /// 极值 cache 46 | internal var extremePointCache: [(min: CGFloat, max: CGFloat)?] = [] 47 | /// 极值 48 | public var extremePoint: (min: CGFloat, max: CGFloat) = (0, 1) 49 | /// 当前 Group 中,标注的高度 50 | public var captionHeight: CGFloat = .zero 51 | /// 当前选择的 Quote 的下标 52 | public var selectedIndex: Int? 53 | /// 当前触摸显示的点 54 | public var indicatorPosition: CGPoint? 55 | /// 当前 ChartGroup 的 formatter 56 | public var preferredFormatter: NumberFormatting = NumberFormatter() 57 | } 58 | 59 | public extension RendererContext { 60 | subscript(_ type: Key.Type) -> Key.Value? { 61 | contextValues[type] 62 | } 63 | } 64 | 65 | extension RendererContext { 66 | /// 获取某个 Y 坐标的值在图表中 y 的偏移量 67 | /// - Parameter value: Y 值 68 | /// - Returns: 偏移量 69 | func yOffset(for value: CGFloat) -> CGFloat { 70 | let height = contentRect.height 71 | let minY = contentRect.minY 72 | let peak = extremePoint.max - extremePoint.min 73 | return height - height * (value - extremePoint.min) / peak + minY 74 | } 75 | 76 | func value(forY y: CGFloat) -> CGFloat { 77 | let peak = extremePoint.max - extremePoint.min 78 | let height = contentRect.height 79 | let maxY = contentRect.maxY 80 | return (maxY - y) * peak / height + extremePoint.min 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/AnalysisAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnalysisAlgorithm.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/14. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 该协议,定义了一个技术分析算法应该包含的方法,以及输出类型 31 | public protocol AnalysisAlgorithm { 32 | associatedtype Input: Quote 33 | associatedtype Output 34 | func process(_ data: [Input]) -> Output 35 | } 36 | 37 | /// 一个抹去类型的算法 38 | public struct AnyAnalysisAlgorithm { 39 | var _process: ([Input]) -> Any 40 | 41 | /// 创建一个抹去类型的算法实例 42 | public init(_ algorithm: A) where A.Input == Input { 43 | _process = { algorithm.process($0) } 44 | } 45 | } 46 | 47 | public extension AnalysisAlgorithm { 48 | /// Wraps this algorithm with a type eraser. 49 | var typeErased: AnyAnalysisAlgorithm { 50 | AnyAnalysisAlgorithm(self) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/BollingerBands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BollingerBands.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/14. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// BOLL 指标 31 | public struct BOLLIndicator: Equatable { 32 | public var lower: CGFloat 33 | public var middle: CGFloat 34 | public var upper: CGFloat 35 | 36 | public init(lower: CGFloat, middle: CGFloat, upper: CGFloat) { 37 | self.lower = lower 38 | self.middle = middle 39 | self.upper = upper 40 | } 41 | } 42 | 43 | /// 用于计算 BOLL 指标的算法 44 | public struct BollingerBandsAlgorithm: AnalysisAlgorithm { 45 | public typealias Output = [BOLLIndicator] 46 | /// BOLL 计算周期 47 | public let period: Int 48 | /// 标准差的倍数 49 | public let standardDeviationMultiplier: CGFloat 50 | 51 | /// 创建一个计算 BOLL 指标的算法 52 | /// - Parameters: 53 | /// - period: 计算周期,默认为 20 54 | /// - standardDeviation: 标准差的倍数,默认为 2 倍 55 | public init(period: Int = 20, standardDeviationMultiplier: CGFloat = 2) { 56 | self.period = period 57 | self.standardDeviationMultiplier = standardDeviationMultiplier 58 | } 59 | 60 | /// 处理蜡烛图数据,生成 BOLL 指标数据 61 | /// - Parameter data: 蜡烛图数据 62 | /// - Returns: BOLL 数据 63 | public func process(_ data: [Input]) -> [BOLLIndicator] { 64 | guard data.count >= period else { return [] } 65 | let maList = MovingAverageAlgorithm(period: period).process(data) 66 | var result: [BOLLIndicator] = [] 67 | 68 | func standardDeviation(at index: Int, ma: CGFloat) -> CGFloat { 69 | var deviation: CGFloat = 0 70 | for idx in (index + 1 - period)...index { 71 | deviation += pow(data[idx].close - ma, 2) 72 | } 73 | deviation /= CGFloat(period) 74 | return pow(deviation, 0.5) 75 | } 76 | 77 | for index in (period - 1) ..< data.count { 78 | let ma = maList[index - period + 1] 79 | let std = standardDeviation(at: index, ma: ma) 80 | let upper = ma + std * standardDeviationMultiplier 81 | let lower = ma - std * standardDeviationMultiplier 82 | result.append(.init(lower: lower, middle: ma, upper: upper)) 83 | } 84 | return result 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/ExponentialMovingAverage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExponentialMovingAverage.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/14. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 用于计算移 EMA 的算法 31 | /// ``` 32 | /// Multiplier: (2 / (Time periods + 1) ) 33 | /// EMA: { Close - EMA(previous day) } * multiplier + EMA(previous day). 34 | /// ``` 35 | /// 36 | /// * OKX 的 EMA,第一天取当天的收盘价,之后进行加权求值; 37 | /// * 看了一些其他的解释,第 N 天使用前 N 天的 SMA,第 N + 1 天之后才进行 EMA 求值; 38 | /// * 币安采用的后者,前 N 日是没有 EMA 数据的。 39 | /// * 这里采用第二种方式计算 40 | /// 所以返回的数据数量为:max(0, data.count - peroid + 1) 41 | public struct ExponentialMovingAverageAlgorithm: AnalysisAlgorithm { 42 | public typealias Output = [CGFloat] 43 | /// EMA 计算周期 44 | public var period: Int { 45 | calculator.period 46 | } 47 | 48 | private let calculator: EMACaculator 49 | 50 | /// 创建一个 计算 EMA 指标的算法 51 | /// - Parameter period: MA 周期 52 | public init(period: Int) { 53 | self.calculator = .init(period: period) 54 | } 55 | 56 | /// 处理蜡烛图数据,生成 MA 数据 57 | /// - Parameter data: 蜡烛图数据 58 | /// - Returns: EMA 数据 59 | public func process(_ data: [Input]) -> [CGFloat] { 60 | calculator.process(data.map { $0.close }) 61 | } 62 | } 63 | 64 | struct EMACaculator { 65 | public let period: Int 66 | private let multiplier: CGFloat 67 | 68 | init(period: Int) { 69 | self.period = period 70 | self.multiplier = 2 / CGFloat(period + 1) 71 | } 72 | 73 | func process(_ data: [CGFloat]) -> [CGFloat] { 74 | guard data.count >= period else { return [] } 75 | var prev: CGFloat = (data[0 ..< period].reduce(0) { $0 + $1 }) / CGFloat(period) 76 | var result: [CGFloat] = [prev] 77 | for value in data.dropFirst(period) { 78 | let ema = (value - prev) * multiplier + prev 79 | result.append(ema) 80 | prev = ema 81 | } 82 | return result 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/Extensions/OffsetCollection+Indictaor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OffsetCollection+Indictaor.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 CoreGraphics 28 | import Foundation 29 | 30 | public protocol ExtremePointValue { 31 | var min: CGFloat { get } 32 | var max: CGFloat { get } 33 | } 34 | 35 | extension BOLLIndicator: ExtremePointValue { 36 | @inline(__always) 37 | public var min: CGFloat { 38 | Swift.min(lower, middle, upper) 39 | } 40 | 41 | @inline(__always) 42 | public var max: CGFloat { 43 | Swift.max(lower, middle, upper) 44 | } 45 | } 46 | 47 | extension KDJIndicator: ExtremePointValue { 48 | @inline(__always) 49 | public var min: CGFloat { 50 | Swift.min(k, d, j) 51 | } 52 | 53 | @inline(__always) 54 | public var max: CGFloat { 55 | Swift.max(k, d, j) 56 | } 57 | } 58 | 59 | extension MACDIndicator: ExtremePointValue { 60 | @inline(__always) 61 | public var min: CGFloat { 62 | [diff, dea, histogram].compactMap { $0 }.min()! 63 | } 64 | 65 | @inline(__always) 66 | public var max: CGFloat { 67 | [diff, dea, histogram].compactMap { $0 }.max()! 68 | } 69 | } 70 | 71 | extension SARIndicator: ExtremePointValue { 72 | @inline(__always) 73 | public var min: CGFloat { sar } 74 | @inline(__always) 75 | public var max: CGFloat { sar } 76 | } 77 | 78 | extension CGFloat: ExtremePointValue { 79 | @inline(__always) 80 | public var min: CGFloat { self } 81 | @inline(__always) 82 | public var max: CGFloat { self } 83 | } 84 | 85 | extension Double: ExtremePointValue { 86 | @inline(__always) 87 | public var min: CGFloat { CGFloat(self) } 88 | @inline(__always) 89 | public var max: CGFloat { CGFloat(self) } 90 | } 91 | 92 | extension Int: ExtremePointValue { 93 | @inline(__always) 94 | public var min: CGFloat { CGFloat(self) } 95 | @inline(__always) 96 | public var max: CGFloat { CGFloat(self) } 97 | } 98 | 99 | extension ReadonlyOffsetArray: ExtremePointRetrievableCollection where Element: ExtremePointValue { 100 | public func extremePoint(in range: Range) -> (min: CGFloat, max: CGFloat)? { 101 | let realRange = ((range.lowerBound - offset)..<(range.upperBound - offset)) 102 | .clamped(to: storage.startIndex.. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// KDJ 指标 31 | public struct KDJIndicator: Equatable { 32 | // swiftlint:disable identifier_name 33 | public var k: CGFloat 34 | public var d: CGFloat 35 | public var j: CGFloat 36 | // swiftlint:enable identifier_name 37 | } 38 | 39 | /// 用于计算 KDJ 指标的算法 40 | /// 目前 kPeriod 和 dPeriod 只支持 3、3 41 | public struct KDJAlgorithm: AnalysisAlgorithm { 42 | public typealias Output = [KDJIndicator] 43 | /// 观察周期 44 | public let period: Int 45 | /// K 值平滑周期 46 | public let kPeriod: Int 47 | /// D 值平滑周期 48 | public let dPeriod: Int 49 | 50 | /// 创建一个计算 KDJ 指标的算法 51 | /// - Parameters: 52 | /// - period: 观察周期,默认为 9 53 | /// - kPeriod: K 值平滑周期,默认为 3 54 | /// - dPeriod: D 值平滑周期,默认为 3 55 | public init(period: Int = 9, kPeriod: Int = 3, dPeriod: Int = 3) { 56 | self.period = period 57 | self.kPeriod = kPeriod 58 | self.dPeriod = dPeriod 59 | } 60 | 61 | /// 处理蜡烛图数据,生成 KDJ 数据 62 | /// - Parameter data: 蜡烛图数据 63 | /// - Returns: KDJ 数据 64 | public func process(_ data: [Input]) -> [KDJIndicator] { 65 | guard data.count >= period else { return [] } 66 | var result: [KDJIndicator] = [] 67 | var prev: (k: CGFloat, d: CGFloat) = (50, 50) 68 | for index in (period - 1) ..< data.count { 69 | let (low, high) = lowHighPrice(of: data, in: (index - period + 1) ..< (index + 1)) 70 | let rsv: CGFloat 71 | if high == low { 72 | rsv = 0 73 | } else { 74 | rsv = (data[index].close - low) / (high - low) * 100 75 | } 76 | // swiftlint:disable identifier_name 77 | let k = 1 / 3 * rsv + 2 / 3 * prev.k 78 | let d = 1 / 3 * k + 2 / 3 * prev.d 79 | let j = 3 * k - 2 * d 80 | // swiftlint:enable identifier_name 81 | result.append(.init(k: k, d: d, j: j)) 82 | prev = (k, d) 83 | } 84 | return result 85 | } 86 | 87 | private func lowHighPrice(of data: [Input], in range: Range) -> (low: CGFloat, high: CGFloat) { 88 | assert(!range.isEmpty) 89 | var low = data[range.startIndex].low 90 | var high = data[range.startIndex].high 91 | data[range].forEach { 92 | if $0.high > high { 93 | high = $0.high 94 | } 95 | if $0.low < low { 96 | low = $0.low 97 | } 98 | } 99 | return (low, high) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/MACDAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MACDAlgorithm.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/15. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// MACD 指标 31 | public struct MACDIndicator: Equatable { 32 | /// 12 日 EMA 与 26 日 EMA 的差 33 | public var diff: CGFloat 34 | /// DIFF 的 EMA(9) 35 | public var dea: CGFloat? 36 | /// 2 * (DIFF - DEA) 37 | public var histogram: CGFloat? 38 | 39 | public init(diff: CGFloat, dea: CGFloat? = nil, histogram: CGFloat? = nil) { 40 | self.diff = diff 41 | self.dea = dea 42 | self.histogram = histogram 43 | } 44 | } 45 | 46 | /// 计算 MACD 指标的算法,以 MACD(12, 26, 9) 为例, 47 | /// 短线使用 EMA(12),长线使用 EMA(26),DEA 计算使用 EMA(9) 48 | /// 数据数量为 max(0, data.count - 26 + 1) 49 | /// 50 | /// ``` 51 | /// MACD Line: (12-day EMA - 26-day EMA) 52 | /// Signal Line: 9-day EMA of MACD Line 53 | /// MACD Histogram: MACD Line - Signal Line 54 | /// ``` 55 | public struct MACDAlgorithm: AnalysisAlgorithm { 56 | public let shorterPeroid: Int 57 | public let longerPeroid: Int 58 | public let deaPeroid: Int 59 | 60 | public typealias Output = [MACDIndicator] 61 | 62 | public init(shorterPeroid: Int = 12, longerPeroid: Int = 26, deaPeroid: Int = 9) { 63 | assert(shorterPeroid < longerPeroid) 64 | self.shorterPeroid = shorterPeroid 65 | self.longerPeroid = longerPeroid 66 | self.deaPeroid = deaPeroid 67 | } 68 | 69 | public func process(_ data: [Input]) -> [MACDIndicator] { 70 | guard data.count >= longerPeroid else { return [] } 71 | // EMA(close, 12) 72 | let ema12 = ExponentialMovingAverageAlgorithm(period: shorterPeroid) 73 | .process(data) 74 | .dropFirst(longerPeroid - shorterPeroid) 75 | // EMA(close, 26) 76 | let ema26 = ExponentialMovingAverageAlgorithm(period: longerPeroid).process(data) 77 | assert(ema12.count == ema26.count) 78 | let diff = zip(ema12, ema26).map(-) 79 | let deaEMA = EMACaculator(period: deaPeroid).process(diff) 80 | var result: [MACDIndicator] = [] 81 | assert(diff.dropFirst(deaPeroid - 1).count == deaEMA.count) 82 | (0.. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 用于计算移动平均线指标的算法,这里采用收盘价格进行计算 31 | /// 返回数据的数量为 max(0, data.count - peroid + 1) 32 | /// 例如:5 日 MA,如果数据是 [1, 2, 3],则返回值为 [] 33 | /// 如果数据为 [1, 2, 3 , 4, 5], 则返回值为 [ 3 ] 34 | public struct MovingAverageAlgorithm: AnalysisAlgorithm { 35 | public typealias Output = [CGFloat] 36 | /// MA 计算周期 37 | public let period: Int 38 | 39 | /// 创建一个 计算 MA 指标的算法 40 | /// - Parameter period: MA 周期 41 | public init(period: Int) { 42 | self.period = period 43 | } 44 | 45 | /// 处理蜡烛图数据,生成 MA 数据 46 | /// - Parameter data: 蜡烛图数据 47 | /// - Returns: MA 数据 48 | public func process(_ data: [Input]) -> [CGFloat] { 49 | guard data.count >= period else { return [] } 50 | var result: [CGFloat] = [] 51 | var sum = data[0.. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 把指标算法转换成 Renderer 可以使用的数据处理器 31 | public struct IndicatorQuoteProcessor: QuoteProcessing 32 | where Element: ExtremePointValue, Key: ContextKey, Key.Value == ReadonlyOffsetArray, A: AnalysisAlgorithm, A.Output == [Element], A.Input == Input 33 | { 34 | public typealias Input = Input 35 | public typealias Key = Key 36 | public typealias Output = ReadonlyOffsetArray 37 | public var identifier: Key? 38 | private var algorithm: A 39 | 40 | /// 根据指标算法,生成一个数据处理器 41 | /// - Parameters: 42 | /// - id: 唯一的 id,如果传入 nil,则使用 Key.self 作为 id 43 | /// - algorithm: 指标计算算法 44 | public init(id: Key? = nil, algorithm: A) { 45 | self.identifier = id 46 | self.algorithm = algorithm 47 | } 48 | 49 | public func process(_ data: [Input]) -> ReadonlyOffsetArray { 50 | let result = algorithm.process(data) 51 | return .init(result, offset: data.count - result.count) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/RSAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RSAlgorithm.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/15. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 用于计算 RS 指标的算法 31 | /// 公式以 RS(14) 为例: 32 | /// ``` 33 | /// RS = Average Gain / Average Loss 34 | /// First Average Gain = Sum of Gains over the past 14 periods / 14. 35 | /// First Average Loss = Sum of Losses over the past 14 periods / 14 36 | /// Average Gain = [(previous Average Gain) x 13 + current Gain] / 14. 37 | /// Average Loss = [(previous Average Loss) x 13 + current Loss] / 14. 38 | /// ``` 39 | public struct RSAlgorithm: AnalysisAlgorithm { 40 | public typealias Output = [CGFloat] 41 | /// 基准周期 42 | public let period: Int 43 | 44 | /// 创建一个计算 RS 指标的算法 45 | /// - Parameters: 46 | /// - period: 基准周期 47 | public init(period: Int) { 48 | self.period = period 49 | } 50 | 51 | /// 处理蜡烛图数据,生成 RS 数据 52 | /// - Parameter data: 蜡烛图数据 53 | /// - Returns: RS 数据 54 | public func process(_ data: [Input]) -> [CGFloat] { 55 | guard data.count >= period + 1 else { return [] } 56 | var result: [CGFloat] = [] 57 | var upMoves: CGFloat = 0 58 | var downMoves: CGFloat = 0 59 | 60 | // 获取第一个 Average Gain 和 Average Loss 61 | for index in 1 ... period { 62 | let current = data[index].close 63 | let prev = data[index - 1].close 64 | if current >= prev { 65 | upMoves += current - prev 66 | } else { 67 | downMoves += prev - current 68 | } 69 | } 70 | let divisor = CGFloat(period) 71 | var averageGain = upMoves / divisor 72 | var averageLoss = downMoves / divisor 73 | result.append(averageGain / averageLoss) 74 | 75 | // 计算剩余的 RS 指标 76 | for index in (period + 1) ..< data.count { 77 | let current = data[index].close 78 | let prev = data[index - 1].close 79 | if current >= prev { 80 | averageGain = (averageGain * (divisor - 1) + current - prev) / divisor 81 | averageLoss = averageLoss * (divisor - 1) / divisor 82 | } else { 83 | averageLoss = (averageLoss * (divisor - 1) + prev - current) / divisor 84 | averageGain = averageGain * (divisor - 1) / divisor 85 | } 86 | result.append(averageGain / averageLoss) 87 | } 88 | return result 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Stockee/IndicatorAlgorithm/RSIAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RSIAlgorithm.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/15. 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 CoreGraphics 28 | import Foundation 29 | 30 | /// 用于计算 RSI 指标的算法 31 | /// ``` 32 | /// 100 33 | /// RSI = 100 - -------- 34 | /// 1 + RS 35 | /// ``` 36 | public struct RSIAlgorithm: AnalysisAlgorithm { 37 | public typealias Output = [CGFloat] 38 | /// 基准周期 39 | public let period: Int 40 | 41 | /// 创建一个计算 RSI 指标的算法 42 | /// - Parameters: 43 | /// - period: 基准周期 44 | public init(period: Int) { 45 | self.period = period 46 | } 47 | 48 | /// 处理蜡烛图数据,生成 RSI 数据 49 | /// - Parameter data: 蜡烛图数据 50 | /// - Returns: RSI 数据 51 | public func process(_ data: [Input]) -> [CGFloat] { 52 | RSAlgorithm(period: period).process(data).map { 53 | 100 - 100 / (1 + $0) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Stockee/Utils/Binding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding.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 | public struct Binding { 30 | private var getter: () -> V 31 | private var setter: (V) -> Void 32 | 33 | public var wrappedValue: V { 34 | get { 35 | getter() 36 | } 37 | set { 38 | setter(newValue) 39 | } 40 | } 41 | 42 | public init(get: @escaping () -> V, set: @escaping (V) -> Void) { 43 | self.getter = get 44 | self.setter = set 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Stockee/Utils/CGPath+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPath+Extensions.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/20. 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 CoreGraphics 28 | import Foundation 29 | 30 | extension CGPath { 31 | static func lineSegments(with points: [CGPoint]) -> CGPath { 32 | let path = CGMutablePath() 33 | guard let first = points.first else { return path } 34 | path.move(to: first) 35 | points.dropFirst().forEach { path.addLine(to: $0) } 36 | return path 37 | } 38 | 39 | private static func directionVector(from: CGPoint, to: CGPoint) -> CGVector { 40 | .init(dx: to.x - from.x, dy: to.y - from.y) 41 | .normalized ?? CGVector(dx: 1, dy: 0) 42 | } 43 | 44 | static func smoothCurve(with points: [CGPoint], granularity: CGFloat) -> CGPath { 45 | let path = CGMutablePath() 46 | let count = points.count 47 | guard count > 1 else { return path } 48 | var preVector = CGVector(dx: 1, dy: 0) 49 | var currentVector: CGVector = .zero 50 | 51 | func addCurve(from: CGPoint, to: CGPoint) { 52 | let distance = (to - from).norm 53 | let controlP1 = from + preVector * distance * granularity 54 | let controlP2 = to - currentVector * distance * granularity 55 | path.addCurve(to: to, control1: controlP1, control2: controlP2) 56 | } 57 | path.move(to: points[0]) 58 | for index in 1 ..< (count - 1) { 59 | currentVector = directionVector(from: points[index - 1], to: points[index + 1]) 60 | defer { preVector = currentVector } 61 | addCurve(from: points[index - 1], to: points[index]) 62 | } 63 | currentVector = .init(dx: 1, dy: 0) 64 | addCurve(from: points[count - 2], to: points[count - 1]) 65 | return path 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Stockee/Utils/Delegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delegate.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/23. 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 final class Delegate { 30 | private var block: ((Input) -> Output?)? 31 | public init() {} 32 | 33 | public func delegate(on target: Target, action: @escaping (Target, Input) -> Output) { 34 | block = { [weak target] in 35 | guard let target = target else { return nil } 36 | return action(target, $0) 37 | } 38 | } 39 | 40 | public func callAsFunction(_ input: Input) -> Output? { 41 | block?(input) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Stockee/Utils/Patch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Patch.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 Foundation 28 | 29 | public struct Patch { 30 | public var deletions: Set = [] 31 | public var insertions: Set = [] 32 | } 33 | 34 | public extension Set { 35 | func patches(to another: Set) -> Patch { 36 | var path = Patch() 37 | union(another) 38 | .forEach { x in 39 | switch (self.contains(x), another.contains(x)) { 40 | case (false, true): 41 | path.insertions.insert(x) 42 | case (true, false): 43 | path.deletions.insert(x) 44 | default: 45 | break 46 | } 47 | } 48 | return path 49 | } 50 | 51 | func patches(from another: Set) -> Patch { 52 | another.patches(to: self) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Stockee/Utils/PixelAlign.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PixelAlign.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 | /* 30 | * 用于像素对齐,防止发生复杂的颜色混合 31 | */ 32 | @MainActor 33 | @inlinable func _pixelCeil(_ value: CGFloat) -> CGFloat { 34 | ceil(value * max(1, UIScreen.main.scale)) / max(1, UIScreen.main.scale) 35 | } 36 | 37 | public extension CGPoint { 38 | @MainActor 39 | @inlinable var pixelCeiled: CGPoint { .init(x: _pixelCeil(x), y: _pixelCeil(y)) } 40 | } 41 | 42 | public extension CGSize { 43 | @MainActor 44 | @inlinable var pixelCeiled: CGSize { .init(width: _pixelCeil(width), height: _pixelCeil(height)) } 45 | } 46 | 47 | public extension CGRect { 48 | @MainActor 49 | @inlinable var pixelCeiled: CGRect { .init(origin: origin.pixelCeiled, size: size.pixelCeiled) } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Stockee/Utils/Range+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Range+Extensions.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 Foundation 28 | 29 | extension Range where Bound == Int { 30 | static var none: Range { 0..<0 } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Stockee/View/ChartDescriptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartDescriptor.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 CoreGraphics 28 | import Foundation 29 | 30 | /// 用于描述如何渲染一组图表的 Model 31 | public struct ChartDescriptor { 32 | public let spacing: CGFloat 33 | public let groups: [ChartGroup] 34 | private var cache: [(y: CGFloat, height: CGFloat)] = [] 35 | public typealias Builder = ChartGroupBuilder 36 | 37 | /// 创建一个 ChartDescriptor 38 | /// - Parameters: 39 | /// - spacing: ``ChartGroup``之间的间隔,默认为 0 40 | /// - groups: 一组 ``ChartGroup`` 41 | public init(spacing: CGFloat = 0, @Builder groups: () -> [ChartGroup]) { 42 | self.spacing = spacing 43 | self.groups = groups() 44 | cacheLayoutInfo() 45 | } 46 | 47 | init() { 48 | spacing = 0 49 | groups = [] 50 | } 51 | 52 | private mutating func cacheLayoutInfo() { 53 | guard groups.count > 0 else { return } 54 | var y: CGFloat = 0 55 | for group in groups { 56 | cache.append((y, group.height)) 57 | y += spacing + group.height 58 | } 59 | } 60 | 61 | func groupIndex(contains point: CGPoint) -> Int? { 62 | for (index, (y, height)) in cache.enumerated() where point.y >= y && point.y <= y + height { 63 | return index 64 | } 65 | return nil 66 | } 67 | } 68 | 69 | extension ChartDescriptor { 70 | var contentHeight: CGFloat { 71 | guard groups.count > 0 else { return 0 } 72 | return cache.last!.y + cache.last!.height 73 | } 74 | 75 | func layoutInfoForGroup(at index: Int) -> (y: CGFloat, height: CGFloat) { 76 | cache[index] 77 | } 78 | 79 | var renderers: [AnyChartRenderer] { 80 | groups.flatMap { $0.charts } 81 | } 82 | 83 | var rendererSet: Set> { 84 | Set(renderers) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Stockee/View/ChartGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartGroup.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 | /// 一组 Chart 的集合,例如,主图可以是一个 Group 30 | /// ``` 31 | /// ┌──────────────────────────────────┐ ─┬─ 32 | /// │ Caption │ │ 33 | /// ├──────────────────────────────────┤ │ 34 | /// │ PaddingTop │ │ 35 | /// ├──────────────────────────────────┤ │ 36 | /// │ │ │ 37 | /// │ │ │ 38 | /// │ Charts Renderer Area │ height 39 | /// │ │ │ 40 | /// │ │ │ 41 | /// ├──────────────────────────────────┤ │ 42 | /// │ PaddingBottom │ │ 43 | /// └──────────────────────────────────┘ ─┴─ 44 | /// ``` 45 | public struct ChartGroup { 46 | public var height: CGFloat 47 | public var chartPadding: (top: CGFloat, bottom: CGFloat) 48 | public var charts: [AnyChartRenderer] 49 | public var preferredFormatter: NumberFormatting 50 | public typealias Builder = ChartRendererBuilder 51 | 52 | /// 创建一个 ChartGroup 53 | /// - Parameters: 54 | /// - height: Group 的高度 55 | /// - preferredFormatter: 当前分组的指标值的格式化程序 56 | /// - chartPadding: 当前图表渲染的 Padding,不包含 caption 57 | /// - charts: 一组 ``ChartRenderer`` 58 | public init(height: CGFloat, 59 | preferredFormatter: NumberFormatting = NumberFormatter(), 60 | chartPadding: (top: CGFloat, bottom: CGFloat) = (0, 0), 61 | @Builder charts: () -> [AnyChartRenderer]) { 62 | self.height = height 63 | self.preferredFormatter = preferredFormatter 64 | self.chartPadding = chartPadding 65 | self.charts = charts() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Stockee/View/ChartGroupBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartGroupBuilder.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 Foundation 28 | 29 | @resultBuilder 30 | public enum ChartGroupBuilder { 31 | public static func buildExpression(_ expression: ChartGroup) -> [ChartGroup] { 32 | [expression] 33 | } 34 | 35 | public static func buildBlock(_ components: [ChartGroup]...) -> [ChartGroup] { 36 | components.flatMap { $0 } 37 | } 38 | 39 | public static func buildIf(_ components: ChartGroup?...) -> [ChartGroup] { 40 | components.compactMap { $0 } 41 | } 42 | 43 | public static func buildEither(first component: [ChartGroup]) -> [ChartGroup] { 44 | component 45 | } 46 | 47 | public static func buildEither(second component: [ChartGroup]) -> [ChartGroup] { 48 | component 49 | } 50 | 51 | public static func buildArray(_ components: [[ChartGroup]]) -> [ChartGroup] { 52 | components.flatMap { $0 } 53 | } 54 | 55 | public static func buildLimitedAvailability(_ component: [ChartGroup]) -> [ChartGroup] { 56 | component 57 | } 58 | 59 | public static func buildOptional(_ component: [ChartGroup]?) -> [ChartGroup] { 60 | component ?? [] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Stockee/View/ChartRendererBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChartRendererBuilder.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 Foundation 28 | 29 | @resultBuilder 30 | @MainActor 31 | public enum ChartRendererBuilder { 32 | public static func buildExpression(_ expression: R) -> [AnyChartRenderer] where R.Input == I { 33 | [AnyChartRenderer(expression)] 34 | } 35 | 36 | public static func buildExpression(_ expression: [AnyChartRenderer]) -> [AnyChartRenderer] { 37 | expression 38 | } 39 | 40 | public static func buildExpression(_ expression: [R]) -> [AnyChartRenderer] where R.Input == I { 41 | expression.map { .init($0) } 42 | } 43 | 44 | public static func buildBlock(_ components: [AnyChartRenderer]...) -> [AnyChartRenderer] { 45 | Array(components.joined()) 46 | } 47 | 48 | public static func buildIf(_ components: AnyChartRenderer?...) -> [AnyChartRenderer] { 49 | components.compactMap { $0 } 50 | } 51 | 52 | public static func buildEither(first component: [AnyChartRenderer]) -> [AnyChartRenderer] { 53 | component 54 | } 55 | 56 | public static func buildEither(second component: [AnyChartRenderer]) -> [AnyChartRenderer] { 57 | component 58 | } 59 | 60 | public static func buildArray(_ components: [[AnyChartRenderer]]) -> [AnyChartRenderer] { 61 | components.flatMap { $0 } 62 | } 63 | 64 | public static func buildLimitedAvailability(_ component: [AnyChartRenderer]) -> [AnyChartRenderer] { 65 | component 66 | } 67 | 68 | public static func buildOptional(_ component: [AnyChartRenderer]?) -> [AnyChartRenderer] { 69 | component ?? [] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Stockee/View/Indicator/SelectionIndicatorDrawer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionIndicatorLayer.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 | /// 用户长按选择时的十字线指示器 30 | @MainActor 31 | class SelectionIndicatorDrawer { 32 | private var lineLayer: ShapeLayer = .init() 33 | private var pointLayer: ShapeLayer = .init() 34 | var zPosition: CGFloat = 0 { 35 | didSet { 36 | lineLayer.zPosition = zPosition 37 | pointLayer.zPosition = zPosition + 0.1 38 | } 39 | } 40 | 41 | var isHidden: Bool = false { 42 | didSet { 43 | lineLayer.isHidden = isHidden 44 | pointLayer.isHidden = isHidden 45 | } 46 | } 47 | 48 | init() { 49 | lineLayer.fillColor = UIColor.clear.cgColor 50 | lineLayer.lineWidth = 1 / UIScreen.main.scale 51 | pointLayer.lineWidth = 4 52 | } 53 | 54 | func setup(in view: ChartView) { 55 | view.layer.addSublayer(lineLayer) 56 | view.layer.addSublayer(pointLayer) 57 | } 58 | 59 | func draw(in view: ChartView, 60 | position: CGPoint, 61 | style: Style, 62 | showHorizontal: Bool) { 63 | let x: CGFloat = view.contentOffset.x 64 | let width = view.frame.width 65 | let height = view.frame.height 66 | let linePath = CGMutablePath() 67 | if showHorizontal { 68 | linePath.addHLine(minX: x, maxX: x + width, y: position.y) 69 | } 70 | linePath.addVLine(minY: 0, maxY: height, x: position.x) 71 | lineLayer.path = linePath 72 | lineLayer.strokeColor = style.selectionIndicatorLineColor.cgColor 73 | pointLayer.isHidden = !showHorizontal 74 | if showHorizontal { 75 | let rect = CGRect(origin: position, size: .zero).insetBy(dx: -3, dy: -3) 76 | pointLayer.path = CGPath(ellipseIn: rect, transform: nil) 77 | pointLayer.fillColor = style.selectionIndicatorPointColor.cgColor 78 | pointLayer.strokeColor = style.selectionIndicatorPointColor 79 | .withAlphaComponent(0.4) 80 | .cgColor 81 | } 82 | } 83 | 84 | func tearDown() { 85 | lineLayer.removeFromSuperlayer() 86 | pointLayer.removeFromSuperlayer() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/Stockee/View/Interaction/LongPressInteraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LongPressInteraction.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 LongPressInteraction: NSObject, UIInteraction { 30 | weak var view: UIView? 31 | private lazy var gesture: UILongPressGestureRecognizer = .init(target: self, action: #selector(handleLongPress(gesture:))) 32 | 33 | private var binding: Binding 34 | 35 | init(binding: Binding) { 36 | self.binding = binding 37 | super.init() 38 | gesture.delegate = self 39 | } 40 | 41 | func willMove(to view: UIView?) { 42 | self.view?.removeGestureRecognizer(gesture) 43 | self.view = nil 44 | } 45 | 46 | func didMove(to view: UIView?) { 47 | self.view = view 48 | view?.addGestureRecognizer(gesture) 49 | } 50 | 51 | @objc private func handleLongPress(gesture: UILongPressGestureRecognizer) { 52 | guard let view = view else { return } 53 | switch gesture.state { 54 | case .began, .changed: 55 | binding.wrappedValue = gesture.location(in: view) 56 | default: 57 | break 58 | } 59 | } 60 | } 61 | 62 | extension LongPressInteraction: UIGestureRecognizerDelegate { 63 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 64 | if otherGestureRecognizer is UIPanGestureRecognizer { 65 | return false 66 | } 67 | return otherGestureRecognizer.view !== view 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Stockee/View/Interaction/SelectionResetInteraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionResetInteraction.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 SelectionResetInteraction: NSObject, UIInteraction { 30 | weak var view: UIView? 31 | private var onReset: () -> Void 32 | 33 | init(onReset: @escaping () -> Void) { 34 | self.onReset = onReset 35 | super.init() 36 | } 37 | 38 | func willMove(to view: UIView?) { 39 | self.view = nil 40 | } 41 | 42 | func didMove(to view: UIView?) { 43 | self.view = view 44 | guard let view = view as? UIScrollView else { return } 45 | view.panGestureRecognizer.delegate = self 46 | } 47 | } 48 | 49 | extension SelectionResetInteraction: UIGestureRecognizerDelegate { 50 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 51 | onReset() 52 | return true 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Stockee/View/Interaction/TapInteraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TapInteraction.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 TapInteraction: NSObject, UIInteraction { 30 | weak var view: UIView? 31 | private lazy var gesture: UITapGestureRecognizer = .init(target: self, action: #selector(handleTap(gesture:))) 32 | 33 | private var handler: (CGPoint) -> Void 34 | 35 | init(handler: @escaping (CGPoint) -> Void) { 36 | self.handler = handler 37 | super.init() 38 | } 39 | 40 | func willMove(to view: UIView?) { 41 | self.view?.removeGestureRecognizer(gesture) 42 | self.view = nil 43 | } 44 | 45 | func didMove(to view: UIView?) { 46 | self.view = view 47 | view?.addGestureRecognizer(gesture) 48 | } 49 | 50 | @objc private func handleTap(gesture: UILongPressGestureRecognizer) { 51 | guard let view = view else { return } 52 | handler(gesture.location(in: view)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/Stockee/View/Interaction/ZoomInteraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomInteraction.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 | final class ZoomInteraction: NSObject, UIInteraction { 30 | weak var view: UIView? 31 | private var preScale: CGFloat = 1.0 32 | private lazy var gesture: UIPinchGestureRecognizer = .init(target: self, action: #selector(handlePinch(gesture:))) 33 | private var handler: (UIPinchGestureRecognizer) -> Void 34 | 35 | init(handler: @escaping (UIPinchGestureRecognizer) -> Void) { 36 | self.handler = handler 37 | super.init() 38 | gesture.delegate = self 39 | } 40 | 41 | func willMove(to view: UIView?) { 42 | self.view?.removeGestureRecognizer(gesture) 43 | self.view = nil 44 | } 45 | 46 | func didMove(to view: UIView?) { 47 | self.view = view 48 | view?.addGestureRecognizer(gesture) 49 | } 50 | 51 | @objc private func handlePinch(gesture: UIPinchGestureRecognizer) { 52 | handler(gesture) 53 | } 54 | } 55 | 56 | extension ZoomInteraction: UIGestureRecognizerDelegate { 57 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 58 | if otherGestureRecognizer is UIPanGestureRecognizer { 59 | return false 60 | } 61 | return otherGestureRecognizer.view !== view 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Stockee.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Stockee' 3 | s.version = '1.3.1' 4 | s.summary = 'Swift k-line chart' 5 | s.description = <<-DESC 6 | Highly customizable performant k-line chart written in swift. 7 | DESC 8 | 9 | s.homepage = 'https://github.com/octree/Stockee' 10 | s.license = { :type => 'MIT', :file => 'LICENSE' } 11 | s.author = { 'Octree' => 'fouljz@gmail.com' } 12 | s.source = { :git => 'https://github.com/octree/Stockee.git', :tag => s.version.to_s } 13 | 14 | s.ios.deployment_target = '13.0' 15 | s.swift_version = '6.0' 16 | 17 | s.source_files = 'Sources/Stockee/**/*.swift' 18 | s.frameworks = 'UIKit' 19 | end 20 | -------------------------------------------------------------------------------- /Stockee.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 12 | 13 | 14 | 17 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Stockee.xcworkspace/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // BMKLine 9 | // 10 | // Created by ___USERNAME___ on ___DATE___. 11 | // 12 | // Copyright (c) ___YEAR___ Octree <fouljz@gmail.com> 13 | 14 | 15 | -------------------------------------------------------------------------------- /Stockee.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Core/AnyChartRendererTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyChartRendererTests.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 | @testable import Stockee 28 | import XCTest 29 | 30 | final class AnyChartRendererTests: XCTestCase { 31 | func testAnyEqual1() { 32 | let any1 = AnyChartRenderer(CounterRenderer()) 33 | XCTAssertEqual(any1, any1) 34 | } 35 | 36 | func testAnyEqual2() { 37 | let renderer = CounterRenderer() 38 | let any1 = AnyChartRenderer(renderer) 39 | let any2 = AnyChartRenderer(renderer) 40 | XCTAssertEqual(any1, any2) 41 | } 42 | 43 | func testHasble1() { 44 | let renderer = CounterRenderer() 45 | let any1 = AnyChartRenderer(renderer) 46 | let any2 = AnyChartRenderer(renderer) 47 | XCTAssertEqual(Set([any1, any2]).count, 1) 48 | } 49 | 50 | func testHasble2() { 51 | let any1 = AnyChartRenderer(CounterRenderer()) 52 | let any2 = AnyChartRenderer(CounterRenderer()) 53 | XCTAssertEqual(Set([any1, any2]).count, 2) 54 | } 55 | 56 | func testNotEqual1() { 57 | let any1 = AnyChartRenderer(CounterRenderer()) 58 | let any2 = AnyChartRenderer(CounterRenderer()) 59 | XCTAssertNotEqual(any1, any2) 60 | } 61 | 62 | func testNotEqual2() { 63 | let any1 = AnyChartRenderer(CounterRenderer()) 64 | let any2 = AnyChartRenderer(AverageRenderer()) 65 | XCTAssertNotEqual(any1, any2) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Core/ContextValuesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextValuesTests.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/4/12. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | 31 | final class ContextValuesTests: XCTestCase { 32 | 33 | enum Key: ContextKey { 34 | typealias Value = Int 35 | case key 36 | } 37 | 38 | 39 | func testWithTypeAsKey() { 40 | var values = ContextValues() 41 | XCTAssertTrue(values[Key.self] == nil) 42 | values[Key.self] = 1 43 | XCTAssertTrue(values[Key.self] == 1) 44 | values[Key.self] = nil 45 | XCTAssertTrue(values[Key.self] == nil) 46 | } 47 | 48 | func testWithCaseAsKey() { 49 | var values = ContextValues() 50 | XCTAssertTrue(values[Key.key] == nil) 51 | values[Key.key] = 1 52 | XCTAssertTrue(values[Key.key] == 1) 53 | values[Key.key] = nil 54 | XCTAssertTrue(values[Key.key] == nil) 55 | } 56 | 57 | func testWithAnyHashable() { 58 | var values = ContextValues() 59 | XCTAssertTrue(values[1] == nil) 60 | values[1] = 1 61 | XCTAssertTrue((values[1] as? Int) == 1) 62 | values[1] = nil 63 | XCTAssertTrue(values[1] == nil) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Core/QuoteProcessingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuoteProcessingTests.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 | @testable import Stockee 28 | import XCTest 29 | 30 | final class QuoteProcessingTests: XCTestCase { 31 | let data: [Candle] = [ 32 | .init(close: 1), 33 | .init(close: 2), 34 | .init(close: 3), 35 | .init(close: 4), 36 | .init(close: 5) 37 | ] 38 | 39 | func testCounterProcessor() { 40 | var contextValue = ContextValues() 41 | let processor = CounterProcessor() 42 | processor.process(data, writeTo: &contextValue) 43 | XCTAssertEqual(contextValue[CounterContextKey.self], 5) 44 | } 45 | 46 | func testAverageProcessor() { 47 | var contextValue = ContextValues() 48 | let processor = AverageCloseProcessor() 49 | processor.process(data, writeTo: &contextValue) 50 | XCTAssertEqual(contextValue[AverageCloseContextKey.self], 3) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/AFWrapperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AFWrapperTests.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 Foundation 28 | 29 | @testable import Stockee 30 | import XCTest 31 | 32 | final class AFWrapperTests: XCTestCase { 33 | func testInitialValue() { 34 | @AF(max: 0.2) var af: CGFloat = 0.02 35 | XCTAssertEqual(af, 0.02) 36 | } 37 | 38 | func testIncrease() { 39 | @AF(max: 0.2) var af: CGFloat = 0.02 40 | _af.increase(0.02) 41 | XCTAssertEqual(af, 0.04) 42 | _af.increase(0.02) 43 | XCTAssertEqual(af, 0.06) 44 | } 45 | 46 | func testIncreaseOverflow() { 47 | @AF(max: 0.2) var af: CGFloat = 0.02 48 | (0 ... 10).forEach { _ in _af.increase(10) } 49 | XCTAssertEqual(af, 0.2) 50 | } 51 | 52 | func testReset() { 53 | @AF(max: 0.2) var af: CGFloat = 0.02 54 | _af.increase(0.02) 55 | _af.reset() 56 | XCTAssertEqual(af, 0.02) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/BOLLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BOLLTests.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/14. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | /// Test case from https://school.stockcharts.com/doku.php?id=technical_indicators:bollinger_bands 31 | final class BOLLTests: XCTestCase { 32 | private let algorithm: BollingerBandsAlgorithm = .init() 33 | private var data: [Candle] = { 34 | [90.70, 92.90, 92.98, 91.80, 92.66, 92.68, 92.30, 92.77, 92.54, 92.95, 93.20, 91.07, 89.83, 89.74, 90.40, 90.74, 88.02, 88.09, 88.84, 90.78, 90.54, 91.39, 90.65] 35 | .map { .init(close: $0) } 36 | }() 37 | 38 | func testWithEmpty() { 39 | XCTAssertTrue(algorithm.process([]).isEmpty) 40 | } 41 | 42 | public func testWithInsufficientDataSet() { 43 | let data: [Candle] = (0 ..< 4).map { _ in .init() } 44 | XCTAssertTrue(algorithm.process(data).isEmpty) 45 | } 46 | 47 | public func testWithFitDataSet() { 48 | let data: [Candle] = Array(self.data[0 ..< 20]) 49 | let expected: [BOLLIndicator] = [ 50 | .init(lower: 87.97, middle: 91.25, upper: 94.53) 51 | ] 52 | XCTAssertEqual(algorithm.process(data).map { $0.rounded }, expected) 53 | } 54 | 55 | public func testWithMoreData() { 56 | let expected: [BOLLIndicator] = [ 57 | .init(lower: 87.97, middle: 91.25, upper: 94.53), 58 | .init(lower: 87.95, middle: 91.24, upper: 94.53), 59 | .init(lower: 87.96, middle: 91.17, upper: 94.37), 60 | .init(lower: 87.95, middle: 91.05, upper: 94.15) 61 | ] 62 | XCTAssertEqual(algorithm.process(data).map { $0.rounded }, expected) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/EMATests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EMATests.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/14. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | /// Test case from https://school.stockcharts.com/doku.php?id=technical_indicators:moving_averages 31 | final class EMATests: XCTestCase { 32 | private let algorithm: ExponentialMovingAverageAlgorithm = .init(period: 10) 33 | 34 | func testWithEmpty() { 35 | XCTAssertTrue(algorithm.process([]).isEmpty) 36 | } 37 | 38 | public func testWithInsufficientDataSet() { 39 | let data: [Candle] = (0 ..< 4).map { _ in .init() } 40 | XCTAssertTrue(algorithm.process(data).isEmpty) 41 | } 42 | 43 | public func testWithFitDataSet() { 44 | let data: [Candle] = [22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 22.29] 45 | .map { 46 | Candle(close: $0) 47 | } 48 | XCTAssertEqual(algorithm.process(data).map { $0.rounded }, [22.22]) 49 | } 50 | 51 | public func testWithMoreData() { 52 | let data: [Candle] = [22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 22.29, 22.15, 22.39, 22.38, 22.61, 23.36] 53 | .map { 54 | Candle(close: $0) 55 | } 56 | let expected: [CGFloat] = [22.22, 22.21, 22.24, 22.27, 22.33, 22.52] 57 | XCTAssertEqual(algorithm.process(data).map { $0.rounded }, expected) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/MACDTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/15. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | // 测试数据来自:https://investexcel.net/how-to-calculate-macd-in-excel/ 31 | final class MACDTests: XCTestCase { 32 | private let algorithm: MACDAlgorithm = .init() 33 | private var data: [Candle] = { 34 | [459.99, 448.85, 446.06, 450.81, 442.8, 448.97, 444.57, 441.4, 430.47, 420.05, 431.14, 425.66, 430.58, 431.72, 437.87, 428.43, 428.35, 432.5, 443.66, 455.72, 454.49, 452.08, 452.73, 461.91, 463.58, 461.14, 452.08, 442.66, 428.91, 429.79, 431.99, 427.72, 423.2, 426.21, 426.98, 435.69, 434.33, 429.8, 419.85, 426.24, 402.8] 35 | .map { .init(close: $0) } 36 | }() 37 | 38 | private var macd: [MACDIndicator] = { 39 | [[8.2752695], 40 | [7.7033784], 41 | [6.4160748], 42 | [4.2375198], 43 | [2.5525833], 44 | [1.3788857], 45 | [0.1029815], 46 | [-1.258402], 47 | [-2.070558, 3.0375259, -5.108084059], 48 | [-2.621842, 1.9056522, -4.527494558], 49 | [-2.329067, 1.0587084, -3.387775176], 50 | [-2.181632, 0.4106403, -2.59227244], 51 | [-2.402626, -0.152013, -2.250613279], 52 | [-3.342122, -0.790035, -2.55208695], 53 | [-3.530363, -1.3381, -2.192262723], 54 | [-5.507471, -2.171975, -3.335496669]] 55 | .map { 56 | $0.count > 1 ? .init(diff: $0[0], dea: $0[1], histogram: $0[2]) : .init(diff: $0[0], dea: nil, histogram: nil) 57 | } 58 | }() 59 | 60 | func testWithEmpty() { 61 | XCTAssertTrue(algorithm.process([]).isEmpty) 62 | } 63 | 64 | public func testWithInsufficientDataSet() { 65 | let data: [Candle] = Array(data[0 ..< 25]) 66 | XCTAssertTrue(algorithm.process(data).isEmpty) 67 | } 68 | 69 | public func testWithFitDataSet() { 70 | let data: [Candle] = Array(data[0 ..< 26]) 71 | let result = algorithm.process(data) 72 | XCTAssertTrue(result.count == 1) 73 | XCTAssertEqual(result[0].diff, macd[0].diff, accuracy: 0.00001) 74 | XCTAssertNil(result[0].dea) 75 | XCTAssertNil(result[0].histogram) 76 | } 77 | 78 | public func testWith33Items() { 79 | let data: [Candle] = Array(data[0 ..< 33]) 80 | let result = algorithm.process(data) 81 | XCTAssertTrue(result.count == 8) 82 | result.indices.forEach { 83 | XCTAssertEqual(result[$0].diff, macd[$0].diff, accuracy: 0.00001) 84 | XCTAssertNil(result[$0].dea) 85 | XCTAssertNil(result[$0].histogram) 86 | } 87 | } 88 | 89 | public func testWith34Items() { 90 | let data: [Candle] = Array(data[0 ..< 34]) 91 | let result = algorithm.process(data) 92 | XCTAssertTrue(result.count == 9) 93 | XCTAssertEqual(result[8].diff, macd[8].diff, accuracy: 0.00001) 94 | XCTAssertNotNil(result[8].dea) 95 | XCTAssertNotNil(result[8].histogram) 96 | XCTAssertEqual(result[8].dea!, macd[8].dea!, accuracy: 0.00001) 97 | XCTAssertEqual(result[8].histogram!, macd[8].histogram!, accuracy: 0.00001) 98 | } 99 | 100 | public func testWithMoreData() { 101 | let result = algorithm.process(data) 102 | XCTAssertEqual(result.count, macd.count) 103 | zip(result, macd).forEach { 104 | switch ($0.0.histogram, $0.1.histogram) { 105 | case let (.some(lhs), .some(rhs)): 106 | XCTAssertEqual(lhs, rhs, accuracy: 0.00001) 107 | case (.none, .none): 108 | break 109 | default: 110 | XCTAssertTrue(false) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/MovingAverageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MovingAverageTests.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/14. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | final class MovingAverageTests: XCTestCase { 31 | private let algorithm: MovingAverageAlgorithm = .init(period: 5) 32 | 33 | public func testWithEmptyDataSet() { 34 | XCTAssertTrue(algorithm.process([]).isEmpty) 35 | } 36 | 37 | public func testWithInsufficientDataSet() { 38 | let data: [Candle] = [.init(close: 11), .init(close: 11), .init(close: 11), .init(close: 11)] 39 | XCTAssertTrue(algorithm.process(data).isEmpty) 40 | } 41 | 42 | public func testWithFitDataSet() { 43 | let data: [Candle] = (11 ... 15).map { .init(close: CGFloat($0)) } 44 | XCTAssertEqual(algorithm.process(data), [13]) 45 | } 46 | 47 | public func testWithMoreData() { 48 | let data: [Candle] = (11 ... 17).map { .init(close: CGFloat($0)) } 49 | XCTAssertEqual(algorithm.process(data), [13, 14, 15]) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/RSITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RSITests.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/15. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | func eq(_ x: CGFloat, _ y: CGFloat) -> Bool { 31 | abs(x - y) < 0.1 32 | } 33 | 34 | /// Test case from https://school.stockcharts.com/doku.php?id=technical_indicators:relative_strength_index_rsi 35 | final class RSITests: XCTestCase { 36 | private let algorithm: RSIAlgorithm = .init(period: 14) 37 | private var data: [Candle] = { 38 | [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64, 46.21, 46.25, 45.71, 46.45] 39 | .map { .init(close: $0) } 40 | }() 41 | 42 | private var rsi: [CGFloat] = [70.53, 66.32, 66.55, 69.41, 66.36, 57.97, 62.93, 63.26, 56.06, 62.38] 43 | 44 | func testWithEmpty() { 45 | XCTAssertTrue(algorithm.process([]).isEmpty) 46 | } 47 | 48 | public func testWithInsufficientDataSet() { 49 | let data: [Candle] = Array(data[0 ..< 14]) 50 | XCTAssertTrue(algorithm.process(data).isEmpty) 51 | } 52 | 53 | public func testWithFitDataSet() { 54 | let data: [Candle] = Array(data[0 ..< 15]) 55 | let result = algorithm.process(data) 56 | XCTAssertEqual(result.count, 1) 57 | XCTAssertTrue(abs(result[0] - 70.53) < 0.1) 58 | } 59 | 60 | public func testWithMoreData() { 61 | XCTAssertTrue( 62 | zip(algorithm.process(data), rsi).allSatisfy(eq) 63 | ) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/RSTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RSTests.swift 3 | // Stockee 4 | // 5 | // Created by octree on 2022/3/15. 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 | @testable import Stockee 28 | import XCTest 29 | 30 | /// Test case from https://school.stockcharts.com/doku.php?id=technical_indicators:relative_strength_index_rsi 31 | final class RSTests: XCTestCase { 32 | private let algorithm: RSAlgorithm = RSAlgorithm(period: 14) 33 | private var data: [Candle] = { 34 | [44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64, 46.21, 46.25, 45.71, 46.45] 35 | .map { .init(close: $0) } 36 | }() 37 | 38 | private var rs: [CGFloat] = [2.39, 1.97, 1.99, 2.27, 1.97, 1.38, 1.70, 1.72, 1.28, 1.66] 39 | 40 | func testWithEmpty() { 41 | XCTAssertTrue(algorithm.process([]).isEmpty) 42 | } 43 | 44 | public func testWithInsufficientDataSet() { 45 | let data: [Candle] = Array(data[0 ..< 14]) 46 | XCTAssertTrue(algorithm.process(data).isEmpty) 47 | } 48 | 49 | public func testWithFitDataSet() { 50 | let data: [Candle] = Array(data[0 ..< 15]) 51 | XCTAssertEqual(algorithm.process(data).map { $0.rounded }, [2.39]) 52 | } 53 | 54 | public func testWithMoreData() { 55 | func eq(_ x: CGFloat, _ y: CGFloat) -> Bool { 56 | abs(x - y) < 0.01 57 | } 58 | XCTAssertTrue( 59 | zip(algorithm.process(data), rs).allSatisfy(eq) 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Indicators/SARTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SARTests.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 | @testable import Stockee 28 | import XCTest 29 | 30 | // 测试数据来自: 富途 FB 季 K 前 8 个数据 31 | final class SARTests: XCTestCase { 32 | private let algorithm: SARAlgorithm = .init() 33 | private var data: [Candle] = { 34 | // low, high, open, close 35 | [ 36 | [25.520, 45.0, 42.050, 31.095], 37 | [17.550, 32.880, 31.250, 21.660], 38 | [18.800, 28.880, 22.080, 26.620], 39 | [24.720, 32.506, 27.440, 25.580], 40 | [22.670, 29.070, 25.630, 24.880], 41 | [24.150, 51.600, 24.969, 50.230], 42 | [43.550, 58.580, 49.970, 54.649], 43 | [51.850, 72.590, 54.830, 60.240] 44 | ].map { .init(low: $0[0], high: $0[1], open: $0[2], close: $0[3]) } 45 | }() 46 | 47 | private var sar: [SARIndicator] = [ 48 | .init(sar: 45.0, isReversal: true, isUp: false), 49 | .init(sar: 44.451, isReversal: false, isUp: false), 50 | .init(sar: 18.800, isReversal: true, isUp: true), 51 | .init(sar: 19.456, isReversal: false, isUp: true), 52 | .init(sar: 21.021, isReversal: false, isUp: true) 53 | ] 54 | 55 | func testWithEmptyData() { 56 | XCTAssertTrue(algorithm.process([]).count == 0) 57 | } 58 | 59 | func testWith4Items() { 60 | let data = Array(data[0 ..< 4]) 61 | XCTAssertTrue(algorithm.process(data).count == 0) 62 | } 63 | 64 | func testWith5Items() { 65 | let data = Array(data[0 ..< 5]) 66 | XCTAssertEqual(algorithm.process(data), Array(sar[0 ..< 2])) 67 | } 68 | 69 | func testWithAllData() { 70 | let result = algorithm.process(data) 71 | XCTAssertEqual(result.count, sar.count) 72 | zip(result, sar).forEach { elt, expected in 73 | XCTAssertEqual(elt.sar, expected.sar, accuracy: 0.001) 74 | XCTAssertEqual(elt.isReversal, expected.isReversal) 75 | } 76 | } 77 | 78 | func testPerformance() { 79 | let data: [Candle] = (0 ..< 1000).map { _ in 80 | let low = CGFloat.random(in: 10 ... 20) 81 | let high = CGFloat.random(in: 40 ... 50) 82 | let open = CGFloat.random(in: low ..< high) 83 | let close = CGFloat.random(in: low ..< high) 84 | return .init(low: low, high: high, open: open, close: close) 85 | } 86 | measure { 87 | _ = algorithm.process(data) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/StockeeTests/UtilTest/ReadonlyOffsetArrayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadonlyOffsetArray.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 | @testable import Stockee 28 | import XCTest 29 | 30 | final class ReadonlyOffsetArrayTests: XCTestCase { 31 | private let array = ReadonlyOffsetArray([1, 2, 3, 4], offset: 3) 32 | 33 | func test() { 34 | XCTAssertEqual(array[0], nil) 35 | XCTAssertEqual(array[2], nil) 36 | XCTAssertEqual(array[3], 1) 37 | XCTAssertEqual(array[6], 4) 38 | XCTAssertEqual(array[7], nil) 39 | } 40 | 41 | func testExtremePoint() { 42 | XCTAssertTrue(array.extremePoint(in: 0 ..< 2) == nil) 43 | XCTAssertTrue(array.extremePoint(in: 3 ..< 3) == nil) 44 | XCTAssertTrue(array.extremePoint(in: 4 ..< 4) == nil) 45 | var result = array.extremePoint(in: 0 ..< 10) 46 | XCTAssertTrue(result?.min == 1 && result?.max == 4) 47 | result = array.extremePoint(in: 5 ..< 8) 48 | XCTAssertTrue(result?.min == 3 && result?.max == 4) 49 | result = array.extremePoint(in: 3 ..< 5) 50 | XCTAssertTrue(result?.min == 1 && result?.max == 2) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Utils/Processors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Processors.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 CoreGraphics 28 | import Stockee 29 | 30 | public enum CounterContextKey: ContextKey { 31 | public typealias Value = Int 32 | } 33 | 34 | public class CounterProcessor: QuoteProcessing { 35 | public typealias Input = Candle 36 | public typealias Output = Int 37 | public typealias Key = CounterContextKey 38 | 39 | public func process(_ data: [Candle]) -> Int { 40 | data.count 41 | } 42 | } 43 | 44 | public enum AverageCloseContextKey: ContextKey { 45 | public typealias Value = CGFloat 46 | } 47 | 48 | public class AverageCloseProcessor: QuoteProcessing { 49 | public typealias Input = Candle 50 | public typealias Output = CGFloat 51 | public typealias Key = AverageCloseContextKey 52 | 53 | public func process(_ data: [Candle]) -> CGFloat { 54 | guard data.count > 0 else { return 0 } 55 | let sum = data.reduce(0) { $0 + $1.close } 56 | return sum / CGFloat(data.count) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Utils/Renderers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderers.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 CoreGraphics 28 | import Stockee 29 | 30 | public class CounterRenderer: ChartRenderer { 31 | public var quoteProcessor: CounterProcessor? { 32 | CounterProcessor() 33 | } 34 | 35 | public typealias Input = Candle 36 | public typealias QuoteProcessor = CounterProcessor 37 | 38 | public func setup(in view: ChartView) {} 39 | 40 | public func render(in view: ChartView, context: Context) {} 41 | 42 | public func tearDown(in view: ChartView) {} 43 | 44 | public func updateZPosition(_ position: CGFloat) {} 45 | 46 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? { 47 | nil 48 | } 49 | } 50 | 51 | public class AverageRenderer: ChartRenderer { 52 | public var quoteProcessor: AverageCloseProcessor? { 53 | AverageCloseProcessor() 54 | } 55 | 56 | public typealias Input = Candle 57 | public typealias QuoteProcessor = AverageCloseProcessor 58 | 59 | public func setup(in view: ChartView) {} 60 | 61 | public func render(in view: ChartView, context: Context) {} 62 | 63 | public func tearDown(in view: ChartView) {} 64 | 65 | public func updateZPosition(_ position: CGFloat) {} 66 | 67 | public func extremePoint(contextValues: ContextValues, visibleRange: Range) -> (min: CGFloat, max: CGFloat)? { 68 | nil 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/StockeeTests/Utils/Rounded.swift: -------------------------------------------------------------------------------- 1 | import Stockee 2 | import CoreGraphics 3 | import Foundation 4 | 5 | extension CGFloat { 6 | var rounded: CGFloat { 7 | (self * 100).rounded() / 100 8 | } 9 | } 10 | 11 | extension BOLLIndicator { 12 | var rounded: BOLLIndicator { 13 | .init(lower: lower.rounded, 14 | middle: middle.rounded, 15 | upper: upper.rounded) 16 | } 17 | } 18 | --------------------------------------------------------------------------------