0%

用平均漲跌幅判斷股價的強弱 | 用程式打造選股策略(7)

前言

前陣子看了JG的書,反市場JG股市操作原理,令人印象深刻。

且這本書也上了熱銷排行榜,雖然以前也看過他的文章,不過並不是很認真,
最近回頭看了這篇: 移動平均線(三) 均線操作在台股的特殊意義

文中提到: 在用均線操作股票的時候,請先不要看股價是否有突破月線,第一件事情,請務必務必盯著月線的角度。一但月線角度往上走揚,在市場上的我們才可以開始有「想買進的念頭」,翻揚之後能越快買進越好。

看完之後總覺得斜率、角度,這種東西太吃感覺,依據股價的不同,甚至看盤軟體不同,看起來也都不一樣。
想了半天,與其計算均線的斜率,不如直接計算平均漲跌幅(%)還比較容易。

平均漲跌幅的意義

平均漲跌幅(%) > 0 : 月線向上彎
平均漲跌幅(%) = 0 : 月線走平
平均漲跌幅(%) < 0 : 月線向下彎

雖然說用平均漲跌幅會有一點小誤差(EX: 10元的股票跌 1% = 9元,再漲 1% = 9.9 元 )
但其實應該還是能代表趨勢…

我試著運用此方法,月線向上彎代表著股價轉為強勢,向下彎則代表股價轉為弱勢
這樣的話,在平均帳跌幅突破0的時候買進,跌破時賣出

程式部分

前陣子全部用 .NET Core 寫了一堆爬蟲,一直想半天要怎麼呈現策略的買賣點比較好
沒什麼好主意,浪費時間想一堆不如直接用已知的做法來硬做…

後端: .NET Core Web API
前端: Vue.js + EChart 來畫出指標

程式部分確實是蠻長的,不想看的可以直接跳到 Demo結果

後端

這裡從資料庫裡撈出之前爬的股價資料,可以參考: 用 C# .NET Core 自動爬取台股每日股價

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public IEnumerable<Stock> GetSmallerThenDateStocks(string stock_id, DateTime date, int days)
{
return _conn.Query<Stock>(
@"
select t1.* from (
select top (@days) * from TWStockPrice_history_1990 where stock_id=@stock_id and trading_date <= @date order by trading_date desc
) t1 order by t1.trading_date",
new {
stock_id,
date,
days
}
);
}

建立回傳的 Model:

這個比資料庫的 Stock 多了幾個欄位要算出來:

  • percent : 當日漲跌幅(%)
  • sum_percent_n : 平均n天漲跌幅加總
  • average_percent_n : 平均n天的漲跌幅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StockCalculateModel
{
public int id { get; set; }
public string stock_id { get; set; }
public string trading_volume { get; set; }
public string trading_money { get; set; }
public string open { get; set; }
public string max { get; set; }
public string min { get; set; }
public string close { get; set; }
public string spread { get; set; }
public string trading_turnover { get; set; }
public DateTime trading_date { get; set; }
public decimal percent { get; set; }
public decimal average_percent_n { get; set; }
public decimal sum_percent_n { get; set; }
}

Web Api 計算並回傳

這裡用到了 AutoMapper 這個套件來方便轉換 Model

安裝 AutoMapper

1
2
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

然後在 Startup.cs 注入就可以使用了
基本上只要 property 名稱一樣就會自動對應

1
2
3
4
5
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper(typeof(Startup));
...
}

不過這裡新增的那三個欄位沒得對應,因此透過 Profile 來 Ignore 掉

