2025-09-16 09:59:38 +08:00
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
2025-11-20 16:10:16 +08:00
"end_time": "2025-11-19T07:32:11.095879Z",
"start_time": "2025-11-19T07:32:11.091010Z"
2025-09-16 09:59:38 +08:00
}
},
"source": [
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
2025-11-07 16:26:00 +08:00
"\n",
2025-11-20 16:10:16 +08:00
"import pandas as pd\n",
2025-09-16 09:59:38 +08:00
"\n",
"import warnings\n",
"\n",
"# 忽略所有警告\n",
"warnings.filterwarnings(\"ignore\")\n",
"\n",
"# --- 0. Configure your file path ---\n",
"# Please replace 'your_futures_data.csv' with the actual path to your CSV file\n",
2025-11-07 16:26:00 +08:00
"file_path = 'D:/PyProject/NewQuant/data/data/KQ_m@SHFE_rb/KQ_m@SHFE_rb_min15.csv'\n",
2025-09-16 09:59:38 +08:00
"\n",
"sns.set(style='whitegrid')\n",
"plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签\n",
"plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号\n"
],
"outputs": [],
2025-11-20 16:10:16 +08:00
"execution_count": 2
2025-09-16 09:59:38 +08:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-20 16:10:16 +08:00
"end_time": "2025-11-19T07:32:11.143726Z",
"start_time": "2025-11-19T07:32:11.100393Z"
2025-09-16 09:59:38 +08:00
}
},
"cell_type": "code",
"source": [
"\n",
"# --- 1. Data Loading and Preprocessing ---\n",
"def load_and_preprocess_data(file_path):\n",
" \"\"\"\n",
" Loads historical futures data and performs basic preprocessing.\n",
" Assumes data contains 'datetime', 'open', 'high', 'low', 'close', 'volume' columns.\n",
" \"\"\"\n",
" try:\n",
" df = pd.read_csv(file_path, parse_dates=['datetime'], index_col='datetime')\n",
" # Ensure data is sorted by time\n",
" df = df.sort_index()\n",
" # Check and handle missing values\n",
" initial_rows = len(df)\n",
" df.dropna(inplace=True)\n",
" if len(df) < initial_rows:\n",
" print(f\"Warning: Missing values found in data, deleted {initial_rows - len(df)} rows.\")\n",
"\n",
" # Check if necessary columns exist\n",
" required_columns = ['open', 'high', 'low', 'close', 'volume']\n",
" if not all(col in df.columns for col in required_columns):\n",
" raise ValueError(f\"CSV file is missing required columns. Please ensure it contains: {required_columns}\")\n",
"\n",
" print(f\"Successfully loaded {len(df)} rows of data.\")\n",
" print(\"First 5 rows of data:\")\n",
" print(df.head())\n",
" return df\n",
" except FileNotFoundError:\n",
" print(f\"Error: File '{file_path}' not found. Please check the path.\")\n",
" return None\n",
" except Exception as e:\n",
" print(f\"Error during data loading or preprocessing: {e}\")\n",
" return None\n",
"\n",
"\n",
"df_raw = load_and_preprocess_data(file_path)\n",
2025-11-20 16:10:16 +08:00
"print(df_raw)\n",
"# df_df_raw = df_df_raw[df_df_raw.index >= '2024-01-01']"
2025-09-16 09:59:38 +08:00
],
"id": "1638e05ca7ef1ac8",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
2025-11-20 16:10:16 +08:00
"Successfully loaded 26532 rows of data.\n",
2025-09-16 09:59:38 +08:00
"First 5 rows of data:\n",
2025-11-07 16:26:00 +08:00
" open high low close volume open_oi \\\n",
"datetime \n",
"2020-12-31 14:45:00 4352.0 4400.0 4345.0 4388.0 213731.0 1221661.0 \n",
"2021-01-04 09:00:00 4356.0 4368.0 4309.0 4336.0 338332.0 1217327.0 \n",
"2021-01-04 09:15:00 4336.0 4342.0 4307.0 4318.0 144479.0 1197881.0 \n",
"2021-01-04 09:30:00 4318.0 4329.0 4312.0 4317.0 85679.0 1194567.0 \n",
"2021-01-04 09:45:00 4317.0 4338.0 4316.0 4338.0 66461.0 1194592.0 \n",
2025-09-24 23:14:14 +08:00
"\n",
2025-11-07 16:26:00 +08:00
" close_oi underlying_symbol \n",
"datetime \n",
"2020-12-31 14:45:00 1217327.0 SHFE.rb2105 \n",
"2021-01-04 09:00:00 1197881.0 SHFE.rb2105 \n",
"2021-01-04 09:15:00 1194567.0 SHFE.rb2105 \n",
"2021-01-04 09:30:00 1194592.0 SHFE.rb2105 \n",
2025-11-20 16:10:16 +08:00
"2021-01-04 09:45:00 1198035.0 SHFE.rb2105 \n",
" open high low close volume open_oi \\\n",
"datetime \n",
"2020-12-31 14:45:00 4352.0 4400.0 4345.0 4388.0 213731.0 1221661.0 \n",
"2021-01-04 09:00:00 4356.0 4368.0 4309.0 4336.0 338332.0 1217327.0 \n",
"2021-01-04 09:15:00 4336.0 4342.0 4307.0 4318.0 144479.0 1197881.0 \n",
"2021-01-04 09:30:00 4318.0 4329.0 4312.0 4317.0 85679.0 1194567.0 \n",
"2021-01-04 09:45:00 4317.0 4338.0 4316.0 4338.0 66461.0 1194592.0 \n",
"... ... ... ... ... ... ... \n",
"2025-10-27 13:45:00 3078.0 3080.0 3073.0 3073.0 29861.0 1982795.0 \n",
"2025-10-27 14:00:00 3073.0 3078.0 3073.0 3078.0 18588.0 1983344.0 \n",
"2025-10-27 14:15:00 3078.0 3084.0 3077.0 3083.0 56102.0 1981459.0 \n",
"2025-10-27 14:30:00 3083.0 3089.0 3082.0 3088.0 77619.0 1979653.0 \n",
"2025-10-27 14:45:00 3088.0 3100.0 3088.0 3100.0 188528.0 1973381.0 \n",
"\n",
" close_oi underlying_symbol \n",
"datetime \n",
"2020-12-31 14:45:00 1217327.0 SHFE.rb2105 \n",
"2021-01-04 09:00:00 1197881.0 SHFE.rb2105 \n",
"2021-01-04 09:15:00 1194567.0 SHFE.rb2105 \n",
"2021-01-04 09:30:00 1194592.0 SHFE.rb2105 \n",
"2021-01-04 09:45:00 1198035.0 SHFE.rb2105 \n",
"... ... ... \n",
"2025-10-27 13:45:00 1983344.0 SHFE.rb2601 \n",
"2025-10-27 14:00:00 1981459.0 SHFE.rb2601 \n",
"2025-10-27 14:15:00 1979653.0 SHFE.rb2601 \n",
"2025-10-27 14:30:00 1973381.0 SHFE.rb2601 \n",
"2025-10-27 14:45:00 1953001.0 SHFE.rb2601 \n",
"\n",
"[26532 rows x 8 columns]\n"
2025-09-24 23:14:14 +08:00
]
2025-09-20 00:04:51 +08:00
}
],
2025-11-20 16:10:16 +08:00
"execution_count": 3
2025-09-20 00:04:51 +08:00
},
{
"metadata": {
"ExecuteTime": {
2025-11-20 16:10:16 +08:00
"end_time": "2025-11-19T07:32:14.587238Z",
"start_time": "2025-11-19T07:32:11.156009Z"
2025-09-20 00:04:51 +08:00
}
},
"cell_type": "code",
2025-09-24 23:14:14 +08:00
"source": [
"import numpy as np\n",
2025-11-20 16:10:16 +08:00
"import pandas as pd\n",
2025-09-24 23:14:14 +08:00
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
2025-11-20 16:10:16 +08:00
"from scipy.signal import stft\n",
"from tqdm import tqdm\n",
2025-09-24 23:14:14 +08:00
"\n",
2025-11-20 16:10:16 +08:00
"# 设置绘图风格\n",
"sns.set(style=\"darkgrid\")\n",
2025-11-07 16:26:00 +08:00
"plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签\n",
"plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号\n",
"\n",
2025-11-20 16:10:16 +08:00
"class SpectralFactorAnalyzer:\n",
" def __init__(\n",
" self,\n",
" bars_per_day: int = 23,\n",
" spectral_window_days: float = 2.0,\n",
" low_freq_days: float = 2.0,\n",
" high_freq_days: float = 1.0\n",
" ):\n",
" \"\"\"\n",
" 初始化分析器,参数需与策略保持一致\n",
" \"\"\"\n",
" self.bars_per_day = bars_per_day\n",
" # 计算窗口大小 (bars)\n",
" self.window_size = int(spectral_window_days * bars_per_day)\n",
" if self.window_size % 2 != 0:\n",
" self.window_size += 1\n",
"\n",
" # 频率边界 (Hz or cycles/day)\n",
" self.low_freq_bound = 1.0 / low_freq_days\n",
" self.high_freq_bound = 1.0 / high_freq_days\n",
"\n",
" def _calculate_single_step(self, window_data: np.array):\n",
" \"\"\"\n",
" 计算单步的趋势强度,逻辑与策略完全一致\n",
" \"\"\"\n",
" # 1. 窗口内标准化 (关键步骤:消除绝对价格影响,只看形态)\n",
" if np.std(window_data) == 0:\n",
" return 0.0\n",
" normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)\n",
"\n",
" # 2. STFT 计算\n",
" try:\n",
" f, t, Zxx = stft(\n",
" normalized,\n",
" fs=self.bars_per_day,\n",
" nperseg=self.window_size,\n",
" noverlap=max(0, self.window_size // 2),\n",
" boundary=None,\n",
" padded=False\n",
" )\n",
" except:\n",
" return 0.0\n",
"\n",
" # 3. 提取有效频率和能量\n",
" valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)\n",
" f = f[valid_mask]\n",
" Zxx = Zxx[valid_mask, :]\n",
"\n",
" if Zxx.size == 0:\n",
" return 0.0\n",
"\n",
" # 取最后一个时间步的能量谱\n",
" current_energy = np.abs(Zxx[:, -1]) ** 2\n",
"\n",
" # 4. 计算能量占比\n",
" low_freq_mask = f < self.low_freq_bound\n",
" high_freq_mask = f > self.high_freq_bound\n",
"\n",
" low_energy = np.sum(current_energy[low_freq_mask]) if np.any(low_freq_mask) else 0.0\n",
" # 注意:策略中分母是 (low + high),忽略中间段\n",
" high_energy = np.sum(current_energy[high_freq_mask]) if np.any(high_freq_mask) else 0.0\n",
" total_energy = low_energy + high_energy + 1e-8\n",
"\n",
" return low_energy / total_energy\n",
"\n",
" def compute_factor(self, df: pd.DataFrame, price_col: str = 'close') -> pd.DataFrame:\n",
" \"\"\"\n",
" 在DataFrame上滚动计算因子\n",
" \"\"\"\n",
" print(f\"开始计算频域趋势强度因子 (窗口={self.window_size} bars)...\")\n",
" prices = df[price_col].values\n",
" strengths = np.full(len(prices), np.nan)\n",
"\n",
" # 滚动计算 (使用 tqdm 显示进度)\n",
" for i in tqdm(range(self.window_size, len(prices))):\n",
" window = prices[i - self.window_size : i]\n",
" strengths[i] = self._calculate_single_step(window)\n",
"\n",
" df['trend_strength'] = strengths\n",
" return df\n",
2025-11-07 16:26:00 +08:00
"\n",
2025-11-20 16:10:16 +08:00
" def analyze_and_visualize(self, df: pd.DataFrame, forward_period: int = 5):\n",
" \"\"\"\n",
" 全方位可视化分析\n",
" \"\"\"\n",
" df = df.copy().dropna(subset=['trend_strength'])\n",
"\n",
" # 计算未来收益率用于分析因子有效性\n",
" df['fwd_ret'] = df['close'].shift(-forward_period) / df['close'] - 1\n",
"\n",
" fig = plt.figure(figsize=(18, 12))\n",
" gs = fig.add_gridspec(3, 2)\n",
"\n",
" # --- 图1: 价格与因子时间序列 ---\n",
" ax1 = fig.add_subplot(gs[0, :])\n",
" ax1.set_title('价格走势 vs 频域趋势强度 (Trend Strength)', fontsize=14)\n",
" ax1.plot(df.index, df['close'], label='Close Price', color='black', alpha=0.6)\n",
" ax1.set_ylabel('Price')\n",
"\n",
" ax1_twin = ax1.twinx()\n",
" # 使用填充颜色表示强度,红色为高强度趋势\n",
" ax1_twin.fill_between(df.index, df['trend_strength'], 0, color='orange', alpha=0.3, label='Trend Strength')\n",
" ax1_twin.axhline(0.8, color='red', linestyle='--', alpha=0.8, label='Threshold (0.8)')\n",
" ax1_twin.set_ylabel('Strength (0-1)')\n",
" ax1_twin.set_ylim(0, 1.1)\n",
"\n",
" lines, labels = ax1.get_legend_handles_labels()\n",
" lines2, labels2 = ax1_twin.get_legend_handles_labels()\n",
" ax1.legend(lines + lines2, labels + labels2, loc='upper left')\n",
"\n",
" # --- 图2: 因子分布直方图 ---\n",
" ax2 = fig.add_subplot(gs[1, 0])\n",
" sns.histplot(df['trend_strength'], bins=50, kde=True, ax=ax2, color='teal')\n",
" ax2.axvline(0.8, color='red', linestyle='--')\n",
" ax2.set_title('因子分布直方图 (Distribution)', fontsize=12)\n",
" ax2.set_xlabel('Trend Strength')\n",
"\n",
" # --- 图3: 因子与未来收益波动率的关系 (肥尾效应验证) ---\n",
" # 逻辑:高强度是否意味着更高的波动/更大的单边行情?\n",
" ax3 = fig.add_subplot(gs[1, 1])\n",
" df['abs_fwd_ret'] = df['fwd_ret'].abs()\n",
" # 将强度分箱\n",
" df['strength_bin'] = pd.cut(df['trend_strength'], bins=10, labels=False)\n",
" sns.barplot(x='strength_bin', y='abs_fwd_ret', data=df, ax=ax3, palette='viridis')\n",
" ax3.set_title(f'不同强度下的未来{forward_period}周期绝对波幅', fontsize=12)\n",
" ax3.set_xlabel('Strength Decile (0=Low, 9=High)')\n",
" ax3.set_ylabel('Mean Absolute Forward Return')\n",
"\n",
" # --- 图4: 因子自相关性 (稳定性分析) ---\n",
" ax4 = fig.add_subplot(gs[2, 0])\n",
" pd.plotting.autocorrelation_plot(df['trend_strength'], ax=ax4)\n",
" ax4.set_title('因子自相关性 (Autocorrelation)', fontsize=12)\n",
" ax4.set_xlim(0, 100)\n",
"\n",
" # --- 图5: 极值特征 - 相变点后的累计收益路径 ---\n",
" # 筛选所有刚突破 0.8 的点, 看后续N天的平均表现\n",
" ax5 = fig.add_subplot(gs[2, 1])\n",
"\n",
" # 找到上穿 0.8 的时刻\n",
" breakouts = df[(df['trend_strength'] > 0.8) & (df['trend_strength'].shift(1) <= 0.8)].index\n",
"\n",
" paths = []\n",
" look_ahead = 50 # 观察后50根bar\n",
" for idx in breakouts:\n",
" if idx + look_ahead < len(df):\n",
" # 获取 loc 整数索引\n",
" loc_idx = df.index.get_loc(idx)\n",
" price_path = df['close'].iloc[loc_idx : loc_idx + look_ahead].values\n",
" # 归一化路径\n",
" norm_path = (price_path / price_path[0] - 1) * 100\n",
" paths.append(norm_path)\n",
"\n",
" if paths:\n",
" avg_path = np.mean(np.array(paths), axis=0)\n",
" # 简单的区分方向: 如果是上涨趋势和下跌趋势混合, 平均值可能为0\n",
" # 所以我们展示绝对值路径或者分开展示,这里展示所有路径的平均绝对位移\n",
" abs_paths = np.mean(np.abs(np.array(paths)), axis=0)\n",
"\n",
" ax5.plot(abs_paths, color='blue', linewidth=2, label='Avg Abs Return')\n",
" ax5.set_title(f'突破0.8阈值后的平均波动路径 ({len(paths)} events)', fontsize=12)\n",
" ax5.set_xlabel('Bars After Breakout')\n",
" ax5.set_ylabel('Cumulative Abs Return %')\n",
" else:\n",
" ax5.text(0.5, 0.5, \"No Breakout Events Found\", ha='center')\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"analyzer = SpectralFactorAnalyzer(\n",
" bars_per_day=23, # 每天24根K线\n",
" spectral_window_days=8.0, # 窗口2天 (48根K线)\n",
" low_freq_days=8.0, # 低频界限\n",
" high_freq_days=4.0 # 高频界限\n",
")\n",
"# 计算因子\n",
"df_analyzed = analyzer.compute_factor(df_raw, price_col='close')\n",
2025-11-07 16:26:00 +08:00
"\n",
2025-11-20 16:10:16 +08:00
"# 打印部分数据查看\n",
"print(df_analyzed[['close', 'trend_strength']].tail(10))\n",
2025-11-07 16:26:00 +08:00
"\n",
2025-11-20 16:10:16 +08:00
"# 可视化\n",
"analyzer.analyze_and_visualize(df_analyzed, forward_period=10)"
2025-09-24 23:14:14 +08:00
],
2025-11-20 16:10:16 +08:00
"id": "992d873f40976519",
2025-09-20 00:04:51 +08:00
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
2025-11-20 16:10:16 +08:00
"开始计算频域趋势强度因子 (窗口=184 bars)...\n"
2025-09-24 23:14:14 +08:00
]
},
{
2025-11-20 16:10:16 +08:00
"name": "stderr",
2025-09-24 23:14:14 +08:00
"output_type": "stream",
"text": [
2025-11-20 16:10:16 +08:00
"100%|██████████| 26348/26348 [00:02<00:00, 12238.54it/s]\n"
2025-09-24 23:14:14 +08:00
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
2025-11-20 16:10:16 +08:00
" close trend_strength\n",
"datetime \n",
"2025-10-27 10:30:00 3066.0 0.046805\n",
"2025-10-27 10:45:00 3070.0 0.053660\n",
"2025-10-27 11:00:00 3072.0 0.057110\n",
"2025-10-27 11:15:00 3069.0 0.057133\n",
"2025-10-27 13:30:00 3078.0 0.054750\n",
"2025-10-27 13:45:00 3073.0 0.047518\n",
"2025-10-27 14:00:00 3078.0 0.044245\n",
"2025-10-27 14:15:00 3083.0 0.037579\n",
"2025-10-27 14:30:00 3088.0 0.030604\n",
"2025-10-27 14:45:00 3100.0 0.025207\n"
2025-09-24 23:14:14 +08:00
]
},
2025-09-16 09:59:38 +08:00
{
2025-11-20 16:10:16 +08:00
"ename": "TypeError",
"evalue": "Addition/subtraction of integers and integer-arrays with Timestamp is no longer supported. Instead of adding/subtracting `n`, use `n * obj.freq`",
"output_type": "error",
"traceback": [
"\u001B[31m---------------------------------------------------------------------------\u001B[39m",
"\u001B[31mTypeError\u001B[39m Traceback (most recent call last)",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[4]\u001B[39m\u001B[32m, line 194\u001B[39m\n\u001B[32m 191\u001B[39m \u001B[38;5;28mprint\u001B[39m(df_analyzed[[\u001B[33m'\u001B[39m\u001B[33mclose\u001B[39m\u001B[33m'\u001B[39m, \u001B[33m'\u001B[39m\u001B[33mtrend_strength\u001B[39m\u001B[33m'\u001B[39m]].tail(\u001B[32m10\u001B[39m))\n\u001B[32m 193\u001B[39m \u001B[38;5;66;03m# 可视化\u001B[39;00m\n\u001B[32m--> \u001B[39m\u001B[32m194\u001B[39m \u001B[43manalyzer\u001B[49m\u001B[43m.\u001B[49m\u001B[43manalyze_and_visualize\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdf_analyzed\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mforward_period\u001B[49m\u001B[43m=\u001B[49m\u001B[32;43m10\u001B[39;49m\u001B[43m)\u001B[49m\n",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[4]\u001B[39m\u001B[32m, line 157\u001B[39m, in \u001B[36mSpectralFactorAnalyzer.analyze_and_visualize\u001B[39m\u001B[34m(self, df, forward_period)\u001B[39m\n\u001B[32m 155\u001B[39m look_ahead = \u001B[32m50\u001B[39m \u001B[38;5;66;03m# 观察后50根bar\u001B[39;00m\n\u001B[32m 156\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m idx \u001B[38;5;129;01min\u001B[39;00m breakouts:\n\u001B[32m--> \u001B[39m\u001B[32m157\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[43midx\u001B[49m\u001B[43m \u001B[49m\u001B[43m+\u001B[49m\u001B[43m \u001B[49m\u001B[43mlook_ahead\u001B[49m < \u001B[38;5;28mlen\u001B[39m(df):\n\u001B[32m 158\u001B[39m \u001B[38;5;66;03m# 获取 loc 整数索引\u001B[39;00m\n\u001B[32m 159\u001B[39m loc_idx = df.index.get_loc(idx)\n\u001B[32m 160\u001B[39m price_path = df[\u001B[33m'\u001B[39m\u001B[33mclose\u001B[39m\u001B[33m'\u001B[39m].iloc[loc_idx : loc_idx + look_ahead].values\n",
"\u001B[36mFile \u001B[39m\u001B[32mpandas/_libs/tslibs/timestamps.pyx:465\u001B[39m, in \u001B[36mpandas._libs.tslibs.timestamps._Timestamp.__add__\u001B[39m\u001B[34m()\u001B[39m\n",
"\u001B[31mTypeError\u001B[39m: Addition/subtraction of integers and integer-arrays with Timestamp is no longer supported. Instead of adding/subtracting `n`, use `n * obj.freq`"
2025-09-24 23:14:14 +08:00
]
},
2025-09-16 09:59:38 +08:00
{
"data": {
"text/plain": [
2025-11-20 16:10:16 +08:00
"<Figure size 1800x1200 with 6 Axes>"
2025-09-16 09:59:38 +08:00
],
2025-11-20 16:10:16 +08:00
"image/png": "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
2025-09-16 09:59:38 +08:00
},
"metadata": {},
2025-11-07 16:26:00 +08:00
"output_type": "display_data",
"jetTransient": {
"display_id": null
}
2025-09-16 09:59:38 +08:00
}
],
2025-11-20 16:10:16 +08:00
"execution_count": 4
2025-09-16 09:59:38 +08:00
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}