1
2
3
4
5
6
7
8
9
public class StockMap : Profile
{
public StockMap()
{
CreateMap<Stock, StockCalculateModel>()
.ForMember(sm => sm.percent, ie => ie.Ignore())
.ForMember(sm => sm.sum_percent_n, ie => ie.Ignore());
}
}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[ApiController]
[Route("api/[controller]/[action]")]
public class StrategyController : ControllerBase
{
private readonly ILogger<StrategyController> _logger;
private StockRepository _stockRepository;
private IMapper _mapper;

public StrategyController(
ILogger<StrategyController> logger,
StockRepository stockRepository,
IMapper mapper)
{
_logger = logger;
_stockRepository = stockRepository;
_mapper = mapper;
}

[HttpGet]
public IActionResult GetAverageIncrease(string stock_id, int averageDay, int totalDay)
{
var stockList = _stockRepository.GetSmallerThenDateStocks(stock_id, DateTime.Now.Date, totalDay);
var stockCalculateModelList = stockList.Select((stock, index) => {
var stockCalculateModel = _mapper.Map<StockCalculateModel>(stock);
// 這裡先算出當日漲跌幅
if(index >= 1)
stockCalculateModel.percent = Math.Round((Convert.ToDecimal(stock.close) / Convert.ToDecimal(stockList.ElementAt(index-1).close) - 1) * 100, 2, MidpointRounding.AwayFromZero);
return stockCalculateModel;
}).ToList();

// 計算平均漲跌幅(average_percent_n),漲跌幅加總(sum_percent_n)只是用來輔助計算平均漲跌幅而已
for(int index=0; index<stockCalculateModelList.Count(); index++)
{
decimal sum_percent_n = 0;

if(index == averageDay-1)
{
for(int i = 0 ; i < averageDay ; i++)
{
sum_percent_n += stockCalculateModelList.ElementAt(index - i).percent;
}
stockCalculateModelList.ElementAt(index).sum_percent_n = sum_percent_n;
}
if(index > averageDay-1)
{
stockCalculateModelList.ElementAt(index).sum_percent_n = stockCalculateModelList.ElementAt(index-1).sum_percent_n + stockCalculateModelList.ElementAt(index).percent - stockCalculateModelList.ElementAt(index-averageDay).percent;
}
stockCalculateModelList.ElementAt(index).average_percent_n = Math.Round(stockCalculateModelList.ElementAt(index).sum_percent_n / averageDay, 2, MidpointRounding.AwayFromZero);
}
return new JsonResult(stockCalculateModelList);
}
}

這樣Api就搞定了,剩下前端部分..

前端

寫了一個解釋太麻煩了,看Code吧…

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
<template>
<div class="stock-area">
<v-chart ref="strengthChart" class="chart" :options="stockOptions" />
</div>
</template>

<script>
import ECharts from "vue-echarts";
import "echarts/lib/chart/line";
import "echarts/lib/chart/bar";
import "echarts/lib/component/polar";
import 'echarts/lib/component/title'
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/dataZoom'
import "echarts/lib/chart/candlestick";
import { SMA } from 'technicalindicators'
export default {
name: "CandlestickStrengthChart",
components: {
"v-chart": ECharts
},
computed: {
chartData() {
return this.rawDataTW.map(function(item) {
return [+item.open, +item.close, +item.min, +item.max];
});
},
closeData() {
return this.chartData.map(item => item[1])
},
chartDates() {
return this.rawDataTW.map(function(item) {
return item.trading_date.slice(0, 10);
});
},
chartSeries() {
let that = this;
return this.chartTitle.map(title => {
if(title.indexOf('MA') >= 0) {
let frequency = Number(title.replace('MA',''));
return {
name: title,
type: "line",
data: that.calculateMA(frequency, that.chartData.map(item => item[1])),
smooth: true,
showSymbol: false,
lineStyle: {
width: 1
}
}
}
else if(title == '日K'){
return {
type: "candlestick",
name: title,
data: that.chartData,
itemStyle: {
color: "#FD1050",
color0: "#0CF49B",
borderColor: "#FD1050",
borderColor0: "#0CF49B"
}
}
}
else if(title == 'SignalBuy') {
return {
name: 'SignalBuy',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: that.rawDataTW.map(x => x.signal_buy),
itemStyle: {
color: '#00f'
},
barGap: '-100%'
}
}
else if(title == 'SignalSell') {
return {
name: 'SignalSell',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: that.rawDataTW.map(x => x.signal_sell),
itemStyle: {
color: '#f00'
},
barGap: '-100%'
}
}
else if(title == 'Strength')
{
return {
name: title,
type: "line",
data: that.rawDataTW.map(x => x.average_percent_n),
smooth: true,
showSymbol: false,
lineStyle: {
width: 1
},
xAxisIndex: 1,
yAxisIndex: 2,
itemStyle: {
color: '#fa0'
}
}
}
})
},
stockOptions() {
return {
title: {
text: this.stockInfo === null ? '': `${this.stockInfo.stock_name} (${this.stockInfo.stock_id})`,
textStyle: {
color: '#fff',
width: '100%'
},
x: 'center',
y: '20px'
},
backgroundColor: "#21202D",
legend: {
data: this.chartTitle,
inactiveColor: "#777",
textStyle: {
color: "#fff"
}
},
tooltip: {
trigger: "axis",
axisPointer: {
animation: false,
type: "cross",
lineStyle: {
color: "#376df4",
width: 2,
opacity: 1
}
},
},
axisPointer: {
link: {xAxisIndex: 'all'},
},
xAxis: [{
type: "category",
data: this.chartDates,
axisLine: { lineStyle: { color: "#8392A5" } }
},
{
type: 'category',
gridIndex: 1,
data: this.chartDates,
min: 'dataMin',
max: 'dataMax'
}],
yAxis: [{
scale: true,
axisLine: { lineStyle: { color: "#8392A5" } },
splitLine: { show: false }
}, {
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLine: {
lineStyle: { color: "#8392A5" }
},
axisLabel: {show: false},
axisTick: {show: false},
splitLine: {show: false}
},{
scale: true,
gridIndex: 1,
min: -2,
max: 2,
axisLine: {
lineStyle: { color: "#8392A5" }
},
axisTick: {show: false},
}],
grid: [{
height: '50%'
},{
top: '73%',
height: '16%',
bottom: 80
}],
dataZoom: [
{
textStyle: {
color: "#8392A5"
},
handleIcon:
"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z",
handleSize: "80%",
dataBackground: {
areaStyle: {
color: "#8392A5"
},
lineStyle: {
opacity: 0.8,
color: "#8392A5"
}
},
handleStyle: {
color: "#fff",
shadowBlur: 3,
shadowColor: "rgba(0, 0, 0, 0.6)",
shadowOffsetX: 2,
shadowOffsetY: 2
}
},
{
type: "inside",
xAxisIndex: [0, 1]
}
],
animation: false,
series: this.chartSeries
}
}
},
async mounted () {
let that = this
await this.queryStock();
window.onresize = () => {
return (() => {
that.$refs.strengthChart.resize();
})()
}
},
data() {
return {
rawDataTW: [],
chartTitle: ["日K", "MA20", "SignalBuy", "SignalSell", "Strength"],
stockInfo: null,
query: {
stock_id: '0050'
}
};
},
methods: {
async queryStock() {
await this.getStocks();
await this.getStockInfo();
},
async getStocks() {
let response = await this.axios.get(`/api/Strategy/GetAverageIncrease?stock_id=${this.query.stock_id}&averageDay=20&totalDay=360`)
let data = response.data;
let lastItem = null;
this.rawDataTW = data.map((item, index)=> {
item.signal_buy = 0;
item.signal_sell = 0;
if(index > 0 && lastItem.average_percent_n < 0 && item.average_percent_n > 0) {
item.signal_buy = 1;
}
else if(index > 0 && lastItem.average_percent_n > 0 && item.average_percent_n < 0) {
item.signal_sell = 1;
}
lastItem = item;
return item;
})
},
async getStockInfo() {
let response = await this.axios.get(`/api/Stock/GetStockInfoByStockId?stock_id=${this.query.stock_id}`)
this.stockInfo = response.data;
},
calculateMA(dayCount, data) {
let result = SMA.calculate({period : dayCount, values : data})
return new Array(dayCount).fill('-').concat(result)
},
}
};
</script>

Demo結果

這裡用 0050 來做個 Demo
上面是 K線圖 和 20日均線
下面是 平均漲跌幅

藍色: 代表「買入」訊號
紅色: 代表「賣出」訊號
如下:

結論

這種方式看起來在盤整沒有趨勢的時候挺慘的..
但在2019年下半年的多頭市場表現感覺還不錯,且也空頭時也不會有大賠的情況..
有空再多看幾檔股票試試看囉~

↓↓↓ 如果喜歡我的文章,可以幫我按個Like! ↓↓↓
>> 或者,請我喝杯咖啡,這樣我會更有動力唷! <<<
街口支付

街口支付

街口帳號: 901061546

歡迎關注我的其它發布渠道